summaryrefslogtreecommitdiff
path: root/java/com/android/contacts
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/contacts')
-rw-r--r--java/com/android/contacts/common/AndroidManifest.xml39
-rw-r--r--java/com/android/contacts/common/Bindings.java52
-rw-r--r--java/com/android/contacts/common/ClipboardUtils.java55
-rw-r--r--java/com/android/contacts/common/Collapser.java95
-rw-r--r--java/com/android/contacts/common/ContactPhotoManager.java487
-rw-r--r--java/com/android/contacts/common/ContactPhotoManagerImpl.java1262
-rw-r--r--java/com/android/contacts/common/ContactPresenceIconUtil.java46
-rw-r--r--java/com/android/contacts/common/ContactStatusUtil.java44
-rw-r--r--java/com/android/contacts/common/ContactTileLoaderFactory.java64
-rw-r--r--java/com/android/contacts/common/ContactsUtils.java265
-rw-r--r--java/com/android/contacts/common/GeoUtil.java55
-rw-r--r--java/com/android/contacts/common/GroupMetaData.java76
-rw-r--r--java/com/android/contacts/common/MoreContactUtils.java251
-rw-r--r--java/com/android/contacts/common/bindings/ContactsCommonBindings.java25
-rw-r--r--java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java24
-rw-r--r--java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java27
-rw-r--r--java/com/android/contacts/common/compat/CallCompat.java45
-rw-r--r--java/com/android/contacts/common/compat/CallableCompat.java36
-rw-r--r--java/com/android/contacts/common/compat/ContactsCompat.java57
-rw-r--r--java/com/android/contacts/common/compat/DirectoryCompat.java51
-rw-r--r--java/com/android/contacts/common/compat/PhoneAccountCompat.java104
-rw-r--r--java/com/android/contacts/common/compat/PhoneCompat.java36
-rw-r--r--java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java174
-rw-r--r--java/com/android/contacts/common/compat/TelephonyManagerCompat.java213
-rw-r--r--java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java302
-rw-r--r--java/com/android/contacts/common/database/ContactUpdateUtils.java49
-rw-r--r--java/com/android/contacts/common/database/EmptyCursor.java84
-rw-r--r--java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java73
-rw-r--r--java/com/android/contacts/common/dialog/CallSubjectDialog.java607
-rw-r--r--java/com/android/contacts/common/dialog/ClearFrequentsDialog.java88
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java28
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java45
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java27
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java29
-rw-r--r--java/com/android/contacts/common/format/FormatUtils.java181
-rw-r--r--java/com/android/contacts/common/format/TextHighlighter.java93
-rw-r--r--java/com/android/contacts/common/format/testing/SpannedTestUtils.java85
-rw-r--r--java/com/android/contacts/common/lettertiles/LetterTileDrawable.java382
-rw-r--r--java/com/android/contacts/common/list/AutoScrollListView.java125
-rw-r--r--java/com/android/contacts/common/list/ContactEntry.java57
-rw-r--r--java/com/android/contacts/common/list/ContactEntryListAdapter.java742
-rw-r--r--java/com/android/contacts/common/list/ContactEntryListFragment.java862
-rw-r--r--java/com/android/contacts/common/list/ContactListAdapter.java232
-rw-r--r--java/com/android/contacts/common/list/ContactListFilter.java297
-rw-r--r--java/com/android/contacts/common/list/ContactListFilterController.java170
-rw-r--r--java/com/android/contacts/common/list/ContactListItemView.java1513
-rw-r--r--java/com/android/contacts/common/list/ContactListPinnedHeaderView.java70
-rw-r--r--java/com/android/contacts/common/list/ContactTileView.java171
-rw-r--r--java/com/android/contacts/common/list/ContactsSectionIndexer.java119
-rw-r--r--java/com/android/contacts/common/list/DefaultContactListAdapter.java216
-rw-r--r--java/com/android/contacts/common/list/DirectoryListLoader.java201
-rw-r--r--java/com/android/contacts/common/list/DirectoryPartition.java179
-rw-r--r--java/com/android/contacts/common/list/IndexerListAdapter.java214
-rw-r--r--java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java39
-rw-r--r--java/com/android/contacts/common/list/PhoneNumberListAdapter.java583
-rw-r--r--java/com/android/contacts/common/list/PhoneNumberPickerFragment.java402
-rw-r--r--java/com/android/contacts/common/list/PinnedHeaderListAdapter.java159
-rw-r--r--java/com/android/contacts/common/list/PinnedHeaderListView.java563
-rw-r--r--java/com/android/contacts/common/list/ViewPagerTabStrip.java109
-rw-r--r--java/com/android/contacts/common/list/ViewPagerTabs.java317
-rw-r--r--java/com/android/contacts/common/location/CountryDetector.java221
-rw-r--r--java/com/android/contacts/common/location/UpdateCountryService.java104
-rw-r--r--java/com/android/contacts/common/model/AccountTypeManager.java813
-rw-r--r--java/com/android/contacts/common/model/BuilderWrapper.java53
-rw-r--r--java/com/android/contacts/common/model/CPOWrapper.java50
-rw-r--r--java/com/android/contacts/common/model/Contact.java384
-rw-r--r--java/com/android/contacts/common/model/ContactLoader.java998
-rw-r--r--java/com/android/contacts/common/model/RawContact.java351
-rw-r--r--java/com/android/contacts/common/model/account/AccountType.java501
-rw-r--r--java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java103
-rw-r--r--java/com/android/contacts/common/model/account/AccountWithDataSet.java229
-rw-r--r--java/com/android/contacts/common/model/account/BaseAccountType.java1890
-rw-r--r--java/com/android/contacts/common/model/account/ExchangeAccountType.java365
-rw-r--r--java/com/android/contacts/common/model/account/ExternalAccountType.java443
-rw-r--r--java/com/android/contacts/common/model/account/FallbackAccountType.java77
-rw-r--r--java/com/android/contacts/common/model/account/GoogleAccountType.java206
-rw-r--r--java/com/android/contacts/common/model/account/SamsungAccountType.java235
-rw-r--r--java/com/android/contacts/common/model/dataitem/DataItem.java258
-rw-r--r--java/com/android/contacts/common/model/dataitem/DataKind.java132
-rw-r--r--java/com/android/contacts/common/model/dataitem/EmailDataItem.java47
-rw-r--r--java/com/android/contacts/common/model/dataitem/EventDataItem.java62
-rw-r--r--java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java40
-rw-r--r--java/com/android/contacts/common/model/dataitem/IdentityDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/ImDataItem.java109
-rw-r--r--java/com/android/contacts/common/model/dataitem/NicknameDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/NoteDataItem.java35
-rw-r--r--java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java64
-rw-r--r--java/com/android/contacts/common/model/dataitem/PhoneDataItem.java76
-rw-r--r--java/com/android/contacts/common/model/dataitem/PhotoDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/RelationDataItem.java62
-rw-r--r--java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java40
-rw-r--r--java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java100
-rw-r--r--java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java68
-rw-r--r--java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java39
-rw-r--r--java/com/android/contacts/common/preference/ContactsPreferences.java269
-rw-r--r--java/com/android/contacts/common/preference/DisplayOrderPreference.java89
-rw-r--r--java/com/android/contacts/common/preference/SortOrderPreference.java89
-rw-r--r--java/com/android/contacts/common/res/color/popup_menu_color.xml20
-rw-r--r--java/com/android/contacts/common/res/color/tab_text_color.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.pngbin0 -> 1115 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.pngbin0 -> 612 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.pngbin0 -> 2477 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.pngbin0 -> 373 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.pngbin0 -> 609 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.pngbin0 -> 370 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.pngbin0 -> 389 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.pngbin0 -> 525 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.pngbin0 -> 485 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.pngbin0 -> 799 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.pngbin0 -> 1954 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.pngbin0 -> 1922 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.pngbin0 -> 220 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.pngbin0 -> 1439 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.pngbin0 -> 1416 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.pngbin0 -> 515 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.pngbin0 -> 1438 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.pngbin0 -> 1211 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.pngbin0 -> 1414 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.pngbin0 -> 167 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.pngbin0 -> 273 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.pngbin0 -> 289 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.pngbin0 -> 828 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.pngbin0 -> 413 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.pngbin0 -> 544 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.pngbin0 -> 370 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.pngbin0 -> 269 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.pngbin0 -> 3607 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.pngbin0 -> 154 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.pngbin0 -> 224 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.pngbin0 -> 159 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.pngbin0 -> 205 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.pngbin0 -> 267 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.pngbin0 -> 219 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.pngbin0 -> 234 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.pngbin0 -> 191 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.pngbin0 -> 258 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.pngbin0 -> 178 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.pngbin0 -> 234 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.pngbin0 -> 180 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.pngbin0 -> 186 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.pngbin0 -> 1666 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.pngbin0 -> 1034 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.pngbin0 -> 2486 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.pngbin0 -> 243 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.pngbin0 -> 234 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.pngbin0 -> 196 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.pngbin0 -> 255 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.pngbin0 -> 781 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.pngbin0 -> 578 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.pngbin0 -> 2040 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.pngbin0 -> 246 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.pngbin0 -> 266 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.pngbin0 -> 572 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.pngbin0 -> 290 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.pngbin0 -> 297 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.pngbin0 -> 320 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.pngbin0 -> 607 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.pngbin0 -> 1266 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.pngbin0 -> 1270 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.pngbin0 -> 171 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.pngbin0 -> 1052 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.pngbin0 -> 1021 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.pngbin0 -> 424 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.pngbin0 -> 1034 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.pngbin0 -> 1018 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.pngbin0 -> 130 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.pngbin0 -> 188 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.pngbin0 -> 204 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.pngbin0 -> 476 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.pngbin0 -> 299 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.pngbin0 -> 504 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.pngbin0 -> 265 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.pngbin0 -> 216 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.pngbin0 -> 2120 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.pngbin0 -> 151 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.pngbin0 -> 188 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.pngbin0 -> 155 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.pngbin0 -> 198 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.pngbin0 -> 199 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.pngbin0 -> 1659 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.pngbin0 -> 1005 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.pngbin0 -> 2478 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.pngbin0 -> 1451 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.pngbin0 -> 765 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.pngbin0 -> 2916 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.pngbin0 -> 420 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.pngbin0 -> 449 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.pngbin0 -> 814 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.pngbin0 -> 426 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.pngbin0 -> 461 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.pngbin0 -> 659 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.pngbin0 -> 655 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.pngbin0 -> 1034 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.pngbin0 -> 2650 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.pngbin0 -> 2632 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.pngbin0 -> 287 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.pngbin0 -> 1844 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.pngbin0 -> 1815 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.pngbin0 -> 593 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.pngbin0 -> 1830 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.pngbin0 -> 1607 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.pngbin0 -> 1827 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.pngbin0 -> 204 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.pngbin0 -> 312 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.pngbin0 -> 329 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.pngbin0 -> 1009 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.pngbin0 -> 439 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.pngbin0 -> 620 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.pngbin0 -> 405 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.pngbin0 -> 301 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.pngbin0 -> 4894 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.pngbin0 -> 245 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.pngbin0 -> 162 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.pngbin0 -> 163 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.pngbin0 -> 210 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.pngbin0 -> 267 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.pngbin0 -> 2100 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.pngbin0 -> 1376 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.pngbin0 -> 2541 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.pngbin0 -> 597 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.pngbin0 -> 647 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.pngbin0 -> 1465 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.pngbin0 -> 668 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.pngbin0 -> 604 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.pngbin0 -> 971 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.pngbin0 -> 953 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.pngbin0 -> 1546 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.pngbin0 -> 3338 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.pngbin0 -> 3381 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.pngbin0 -> 414 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.pngbin0 -> 2357 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.pngbin0 -> 2363 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.pngbin0 -> 1381 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.pngbin0 -> 2111 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.pngbin0 -> 2119 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.pngbin0 -> 2117 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.pngbin0 -> 269 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.pngbin0 -> 440 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.pngbin0 -> 464 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.pngbin0 -> 1517 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.pngbin0 -> 603 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.pngbin0 -> 837 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.pngbin0 -> 551 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.pngbin0 -> 398 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.pngbin0 -> 7976 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.pngbin0 -> 1140 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.pngbin0 -> 1147 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.pngbin0 -> 465 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.pngbin0 -> 2571 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.pngbin0 -> 1512 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.pngbin0 -> 2915 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.pngbin0 -> 778 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.pngbin0 -> 853 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.pngbin0 -> 1688 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.pngbin0 -> 612 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.pngbin0 -> 1311 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.pngbin0 -> 1279 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.pngbin0 -> 342 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.pngbin0 -> 610 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.pngbin0 -> 2135 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.pngbin0 -> 1579 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.pngbin0 -> 481 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.pngbin0 -> 11277 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable/dialog_background_material.xml23
-rw-r--r--java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_back_arrow.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_call.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_message_24dp.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_more_vert.xml9
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_search_video_call.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_tab_all.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_tab_groups.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_tab_starred.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_work_profile.xml16
-rw-r--r--java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/item_background_material_dark.xml23
-rw-r--r--java/com/android/contacts/common/res/drawable/item_background_material_light.xml23
-rw-r--r--java/com/android/contacts/common/res/drawable/list_item_activated_background.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml7
-rw-r--r--java/com/android/contacts/common/res/drawable/unread_count_background.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml22
-rw-r--r--java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml48
-rw-r--r--java/com/android/contacts/common/res/layout/account_filter_header.xml44
-rw-r--r--java/com/android/contacts/common/res/layout/account_selector_list_item.xml57
-rw-r--r--java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml56
-rw-r--r--java/com/android/contacts/common/res/layout/call_subject_history.xml33
-rw-r--r--java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml29
-rw-r--r--java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml27
-rw-r--r--java/com/android/contacts/common/res/layout/contact_list_card.xml39
-rw-r--r--java/com/android/contacts/common/res/layout/contact_list_content.xml61
-rw-r--r--java/com/android/contacts/common/res/layout/default_account_checkbox.xml36
-rw-r--r--java/com/android/contacts/common/res/layout/dialog_call_subject.xml159
-rw-r--r--java/com/android/contacts/common/res/layout/directory_header.xml55
-rw-r--r--java/com/android/contacts/common/res/layout/list_separator.xml27
-rw-r--r--java/com/android/contacts/common/res/layout/search_bar_expanded.xml62
-rw-r--r--java/com/android/contacts/common/res/layout/select_account_list_item.xml56
-rw-r--r--java/com/android/contacts/common/res/layout/unread_count_tab.xml43
-rw-r--r--java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.pngbin0 -> 3169 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.pngbin0 -> 2062 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.pngbin0 -> 4430 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.pngbin0 -> 7228 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.pngbin0 -> 10065 bytes
-rw-r--r--java/com/android/contacts/common/res/values-ja/donottranslate_config.xml20
-rw-r--r--java/com/android/contacts/common/res/values-ko/donottranslate_config.xml17
-rw-r--r--java/com/android/contacts/common/res/values-land/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-sw600dp-land/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-sw600dp/dimens.xml29
-rw-r--r--java/com/android/contacts/common/res/values-sw600dp/integers.xml24
-rw-r--r--java/com/android/contacts/common/res/values-sw720dp-land/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-sw720dp/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml17
-rw-r--r--java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml17
-rw-r--r--java/com/android/contacts/common/res/values/animation_constants.xml19
-rw-r--r--java/com/android/contacts/common/res/values/attrs.xml83
-rw-r--r--java/com/android/contacts/common/res/values/colors.xml158
-rw-r--r--java/com/android/contacts/common/res/values/dimens.xml161
-rw-r--r--java/com/android/contacts/common/res/values/donottranslate_config.xml95
-rw-r--r--java/com/android/contacts/common/res/values/ids.xml30
-rw-r--r--java/com/android/contacts/common/res/values/integers.xml39
-rw-r--r--java/com/android/contacts/common/res/values/strings.xml798
-rw-r--r--java/com/android/contacts/common/res/values/styles.xml97
-rw-r--r--java/com/android/contacts/common/testing/InjectedServices.java65
-rw-r--r--java/com/android/contacts/common/util/AccountFilterUtil.java125
-rw-r--r--java/com/android/contacts/common/util/BitmapUtil.java167
-rw-r--r--java/com/android/contacts/common/util/CommonDateUtils.java37
-rw-r--r--java/com/android/contacts/common/util/Constants.java28
-rw-r--r--java/com/android/contacts/common/util/ContactDisplayUtils.java307
-rw-r--r--java/com/android/contacts/common/util/ContactListViewUtils.java89
-rw-r--r--java/com/android/contacts/common/util/ContactLoaderUtils.java78
-rw-r--r--java/com/android/contacts/common/util/DateUtils.java283
-rw-r--r--java/com/android/contacts/common/util/FabUtil.java71
-rw-r--r--java/com/android/contacts/common/util/MaterialColorMapUtils.java181
-rw-r--r--java/com/android/contacts/common/util/NameConverter.java242
-rw-r--r--java/com/android/contacts/common/util/SearchUtil.java198
-rw-r--r--java/com/android/contacts/common/util/StopWatch.java100
-rw-r--r--java/com/android/contacts/common/util/TelephonyManagerUtils.java45
-rw-r--r--java/com/android/contacts/common/util/TrafficStatsTags.java22
-rw-r--r--java/com/android/contacts/common/util/UriUtils.java90
-rw-r--r--java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java43
-rw-r--r--java/com/android/contacts/common/widget/FloatingActionButtonController.java226
-rw-r--r--java/com/android/contacts/common/widget/LayoutSuppressingImageView.java39
-rw-r--r--java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java297
358 files changed, 27457 insertions, 0 deletions
diff --git a/java/com/android/contacts/common/AndroidManifest.xml b/java/com/android/contacts/common/AndroidManifest.xml
new file mode 100644
index 000000000..eae70cd30
--- /dev/null
+++ b/java/com/android/contacts/common/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.contacts.common">
+
+ <application>
+
+ <activity
+ android:name="com.android.contacts.common.dialog.CallSubjectDialog"
+ android:theme="@style/Theme.CallSubjectDialogTheme"
+ android:windowSoftInputMode="stateVisible|adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ </intent-filter>
+ </activity>
+
+ <!-- Broadcast receiver that passively listens to location updates -->
+ <receiver android:name="com.android.contacts.common.location.CountryDetector$LocationChangedReceiver"/>
+
+ <!-- IntentService to update the user's current country -->
+ <service
+ android:exported="false"
+ android:name="com.android.contacts.common.location.UpdateCountryService"/>
+ </application>
+</manifest>
+
diff --git a/java/com/android/contacts/common/Bindings.java b/java/com/android/contacts/common/Bindings.java
new file mode 100644
index 000000000..29cf7950a
--- /dev/null
+++ b/java/com/android/contacts/common/Bindings.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import com.android.contacts.common.bindings.ContactsCommonBindings;
+import com.android.contacts.common.bindings.ContactsCommonBindingsFactory;
+import com.android.contacts.common.bindings.ContactsCommonBindingsStub;
+import java.util.Objects;
+
+/** Accessor for the contacts common bindings. */
+public class Bindings {
+
+ private static ContactsCommonBindings instance;
+
+ private Bindings() {}
+
+ public static ContactsCommonBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof ContactsCommonBindingsFactory) {
+ instance = ((ContactsCommonBindingsFactory) application).newContactsCommonBindings();
+ }
+
+ if (instance == null) {
+ instance = new ContactsCommonBindingsStub();
+ }
+ return instance;
+ }
+
+ public static void setForTesting(ContactsCommonBindings testInstance) {
+ instance = testInstance;
+ }
+}
diff --git a/java/com/android/contacts/common/ClipboardUtils.java b/java/com/android/contacts/common/ClipboardUtils.java
new file mode 100644
index 000000000..9345b0f9c
--- /dev/null
+++ b/java/com/android/contacts/common/ClipboardUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+public class ClipboardUtils {
+
+ private static final String TAG = "ClipboardUtils";
+
+ private ClipboardUtils() {}
+
+ /**
+ * Copy a text to clipboard.
+ *
+ * @param context Context
+ * @param label Label to show to the user describing this clip.
+ * @param text Text to copy.
+ * @param showToast If {@code true}, a toast is shown to the user.
+ */
+ public static void copyText(
+ Context context, CharSequence label, CharSequence text, boolean showToast) {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+
+ ClipboardManager clipboardManager =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = ClipData.newPlainText(label == null ? "" : label, text);
+ clipboardManager.setPrimaryClip(clipData);
+
+ if (showToast) {
+ String toastText = context.getString(R.string.toast_text_copied);
+ Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/Collapser.java b/java/com/android/contacts/common/Collapser.java
new file mode 100644
index 000000000..0b5c48bf2
--- /dev/null
+++ b/java/com/android/contacts/common/Collapser.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class used for collapsing data items into groups of similar items. The data items that should be
+ * collapsible should implement the Collapsible interface. The class also contains a utility
+ * function that takes an ArrayList of items and returns a list of the same items collapsed into
+ * groups.
+ */
+public final class Collapser {
+
+ /*
+ * The Collapser uses an n^2 algorithm so we don't want it to run on
+ * lists beyond a certain size. This specifies the maximum size to collapse.
+ */
+ private static final int MAX_LISTSIZE_TO_COLLAPSE = 20;
+
+ /*
+ * This utility class cannot be instantiated.
+ */
+ private Collapser() {}
+
+ /**
+ * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed if
+ * {@link Collapsible#shouldCollapseWith(Object)} returns true, and are collapsed through the
+ * {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
+ *
+ * @param list List of Objects of type <T extends Collapsible<T>> to be collapsed.
+ */
+ public static <T extends Collapsible<T>> void collapseList(List<T> list, Context context) {
+
+ int listSize = list.size();
+ // The algorithm below is n^2 so don't run on long lists
+ if (listSize > MAX_LISTSIZE_TO_COLLAPSE) {
+ return;
+ }
+
+ for (int i = 0; i < listSize; i++) {
+ T iItem = list.get(i);
+ if (iItem != null) {
+ for (int j = i + 1; j < listSize; j++) {
+ T jItem = list.get(j);
+ if (jItem != null) {
+ if (iItem.shouldCollapseWith(jItem, context)) {
+ iItem.collapseWith(jItem);
+ list.set(j, null);
+ } else if (jItem.shouldCollapseWith(iItem, context)) {
+ jItem.collapseWith(iItem);
+ list.set(i, null);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Remove the null items
+ Iterator<T> itr = list.iterator();
+ while (itr.hasNext()) {
+ if (itr.next() == null) {
+ itr.remove();
+ }
+ }
+ }
+
+ /*
+ * Interface implemented by data types that can be collapsed into groups of similar data. This
+ * can be used for example to collapse similar contact data items into a single item.
+ */
+ public interface Collapsible<T> {
+
+ void collapseWith(T t);
+
+ boolean shouldCollapseWith(T t, Context context);
+ }
+}
diff --git a/java/com/android/contacts/common/ContactPhotoManager.java b/java/com/android/contacts/common/ContactPhotoManager.java
new file mode 100644
index 000000000..834471047
--- /dev/null
+++ b/java/com/android/contacts/common/ContactPhotoManager.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Asynchronously loads contact photos and maintains a cache of photos. */
+public abstract class ContactPhotoManager implements ComponentCallbacks2 {
+
+ /** Contact type constants used for default letter images */
+ public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON;
+
+ public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
+ public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
+ public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
+ /** Scale and offset default constants used for default letter images */
+ public static final float SCALE_DEFAULT = 1.0f;
+
+ public static final float OFFSET_DEFAULT = 0.0f;
+ public static final boolean IS_CIRCULAR_DEFAULT = false;
+ // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check.
+ // LINT.DoNotSubmitIf(true)
+ static final boolean DEBUG = false;
+ // LINT.DoNotSubmitIf(true)
+ static final boolean DEBUG_SIZES = false;
+ /** Uri-related constants used for default letter images */
+ private static final String DISPLAY_NAME_PARAM_KEY = "display_name";
+
+ private static final String IDENTIFIER_PARAM_KEY = "identifier";
+ private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
+ private static final String SCALE_PARAM_KEY = "scale";
+ private static final String OFFSET_PARAM_KEY = "offset";
+ private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
+ private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
+ private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
+ public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
+ private static ContactPhotoManager sInstance;
+
+ /**
+ * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile
+ * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri
+ * is not guaranteed to remain the same across application versions, so the actual uri should
+ * never be persisted in long-term storage and reused.
+ *
+ * @param request A {@link DefaultImageRequest} object with the fields configured to return a
+ * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link
+ * #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to
+ * request a default contact image, drawn as a letter tile using the parameters as configured
+ * in the provided {@link DefaultImageRequest}
+ */
+ public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
+ final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
+ if (request != null) {
+ if (!TextUtils.isEmpty(request.displayName)) {
+ builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
+ }
+ if (!TextUtils.isEmpty(request.identifier)) {
+ builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
+ }
+ if (request.contactType != TYPE_DEFAULT) {
+ builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType));
+ }
+ if (request.scale != SCALE_DEFAULT) {
+ builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
+ }
+ if (request.offset != OFFSET_DEFAULT) {
+ builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
+ }
+ if (request.isCircular != IS_CIRCULAR_DEFAULT) {
+ builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby
+ * Places can be identified as business photo URLs rather than URLs for personal contact photos.
+ *
+ * @param photoUrl The photo URL to modify.
+ * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
+ */
+ public static String appendBusinessContactType(String photoUrl) {
+ Uri uri = Uri.parse(photoUrl);
+ Builder builder = uri.buildUpon();
+ builder.encodedFragment(String.valueOf(TYPE_BUSINESS));
+ return builder.build().toString();
+ }
+
+ /**
+ * Removes the contact type information stored in the photo URI encoded fragment.
+ *
+ * @param photoUri The photo URI to remove the contact type from.
+ * @return The photo URI with contact type removed.
+ */
+ public static Uri removeContactType(Uri photoUri) {
+ String encodedFragment = photoUri.getEncodedFragment();
+ if (!TextUtils.isEmpty(encodedFragment)) {
+ Builder builder = photoUri.buildUpon();
+ builder.encodedFragment(null);
+ return builder.build();
+ }
+ return photoUri;
+ }
+
+ /**
+ * Inspects a photo URI to determine if the photo URI represents a business.
+ *
+ * @param photoUri The URI to inspect.
+ * @return Whether the URI represents a business photo or not.
+ */
+ public static boolean isBusinessContactUri(Uri photoUri) {
+ if (photoUri == null) {
+ return false;
+ }
+
+ String encodedFragment = photoUri.getEncodedFragment();
+ return !TextUtils.isEmpty(encodedFragment)
+ && encodedFragment.equals(String.valueOf(TYPE_BUSINESS));
+ }
+
+ protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
+ final DefaultImageRequest request =
+ new DefaultImageRequest(
+ uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
+ uri.getQueryParameter(IDENTIFIER_PARAM_KEY),
+ false);
+ try {
+ String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
+ if (!TextUtils.isEmpty(contactType)) {
+ request.contactType = Integer.valueOf(contactType);
+ }
+
+ String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
+ if (!TextUtils.isEmpty(scale)) {
+ request.scale = Float.valueOf(scale);
+ }
+
+ String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
+ if (!TextUtils.isEmpty(offset)) {
+ request.offset = Float.valueOf(offset);
+ }
+
+ String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
+ if (!TextUtils.isEmpty(isCircular)) {
+ request.isCircular = Boolean.valueOf(isCircular);
+ }
+ } catch (NumberFormatException e) {
+ LogUtil.w(
+ "ContactPhotoManager.getDefaultImageRequestFromUri",
+ "Invalid DefaultImageRequest image parameters provided, ignoring and using "
+ + "defaults.");
+ }
+
+ return request;
+ }
+
+ public static ContactPhotoManager getInstance(Context context) {
+ if (sInstance == null) {
+ Context applicationContext = context.getApplicationContext();
+ sInstance = createContactPhotoManager(applicationContext);
+ applicationContext.registerComponentCallbacks(sInstance);
+ if (PermissionsUtil.hasContactsPermissions(context)) {
+ sInstance.preloadPhotosInBackground();
+ }
+ }
+ return sInstance;
+ }
+
+ public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
+ return new ContactPhotoManagerImpl(context);
+ }
+
+ @VisibleForTesting
+ public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
+ sInstance = photoManager;
+ }
+
+ protected boolean isDefaultImageUri(Uri uri) {
+ return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
+ }
+
+ /**
+ * Load thumbnail image into the supplied image view. If the photo is already cached, it is
+ * displayed immediately. Otherwise a request is sent to load the photo from the database.
+ */
+ public abstract void loadThumbnail(
+ ImageView view,
+ long photoId,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider);
+
+ /**
+ * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest,
+ * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
+ */
+ public final void loadThumbnail(
+ ImageView view,
+ long photoId,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest) {
+ loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
+ }
+
+ /**
+ * Load photo into the supplied image view. If the photo is already cached, it is displayed
+ * immediately. Otherwise a request is sent to load the photo from the location specified by the
+ * URI.
+ *
+ * @param view The target view
+ * @param photoUri The uri of the photo to load
+ * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is
+ * useful if the source image can be a lot bigger that the target, so that the decoding is
+ * done using efficient sampling. If requestedExtent is specified, no sampling of the image is
+ * performed
+ * @param darkTheme Whether the background is dark. This is used for default avatars
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer
+ * to an existing image)
+ */
+ public abstract void loadPhoto(
+ ImageView view,
+ Uri photoUri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider);
+
+ /**
+ * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
+ * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup
+ * keys.
+ *
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ */
+ public final void loadPhoto(
+ ImageView view,
+ Uri photoUri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest) {
+ loadPhoto(
+ view,
+ photoUri,
+ requestedExtent,
+ darkTheme,
+ isCircular,
+ defaultImageRequest,
+ DEFAULT_AVATAR);
+ }
+
+ /**
+ * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
+ * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is
+ * a thumbnail.
+ *
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ */
+ public final void loadDirectoryPhoto(
+ ImageView view,
+ Uri photoUri,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest) {
+ loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
+ }
+
+ /**
+ * Remove photo from the supplied image view. This also cancels current pending load request
+ * inside this photo manager.
+ */
+ public abstract void removePhoto(ImageView view);
+
+ /** Cancels all pending requests to load photos asynchronously. */
+ public abstract void cancelPendingRequests(View fragmentRootView);
+
+ /** Temporarily stops loading photos from the database. */
+ public abstract void pause();
+
+ /** Resumes loading photos from the database. */
+ public abstract void resume();
+
+ /**
+ * Marks all cached photos for reloading. We can continue using cache but should also make sure
+ * the photos haven't changed in the background and notify the views if so.
+ */
+ public abstract void refreshCache();
+
+ /** Initiates a background process that over time will fill up cache with preload photos. */
+ public abstract void preloadPhotosInBackground();
+
+ // ComponentCallbacks2
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {}
+
+ // ComponentCallbacks2
+ @Override
+ public void onLowMemory() {}
+
+ // ComponentCallbacks2
+ @Override
+ public void onTrimMemory(int level) {}
+
+ /**
+ * Contains fields used to contain contact details and other user-defined settings that might be
+ * used by the ContactPhotoManager to generate a default contact image. This contact image takes
+ * the form of a letter or bitmap drawn on top of a colored tile.
+ */
+ public static class DefaultImageRequest {
+
+ /**
+ * Used to indicate that a drawable that represents a contact without any contact details should
+ * be returned.
+ */
+ public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
+ /**
+ * Used to indicate that a drawable that represents a business without a business photo should
+ * be returned.
+ */
+ public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
+ new DefaultImageRequest(null, null, TYPE_BUSINESS, false);
+ /**
+ * Used to indicate that a circular drawable that represents a contact without any contact
+ * details should be returned.
+ */
+ public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
+ new DefaultImageRequest(null, null, true);
+ /**
+ * Used to indicate that a circular drawable that represents a business without a business photo
+ * should be returned.
+ */
+ public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
+ new DefaultImageRequest(null, null, TYPE_BUSINESS, true);
+ /** The contact's display name. The display name is used to */
+ public String displayName;
+ /**
+ * A unique and deterministic string that can be used to identify this contact. This is usually
+ * the contact's lookup key, but other contact details can be used as well, especially for
+ * non-local or temporary contacts that might not have a lookup key. This is used to determine
+ * the color of the tile.
+ */
+ public String identifier;
+ /**
+ * The type of this contact. This contact type may be used to decide the kind of image to use in
+ * the case where a unique letter cannot be generated from the contact's display name and
+ * identifier. See: {@link #TYPE_PERSON} {@link #TYPE_BUSINESS} {@link #TYPE_PERSON} {@link
+ * #TYPE_DEFAULT}
+ */
+ public int contactType = TYPE_DEFAULT;
+ /**
+ * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of
+ * 0.0f to 2.0f). The default value is 1.0f.
+ */
+ public float scale = SCALE_DEFAULT;
+ /**
+ * The amount to vertically offset the letter or image to within the tile. The provided offset
+ * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted
+ * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be
+ * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f,
+ * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn
+ * on, which means it will be drawn with the center of the letter starting at the bottom edge of
+ * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center
+ * of the tile.
+ */
+ public float offset = OFFSET_DEFAULT;
+ /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */
+ public boolean isCircular = false;
+
+ public DefaultImageRequest() {}
+
+ public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
+ this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
+ }
+
+ public DefaultImageRequest(
+ String displayName, String identifier, int contactType, boolean isCircular) {
+ this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
+ }
+
+ public DefaultImageRequest(
+ String displayName,
+ String identifier,
+ int contactType,
+ float scale,
+ float offset,
+ boolean isCircular) {
+ this.displayName = displayName;
+ this.identifier = identifier;
+ this.contactType = contactType;
+ this.scale = scale;
+ this.offset = offset;
+ this.isCircular = isCircular;
+ }
+ }
+
+ public abstract static class DefaultImageProvider {
+
+ /**
+ * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or
+ * height). If darkTheme is set, the avatar is one that looks better on dark background
+ *
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ */
+ public abstract void applyDefaultImage(
+ ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest);
+ }
+
+ /**
+ * A default image provider that applies a letter tile consisting of a colored background and a
+ * letter in the foreground as the default image for a contact. The color of the background and
+ * the type of letter is decided based on the contact's details.
+ */
+ private static class LetterTileDefaultImageProvider extends DefaultImageProvider {
+
+ public static Drawable getDefaultImageForContact(
+ Resources resources, DefaultImageRequest defaultImageRequest) {
+ final LetterTileDrawable drawable = new LetterTileDrawable(resources);
+ final int tileShape =
+ defaultImageRequest.isCircular
+ ? LetterTileDrawable.SHAPE_CIRCLE
+ : LetterTileDrawable.SHAPE_RECTANGLE;
+ if (defaultImageRequest != null) {
+ // If the contact identifier is null or empty, fallback to the
+ // displayName. In that case, use {@code null} for the contact's
+ // display name so that a default bitmap will be used instead of a
+ // letter
+ if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
+ drawable.setCanonicalDialerLetterTileDetails(
+ null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType);
+ } else {
+ drawable.setCanonicalDialerLetterTileDetails(
+ defaultImageRequest.displayName,
+ defaultImageRequest.identifier,
+ tileShape,
+ defaultImageRequest.contactType);
+ }
+ drawable.setScale(defaultImageRequest.scale);
+ drawable.setOffset(defaultImageRequest.offset);
+ }
+ return drawable;
+ }
+
+ @Override
+ public void applyDefaultImage(
+ ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) {
+ final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest);
+ view.setImageDrawable(drawable);
+ }
+ }
+}
+
diff --git a/java/com/android/contacts/common/ContactPhotoManagerImpl.java b/java/com/android/contacts/common/ContactPhotoManagerImpl.java
new file mode 100644
index 000000000..2e6ff9fdc
--- /dev/null
+++ b/java/com/android/contacts/common/ContactPhotoManagerImpl.java
@@ -0,0 +1,1262 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common;
+
+import android.app.ActivityManager;
+import android.content.ComponentCallbacks2;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.ThumbnailUtils;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.UiThread;
+import android.support.annotation.WorkerThread;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import android.text.TextUtils;
+import android.util.LruCache;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.TrafficStatsTags;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
+
+ private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
+
+ private static final int FADE_TRANSITION_DURATION = 200;
+
+ /**
+ * Type of message sent by the UI thread to itself to indicate that some photos need to be loaded.
+ */
+ private static final int MESSAGE_REQUEST_LOADING = 1;
+
+ /** Type of message sent by the loader thread to indicate that some photos have been loaded. */
+ private static final int MESSAGE_PHOTOS_LOADED = 2;
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static final String[] COLUMNS = new String[] {Photo._ID, Photo.PHOTO};
+
+ /**
+ * Dummy object used to indicate that a bitmap for a given key could not be stored in the cache.
+ */
+ private static final BitmapHolder BITMAP_UNAVAILABLE;
+ /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
+ private static final int HOLDER_CACHE_SIZE = 2000000;
+ /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
+ private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
+ /** Height/width of a thumbnail image */
+ private static int mThumbnailSize;
+
+ static {
+ BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
+ BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
+ }
+
+ private final Context mContext;
+ /**
+ * An LRU cache for bitmap holders. The cache contains bytes for photos just as they come from the
+ * database. Each holder has a soft reference to the actual bitmap.
+ */
+ private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
+ /** Cache size threshold at which bitmaps will not be preloaded. */
+ private final int mBitmapHolderCacheRedZoneBytes;
+ /**
+ * Level 2 LRU cache for bitmaps. This is a smaller cache that holds the most recently used
+ * bitmaps to save time on decoding them from bytes (the bytes are stored in {@link
+ * #mBitmapHolderCache}.
+ */
+ private final LruCache<Object, Bitmap> mBitmapCache;
+ /**
+ * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. The
+ * request may swapped out before the photo loading request is started.
+ */
+ private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
+ new ConcurrentHashMap<ImageView, Request>();
+ /** Handler for messages sent to the UI thread. */
+ private final Handler mMainThreadHandler = new Handler(this);
+ /** For debug: How many times we had to reload cached photo for a stale entry */
+ private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
+ /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
+ private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
+ /** {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */
+ private volatile boolean mBitmapHolderCacheAllUnfresh = true;
+ /** Thread responsible for loading photos from the database. Created upon the first request. */
+ private LoaderThread mLoaderThread;
+ /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */
+ private boolean mLoadingRequested;
+ /** Flag indicating if the image loading is paused. */
+ private boolean mPaused;
+ /** The user agent string to use when loading URI based photos. */
+ private String mUserAgent;
+
+ public ContactPhotoManagerImpl(Context context) {
+ mContext = context;
+
+ final ActivityManager am =
+ ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE));
+
+ final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
+
+ final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
+ mBitmapCache =
+ new LruCache<Object, Bitmap>(bitmapCacheSize) {
+ @Override
+ protected int sizeOf(Object key, Bitmap value) {
+ return value.getByteCount();
+ }
+
+ @Override
+ protected void entryRemoved(
+ boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
+ if (DEBUG) {
+ dumpStats();
+ }
+ }
+ };
+ final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
+ mBitmapHolderCache =
+ new LruCache<Object, BitmapHolder>(holderCacheSize) {
+ @Override
+ protected int sizeOf(Object key, BitmapHolder value) {
+ return value.bytes != null ? value.bytes.length : 0;
+ }
+
+ @Override
+ protected void entryRemoved(
+ boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
+ if (DEBUG) {
+ dumpStats();
+ }
+ }
+ };
+ mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
+ LogUtil.i(
+ "ContactPhotoManagerImpl.ContactPhotoManagerImpl", "cache adj: " + cacheSizeAdjustment);
+ if (DEBUG) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.ContactPhotoManagerImpl",
+ "Cache size: " + btk(mBitmapHolderCache.maxSize()) + " + " + btk(mBitmapCache.maxSize()));
+ }
+
+ mThumbnailSize =
+ context.getResources().getDimensionPixelSize(R.dimen.contact_browser_list_item_photo_size);
+
+ // Get a user agent string to use for URI photo requests.
+ mUserAgent = Bindings.get(context).getUserAgent();
+ if (mUserAgent == null) {
+ mUserAgent = "";
+ }
+ }
+
+ /** Converts bytes to K bytes, rounding up. Used only for debug log. */
+ private static String btk(int bytes) {
+ return ((bytes + 1023) / 1024) + "K";
+ }
+
+ private static final int safeDiv(int dividend, int divisor) {
+ return (divisor == 0) ? 0 : (dividend / divisor);
+ }
+
+ private static boolean isChildView(View parent, View potentialChild) {
+ return potentialChild.getParent() != null
+ && (potentialChild.getParent() == parent
+ || (potentialChild.getParent() instanceof ViewGroup
+ && isChildView(parent, (ViewGroup) potentialChild.getParent())));
+ }
+
+ /**
+ * If necessary, decodes bytes stored in the holder to Bitmap. As long as the bitmap is held
+ * either by {@link #mBitmapCache} or by a soft reference in the holder, it will not be necessary
+ * to decode the bitmap.
+ */
+ private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
+ final int sampleSize =
+ BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
+ byte[] bytes = holder.bytes;
+ if (bytes == null || bytes.length == 0) {
+ return;
+ }
+
+ if (sampleSize == holder.decodedSampleSize) {
+ // Check the soft reference. If will be retained if the bitmap is also
+ // in the LRU cache, so we don't need to check the LRU cache explicitly.
+ if (holder.bitmapRef != null) {
+ holder.bitmap = holder.bitmapRef.get();
+ if (holder.bitmap != null) {
+ return;
+ }
+ }
+ }
+
+ try {
+ Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
+
+ // TODO: As a temporary workaround while framework support is being added to
+ // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
+ // into a square if it will be displayed as a thumbnail so that it can be cropped
+ // into a circle.
+ final int height = bitmap.getHeight();
+ final int width = bitmap.getWidth();
+
+ // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
+ // below twice the length of a thumbnail image due to the way we calculate the optimal
+ // sample size.
+ if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
+ final int dimension = Math.min(height, width);
+ bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
+ }
+ // make bitmap mutable and draw size onto it
+ if (DEBUG_SIZES) {
+ Bitmap original = bitmap;
+ bitmap = bitmap.copy(bitmap.getConfig(), true);
+ original.recycle();
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setTextSize(16);
+ paint.setColor(Color.BLUE);
+ paint.setStyle(Style.FILL);
+ canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
+ paint.setColor(Color.WHITE);
+ paint.setAntiAlias(true);
+ canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
+ }
+
+ holder.decodedSampleSize = sampleSize;
+ holder.bitmap = bitmap;
+ holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
+ if (DEBUG) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.inflateBitmap",
+ "inflateBitmap "
+ + btk(bytes.length)
+ + " -> "
+ + bitmap.getWidth()
+ + "x"
+ + bitmap.getHeight()
+ + ", "
+ + btk(bitmap.getByteCount()));
+ }
+ } catch (OutOfMemoryError e) {
+ // Do nothing - the photo will appear to be missing
+ }
+ }
+
+ /** Dump cache stats on logcat. */
+ private void dumpStats() {
+ if (!DEBUG) {
+ return;
+ }
+ {
+ int numHolders = 0;
+ int rawBytes = 0;
+ int bitmapBytes = 0;
+ int numBitmaps = 0;
+ for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
+ numHolders++;
+ if (h.bytes != null) {
+ rawBytes += h.bytes.length;
+ }
+ Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
+ if (b != null) {
+ numBitmaps++;
+ bitmapBytes += b.getByteCount();
+ }
+ }
+ LogUtil.d(
+ "ContactPhotoManagerImpl.dumpStats",
+ "L1: "
+ + btk(rawBytes)
+ + " + "
+ + btk(bitmapBytes)
+ + " = "
+ + btk(rawBytes + bitmapBytes)
+ + ", "
+ + numHolders
+ + " holders, "
+ + numBitmaps
+ + " bitmaps, avg: "
+ + btk(safeDiv(rawBytes, numHolders))
+ + ","
+ + btk(safeDiv(bitmapBytes, numBitmaps)));
+ LogUtil.d(
+ "ContactPhotoManagerImpl.dumpStats",
+ "L1 Stats: "
+ + mBitmapHolderCache.toString()
+ + ", overwrite: fresh="
+ + mFreshCacheOverwrite.get()
+ + " stale="
+ + mStaleCacheOverwrite.get());
+ }
+
+ {
+ int numBitmaps = 0;
+ int bitmapBytes = 0;
+ for (Bitmap b : mBitmapCache.snapshot().values()) {
+ numBitmaps++;
+ bitmapBytes += b.getByteCount();
+ }
+ LogUtil.d(
+ "ContactPhotoManagerImpl.dumpStats",
+ "L2: "
+ + btk(bitmapBytes)
+ + ", "
+ + numBitmaps
+ + " bitmaps"
+ + ", avg: "
+ + btk(safeDiv(bitmapBytes, numBitmaps)));
+ // We don't get from L2 cache, so L2 stats is meaningless.
+ }
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.onTrimMemory", "onTrimMemory: " + level);
+ }
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+ // Clear the caches. Note all pending requests will be removed too.
+ clear();
+ }
+ }
+
+ @Override
+ public void preloadPhotosInBackground() {
+ ensureLoaderThread();
+ mLoaderThread.requestPreloading();
+ }
+
+ @Override
+ public void loadThumbnail(
+ ImageView view,
+ long photoId,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider) {
+ if (photoId == 0) {
+ // No photo is needed
+ defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
+ mPendingRequests.remove(view);
+ } else {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.loadThumbnail", "loadPhoto request: " + photoId);
+ }
+ loadPhotoByIdOrUri(
+ view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, defaultProvider));
+ }
+ }
+
+ @Override
+ public void loadPhoto(
+ ImageView view,
+ Uri photoUri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider) {
+ if (photoUri == null) {
+ // No photo is needed
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, defaultImageRequest);
+ mPendingRequests.remove(view);
+ } else {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri);
+ }
+ if (isDefaultImageUri(photoUri)) {
+ createAndApplyDefaultImageForUri(
+ view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider);
+ } else {
+ loadPhotoByIdOrUri(
+ view,
+ Request.createFromUri(
+ photoUri, requestedExtent, darkTheme, isCircular, defaultProvider));
+ }
+ }
+ }
+
+ private void createAndApplyDefaultImageForUri(
+ ImageView view,
+ Uri uri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageProvider defaultProvider) {
+ DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
+ request.isCircular = isCircular;
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
+ }
+
+ private void loadPhotoByIdOrUri(ImageView view, Request request) {
+ boolean loaded = loadCachedPhoto(view, request, false);
+ if (loaded) {
+ mPendingRequests.remove(view);
+ } else {
+ mPendingRequests.put(view, request);
+ if (!mPaused) {
+ // Send a request to start loading photos
+ requestLoading();
+ }
+ }
+ }
+
+ @Override
+ public void removePhoto(ImageView view) {
+ view.setImageDrawable(null);
+ mPendingRequests.remove(view);
+ }
+
+ /**
+ * Cancels pending requests to load photos asynchronously for views inside {@param
+ * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
+ */
+ @Override
+ public void cancelPendingRequests(View fragmentRootView) {
+ if (fragmentRootView == null) {
+ mPendingRequests.clear();
+ return;
+ }
+ final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
+ while (iterator.hasNext()) {
+ final ImageView imageView = iterator.next().getKey();
+ // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
+ // we can safely remove its request.
+ if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
+ iterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public void refreshCache() {
+ if (mBitmapHolderCacheAllUnfresh) {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries.");
+ }
+ return;
+ }
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache");
+ }
+ mBitmapHolderCacheAllUnfresh = true;
+ for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
+ if (holder != BITMAP_UNAVAILABLE) {
+ holder.fresh = false;
+ }
+ }
+ }
+
+ /**
+ * Checks if the photo is present in cache. If so, sets the photo on the view.
+ *
+ * @return false if the photo needs to be (re)loaded from the provider.
+ */
+ @UiThread
+ private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
+ BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
+ if (holder == null) {
+ // The bitmap has not been loaded ==> show default avatar
+ request.applyDefaultImage(view, request.mIsCircular);
+ return false;
+ }
+
+ if (holder.bytes == null) {
+ request.applyDefaultImage(view, request.mIsCircular);
+ return holder.fresh;
+ }
+
+ Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
+ if (cachedBitmap == null) {
+ request.applyDefaultImage(view, request.mIsCircular);
+ return false;
+ }
+
+ final Drawable previousDrawable = view.getDrawable();
+ if (fadeIn && previousDrawable != null) {
+ final Drawable[] layers = new Drawable[2];
+ // Prevent cascade of TransitionDrawables.
+ if (previousDrawable instanceof TransitionDrawable) {
+ final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable;
+ layers[0] =
+ previousTransitionDrawable.getDrawable(
+ previousTransitionDrawable.getNumberOfLayers() - 1);
+ } else {
+ layers[0] = previousDrawable;
+ }
+ layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
+ TransitionDrawable drawable = new TransitionDrawable(layers);
+ view.setImageDrawable(drawable);
+ drawable.startTransition(FADE_TRANSITION_DURATION);
+ } else {
+ view.setImageDrawable(getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
+ }
+
+ // Put the bitmap in the LRU cache. But only do this for images that are small enough
+ // (we require that at least six of those can be cached at the same time)
+ if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
+ mBitmapCache.put(request.getKey(), cachedBitmap);
+ }
+
+ // Soften the reference
+ holder.bitmap = null;
+
+ return holder.fresh;
+ }
+
+ /**
+ * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
+ * specified request.
+ */
+ private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
+ if (request.mIsCircular) {
+ final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap);
+ drawable.setAntiAlias(true);
+ drawable.setCornerRadius(bitmap.getHeight() / 2);
+ return drawable;
+ } else {
+ return new BitmapDrawable(resources, bitmap);
+ }
+ }
+
+ public void clear() {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.clear", "clear");
+ }
+ mPendingRequests.clear();
+ mBitmapHolderCache.evictAll();
+ mBitmapCache.evictAll();
+ }
+
+ @Override
+ public void pause() {
+ mPaused = true;
+ }
+
+ @Override
+ public void resume() {
+ mPaused = false;
+ if (DEBUG) {
+ dumpStats();
+ }
+ if (!mPendingRequests.isEmpty()) {
+ requestLoading();
+ }
+ }
+
+ /**
+ * Sends a message to this thread itself to start loading images. If the current view contains
+ * multiple image views, all of those image views will get a chance to request their respective
+ * photos before any of those requests are executed. This allows us to load images in bulk.
+ */
+ private void requestLoading() {
+ if (!mLoadingRequested) {
+ mLoadingRequested = true;
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
+ }
+ }
+
+ /** Processes requests on the main thread. */
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_REQUEST_LOADING:
+ {
+ mLoadingRequested = false;
+ if (!mPaused) {
+ ensureLoaderThread();
+ mLoaderThread.requestLoading();
+ }
+ return true;
+ }
+
+ case MESSAGE_PHOTOS_LOADED:
+ {
+ if (!mPaused) {
+ processLoadedImages();
+ }
+ if (DEBUG) {
+ dumpStats();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void ensureLoaderThread() {
+ if (mLoaderThread == null) {
+ mLoaderThread = new LoaderThread(mContext.getContentResolver());
+ mLoaderThread.start();
+ }
+ }
+
+ /**
+ * Goes over pending loading requests and displays loaded photos. If some of the photos still
+ * haven't been loaded, sends another request for image loading.
+ */
+ private void processLoadedImages() {
+ final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
+ while (iterator.hasNext()) {
+ final Entry<ImageView, Request> entry = iterator.next();
+ // TODO: Temporarily disable contact photo fading in, until issues with
+ // RoundedBitmapDrawables overlapping the default image drawables are resolved.
+ final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
+ if (loaded) {
+ iterator.remove();
+ }
+ }
+
+ softenCache();
+
+ if (!mPendingRequests.isEmpty()) {
+ requestLoading();
+ }
+ }
+
+ /**
+ * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed.
+ * Some of the bitmaps will still be retained by {@link #mBitmapCache}.
+ */
+ private void softenCache() {
+ for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
+ holder.bitmap = null;
+ }
+ }
+
+ /** Stores the supplied bitmap in cache. */
+ private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
+ if (DEBUG) {
+ BitmapHolder prev = mBitmapHolderCache.get(key);
+ if (prev != null && prev.bytes != null) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.cacheBitmap",
+ "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
+ if (prev.fresh) {
+ mFreshCacheOverwrite.incrementAndGet();
+ } else {
+ mStaleCacheOverwrite.incrementAndGet();
+ }
+ }
+ LogUtil.d(
+ "ContactPhotoManagerImpl.cacheBitmap",
+ "caching data: key=" + key + ", " + (bytes == null ? "<null>" : btk(bytes.length)));
+ }
+ BitmapHolder holder =
+ new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
+
+ // Unless this image is being preloaded, decode it right away while
+ // we are still on the background thread.
+ if (!preloading) {
+ inflateBitmap(holder, requestedExtent);
+ }
+
+ if (bytes != null) {
+ mBitmapHolderCache.put(key, holder);
+ if (mBitmapHolderCache.get(key) != holder) {
+ LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache.");
+ mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
+ }
+ } else {
+ mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
+ }
+
+ mBitmapHolderCacheAllUnfresh = false;
+ }
+
+ /**
+ * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
+ * already loaded
+ */
+ private void obtainPhotoIdsAndUrisToLoad(
+ Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris) {
+ photoIds.clear();
+ photoIdsAsStrings.clear();
+ uris.clear();
+
+ boolean jpegsDecoded = false;
+
+ /*
+ * Since the call is made from the loader thread, the map could be
+ * changing during the iteration. That's not really a problem:
+ * ConcurrentHashMap will allow those changes to happen without throwing
+ * exceptions. Since we may miss some requests in the situation of
+ * concurrent change, we will need to check the map again once loading
+ * is complete.
+ */
+ Iterator<Request> iterator = mPendingRequests.values().iterator();
+ while (iterator.hasNext()) {
+ Request request = iterator.next();
+ final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
+ if (holder == BITMAP_UNAVAILABLE) {
+ continue;
+ }
+ if (holder != null
+ && holder.bytes != null
+ && holder.fresh
+ && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
+ // This was previously loaded but we don't currently have the inflated Bitmap
+ inflateBitmap(holder, request.getRequestedExtent());
+ jpegsDecoded = true;
+ } else {
+ if (holder == null || !holder.fresh) {
+ if (request.isUriRequest()) {
+ uris.add(request);
+ } else {
+ photoIds.add(request.getId());
+ photoIdsAsStrings.add(String.valueOf(request.mId));
+ }
+ }
+ }
+ }
+
+ if (jpegsDecoded) {
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+ }
+ }
+
+ /** Maintains the state of a particular photo. */
+ private static class BitmapHolder {
+
+ final byte[] bytes;
+ final int originalSmallerExtent;
+
+ volatile boolean fresh;
+ Bitmap bitmap;
+ Reference<Bitmap> bitmapRef;
+ int decodedSampleSize;
+
+ public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
+ this.bytes = bytes;
+ this.fresh = true;
+ this.originalSmallerExtent = originalSmallerExtent;
+ }
+ }
+
+ /**
+ * A holder for either a Uri or an id and a flag whether this was requested for the dark or light
+ * theme
+ */
+ private static final class Request {
+
+ private final long mId;
+ private final Uri mUri;
+ private final boolean mDarkTheme;
+ private final int mRequestedExtent;
+ private final DefaultImageProvider mDefaultProvider;
+ /** Whether or not the contact photo is to be displayed as a circle */
+ private final boolean mIsCircular;
+
+ private Request(
+ long id,
+ Uri uri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageProvider defaultProvider) {
+ mId = id;
+ mUri = uri;
+ mDarkTheme = darkTheme;
+ mIsCircular = isCircular;
+ mRequestedExtent = requestedExtent;
+ mDefaultProvider = defaultProvider;
+ }
+
+ public static Request createFromThumbnailId(
+ long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
+ return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider);
+ }
+
+ public static Request createFromUri(
+ Uri uri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageProvider defaultProvider) {
+ return new Request(
+ 0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider);
+ }
+
+ public boolean isUriRequest() {
+ return mUri != null;
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public int getRequestedExtent() {
+ return mRequestedExtent;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (mId ^ (mId >>> 32));
+ result = prime * result + mRequestedExtent;
+ result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Request that = (Request) obj;
+ if (mId != that.mId) {
+ return false;
+ }
+ if (mRequestedExtent != that.mRequestedExtent) {
+ return false;
+ }
+ if (!UriUtils.areEqual(mUri, that.mUri)) {
+ return false;
+ }
+ // Don't compare equality of mDarkTheme because it is only used in the default contact
+ // photo case. When the contact does have a photo, the contact photo is the same
+ // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
+ // twice.
+ return true;
+ }
+
+ public Object getKey() {
+ return mUri == null ? mId : mUri;
+ }
+
+ /**
+ * Applies the default image to the current view. If the request is URI-based, looks for the
+ * contact type encoded fragment to determine if this is a request for a business photo, in
+ * which case we will load the default business photo.
+ *
+ * @param view The current image view to apply the image to.
+ * @param isCircular Whether the image is circular or not.
+ */
+ public void applyDefaultImage(ImageView view, boolean isCircular) {
+ final DefaultImageRequest request;
+
+ if (isCircular) {
+ request =
+ ContactPhotoManager.isBusinessContactUri(mUri)
+ ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
+ : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
+ } else {
+ request =
+ ContactPhotoManager.isBusinessContactUri(mUri)
+ ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
+ : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
+ }
+ mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
+ }
+ }
+
+ /** The thread that performs loading of photos from the database. */
+ private class LoaderThread extends HandlerThread implements Callback {
+
+ private static final int BUFFER_SIZE = 1024 * 16;
+ private static final int MESSAGE_PRELOAD_PHOTOS = 0;
+ private static final int MESSAGE_LOAD_PHOTOS = 1;
+
+ /** A pause between preload batches that yields to the UI thread. */
+ private static final int PHOTO_PRELOAD_DELAY = 1000;
+
+ /** Number of photos to preload per batch. */
+ private static final int PRELOAD_BATCH = 25;
+
+ /**
+ * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size
+ * of a photo is 4kb, then this number should be 2Mb/4kb = 500.
+ */
+ private static final int MAX_PHOTOS_TO_PRELOAD = 100;
+
+ private static final int PRELOAD_STATUS_NOT_STARTED = 0;
+ private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
+ private static final int PRELOAD_STATUS_DONE = 2;
+ private final ContentResolver mResolver;
+ private final StringBuilder mStringBuilder = new StringBuilder();
+ private final Set<Long> mPhotoIds = new HashSet<>();
+ private final Set<String> mPhotoIdsAsStrings = new HashSet<>();
+ private final Set<Request> mPhotoUris = new HashSet<>();
+ private final List<Long> mPreloadPhotoIds = new ArrayList<>();
+ private Handler mLoaderThreadHandler;
+ private byte[] mBuffer;
+ private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
+
+ public LoaderThread(ContentResolver resolver) {
+ super(LOADER_THREAD_NAME);
+ mResolver = resolver;
+ }
+
+ public void ensureHandler() {
+ if (mLoaderThreadHandler == null) {
+ mLoaderThreadHandler = new Handler(getLooper(), this);
+ }
+ }
+
+ /**
+ * Kicks off preloading of the next batch of photos on the background thread. Preloading will
+ * happen after a delay: we want to yield to the UI thread as much as possible.
+ *
+ * <p>If preloading is already complete, does nothing.
+ */
+ public void requestPreloading() {
+ if (mPreloadStatus == PRELOAD_STATUS_DONE) {
+ return;
+ }
+
+ ensureHandler();
+ if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
+ return;
+ }
+
+ mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
+ }
+
+ /**
+ * Sends a message to this thread to load requested photos. Cancels a preloading request, if
+ * any: we don't want preloading to impede loading of the photos we need to display now.
+ */
+ public void requestLoading() {
+ ensureHandler();
+ mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
+ mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
+ }
+
+ /**
+ * Receives the above message, loads photos and then sends a message to the main thread to
+ * process them.
+ */
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_PRELOAD_PHOTOS:
+ preloadPhotosInBackground();
+ break;
+ case MESSAGE_LOAD_PHOTOS:
+ loadPhotosInBackground();
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * The first time it is called, figures out which photos need to be preloaded. Each subsequent
+ * call preloads the next batch of photos and requests another cycle of preloading after a
+ * delay. The whole process ends when we either run out of photos to preload or fill up cache.
+ */
+ @WorkerThread
+ private void preloadPhotosInBackground() {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+ return;
+ }
+
+ if (mPreloadStatus == PRELOAD_STATUS_DONE) {
+ return;
+ }
+
+ if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
+ queryPhotosForPreload();
+ if (mPreloadPhotoIds.isEmpty()) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ } else {
+ mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
+ }
+ requestPreloading();
+ return;
+ }
+
+ if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ return;
+ }
+
+ mPhotoIds.clear();
+ mPhotoIdsAsStrings.clear();
+
+ int count = 0;
+ int preloadSize = mPreloadPhotoIds.size();
+ while (preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
+ preloadSize--;
+ count++;
+ Long photoId = mPreloadPhotoIds.get(preloadSize);
+ mPhotoIds.add(photoId);
+ mPhotoIdsAsStrings.add(photoId.toString());
+ mPreloadPhotoIds.remove(preloadSize);
+ }
+
+ loadThumbnails(true);
+
+ if (preloadSize == 0) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ }
+
+ LogUtil.v(
+ "ContactPhotoManagerImpl.preloadPhotosInBackground",
+ "preloaded " + count + " photos. cached bytes: " + mBitmapHolderCache.size());
+
+ requestPreloading();
+ }
+
+ @WorkerThread
+ private void queryPhotosForPreload() {
+ Cursor cursor = null;
+ try {
+ Uri uri =
+ Contacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
+ .appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD))
+ .build();
+ cursor =
+ mResolver.query(
+ uri,
+ new String[] {Contacts.PHOTO_ID},
+ Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
+ null,
+ Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ // Insert them in reverse order, because we will be taking
+ // them from the end of the list for loading.
+ mPreloadPhotoIds.add(0, cursor.getLong(0));
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ @WorkerThread
+ private void loadPhotosInBackground() {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+ return;
+ }
+ obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
+ loadThumbnails(false);
+ loadUriBasedPhotos();
+ requestPreloading();
+ }
+
+ /** Loads thumbnail photos with ids */
+ @WorkerThread
+ private void loadThumbnails(boolean preloading) {
+ if (mPhotoIds.isEmpty()) {
+ return;
+ }
+
+ // Remove loaded photos from the preload queue: we don't want
+ // the preloading process to load them again.
+ if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
+ for (Long id : mPhotoIds) {
+ mPreloadPhotoIds.remove(id);
+ }
+ if (mPreloadPhotoIds.isEmpty()) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ }
+ }
+
+ mStringBuilder.setLength(0);
+ mStringBuilder.append(Photo._ID + " IN(");
+ for (int i = 0; i < mPhotoIds.size(); i++) {
+ if (i != 0) {
+ mStringBuilder.append(',');
+ }
+ mStringBuilder.append('?');
+ }
+ mStringBuilder.append(')');
+
+ Cursor cursor = null;
+ try {
+ if (DEBUG) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.loadThumbnails",
+ "loading " + TextUtils.join(",", mPhotoIdsAsStrings));
+ }
+ cursor =
+ mResolver.query(
+ Data.CONTENT_URI,
+ COLUMNS,
+ mStringBuilder.toString(),
+ mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
+ null);
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ Long id = cursor.getLong(0);
+ byte[] bytes = cursor.getBlob(1);
+ cacheBitmap(id, bytes, preloading, -1);
+ mPhotoIds.remove(id);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // Remaining photos were not found in the contacts database (but might be in profile).
+ for (Long id : mPhotoIds) {
+ if (ContactsContract.isProfileId(id)) {
+ Cursor profileCursor = null;
+ try {
+ profileCursor =
+ mResolver.query(
+ ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null);
+ if (profileCursor != null && profileCursor.moveToFirst()) {
+ cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1);
+ } else {
+ // Couldn't load a photo this way either.
+ cacheBitmap(id, null, preloading, -1);
+ }
+ } finally {
+ if (profileCursor != null) {
+ profileCursor.close();
+ }
+ }
+ } else {
+ // Not a profile photo and not found - mark the cache accordingly
+ cacheBitmap(id, null, preloading, -1);
+ }
+ }
+
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+ }
+
+ /**
+ * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches),
+ * display photos etc
+ */
+ @WorkerThread
+ private void loadUriBasedPhotos() {
+ for (Request uriRequest : mPhotoUris) {
+ // Keep the original URI and use this to key into the cache. Failure to do so will
+ // result in an image being continually reloaded into cache if the original URI
+ // has a contact type encodedFragment (eg nearby places business photo URLs).
+ Uri originalUri = uriRequest.getUri();
+
+ // Strip off the "contact type" we added to the URI to ensure it was identifiable as
+ // a business photo -- there is no need to pass this on to the server.
+ Uri uri = ContactPhotoManager.removeContactType(originalUri);
+
+ if (mBuffer == null) {
+ mBuffer = new byte[BUFFER_SIZE];
+ }
+ try {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri);
+ }
+ final String scheme = uri.getScheme();
+ InputStream is = null;
+ if (scheme.equals("http") || scheme.equals("https")) {
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
+ final HttpURLConnection connection =
+ (HttpURLConnection) new URL(uri.toString()).openConnection();
+
+ // Include the user agent if it is specified.
+ if (!TextUtils.isEmpty(mUserAgent)) {
+ connection.setRequestProperty("User-Agent", mUserAgent);
+ }
+ try {
+ is = connection.getInputStream();
+ } catch (IOException e) {
+ connection.disconnect();
+ is = null;
+ }
+ TrafficStats.clearThreadStatsTag();
+ } else {
+ is = mResolver.openInputStream(uri);
+ }
+ if (is != null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ int size;
+ while ((size = is.read(mBuffer)) != -1) {
+ baos.write(mBuffer, 0, size);
+ }
+ } finally {
+ is.close();
+ }
+ cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent());
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+ } else {
+ LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri);
+ cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
+ }
+ } catch (final Exception | OutOfMemoryError ex) {
+ LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex);
+ cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/ContactPresenceIconUtil.java b/java/com/android/contacts/common/ContactPresenceIconUtil.java
new file mode 100644
index 000000000..eeaf652a8
--- /dev/null
+++ b/java/com/android/contacts/common/ContactPresenceIconUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.StatusUpdates;
+
+/** Define the contact present show policy in Contacts */
+public class ContactPresenceIconUtil {
+
+ /**
+ * Get the presence icon resource according the status.
+ *
+ * @return null means don't show the status icon.
+ */
+ public static Drawable getPresenceIcon(Context context, int status) {
+ // We don't show the offline status in Contacts
+ switch (status) {
+ case StatusUpdates.AVAILABLE:
+ case StatusUpdates.IDLE:
+ case StatusUpdates.AWAY:
+ case StatusUpdates.DO_NOT_DISTURB:
+ case StatusUpdates.INVISIBLE:
+ return context.getResources().getDrawable(StatusUpdates.getPresenceIconResourceId(status));
+ case StatusUpdates.OFFLINE:
+ // The undefined status is treated as OFFLINE in getPresenceIconResourceId();
+ default:
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/ContactStatusUtil.java b/java/com/android/contacts/common/ContactStatusUtil.java
new file mode 100644
index 000000000..97d84c876
--- /dev/null
+++ b/java/com/android/contacts/common/ContactStatusUtil.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.StatusUpdates;
+
+/** Provides static function to get default contact status message. */
+public class ContactStatusUtil {
+
+ private static final String TAG = "ContactStatusUtil";
+
+ public static String getStatusString(Context context, int presence) {
+ Resources resources = context.getResources();
+ switch (presence) {
+ case StatusUpdates.AVAILABLE:
+ return resources.getString(R.string.status_available);
+ case StatusUpdates.IDLE:
+ case StatusUpdates.AWAY:
+ return resources.getString(R.string.status_away);
+ case StatusUpdates.DO_NOT_DISTURB:
+ return resources.getString(R.string.status_busy);
+ case StatusUpdates.OFFLINE:
+ case StatusUpdates.INVISIBLE:
+ default:
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/ContactTileLoaderFactory.java b/java/com/android/contacts/common/ContactTileLoaderFactory.java
new file mode 100644
index 000000000..d71472ef8
--- /dev/null
+++ b/java/com/android/contacts/common/ContactTileLoaderFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 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
+ */
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * Used to create {@link CursorLoader} which finds contacts information from the strequents table.
+ *
+ * <p>Only returns contacts with phone numbers.
+ */
+public final class ContactTileLoaderFactory {
+
+ /**
+ * The _ID field returned for strequent items actually contains data._id instead of contacts._id
+ * because the query is performed on the data table. In order to obtain the contact id for
+ * strequent items, use Phone.contact_id instead.
+ */
+ @VisibleForTesting
+ public static final String[] COLUMNS_PHONE_ONLY =
+ new String[] {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME_PRIMARY,
+ Contacts.STARRED,
+ Contacts.PHOTO_URI,
+ Contacts.LOOKUP_KEY,
+ Phone.NUMBER,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.IS_SUPER_PRIMARY,
+ Contacts.PINNED,
+ Phone.CONTACT_ID,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ };
+
+ public static CursorLoader createStrequentPhoneOnlyLoader(Context context) {
+ Uri uri =
+ Contacts.CONTENT_STREQUENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
+ .build();
+
+ return new CursorLoader(context, uri, COLUMNS_PHONE_ONLY, null, null, null);
+ }
+}
diff --git a/java/com/android/contacts/common/ContactsUtils.java b/java/com/android/contacts/common/ContactsUtils.java
new file mode 100644
index 000000000..60af44b9a
--- /dev/null
+++ b/java/com/android/contacts/common/ContactsUtils.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import android.util.Pair;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.ImDataItem;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+public class ContactsUtils {
+
+ // Telecomm related schemes are in CallUtil
+ public static final String SCHEME_IMTO = "imto";
+ public static final String SCHEME_MAILTO = "mailto";
+ public static final String SCHEME_SMSTO = "smsto";
+ public static final long USER_TYPE_CURRENT = 0;
+ public static final long USER_TYPE_WORK = 1;
+ private static final String TAG = "ContactsUtils";
+ private static final int DEFAULT_THUMBNAIL_SIZE = 96;
+ private static int sThumbnailSize = -1;
+
+ /**
+ * This looks up the provider name defined in ProviderNames from the predefined IM protocol id.
+ * This is used for interacting with the IM application.
+ *
+ * @param protocol the protocol ID
+ * @return the provider name the IM app uses for the given protocol, or null if no provider is
+ * defined for the given protocol
+ * @hide
+ */
+ public static String lookupProviderNameFromId(int protocol) {
+ switch (protocol) {
+ case Im.PROTOCOL_GOOGLE_TALK:
+ return ProviderNames.GTALK;
+ case Im.PROTOCOL_AIM:
+ return ProviderNames.AIM;
+ case Im.PROTOCOL_MSN:
+ return ProviderNames.MSN;
+ case Im.PROTOCOL_YAHOO:
+ return ProviderNames.YAHOO;
+ case Im.PROTOCOL_ICQ:
+ return ProviderNames.ICQ;
+ case Im.PROTOCOL_JABBER:
+ return ProviderNames.JABBER;
+ case Im.PROTOCOL_SKYPE:
+ return ProviderNames.SKYPE;
+ case Im.PROTOCOL_QQ:
+ return ProviderNames.QQ;
+ }
+ return null;
+ }
+
+ /**
+ * Test if the given {@link CharSequence} contains any graphic characters, first checking {@link
+ * TextUtils#isEmpty(CharSequence)} to handle null.
+ */
+ public static boolean isGraphic(CharSequence str) {
+ return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
+ }
+
+ /** Returns true if two objects are considered equal. Two null references are equal here. */
+ public static boolean areObjectsEqual(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ /** Returns true if two {@link Intent}s are both null, or have the same action. */
+ public static final boolean areIntentActionEqual(Intent a, Intent b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ return TextUtils.equals(a.getAction(), b.getAction());
+ }
+
+ public static boolean areGroupWritableAccountsAvailable(Context context) {
+ final List<AccountWithDataSet> accounts =
+ AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+ return !accounts.isEmpty();
+ }
+
+ /**
+ * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
+ * can safely be called from the UI thread, as the provider can serve this without performing a
+ * database access
+ */
+ public static int getThumbnailSize(Context context) {
+ if (sThumbnailSize == -1) {
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(
+ DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+ new String[] {DisplayPhoto.THUMBNAIL_MAX_DIM},
+ null,
+ null,
+ null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ sThumbnailSize = c.getInt(0);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ return sThumbnailSize != -1 ? sThumbnailSize : DEFAULT_THUMBNAIL_SIZE;
+ }
+
+ private static Intent getCustomImIntent(ImDataItem im, int protocol) {
+ String host = im.getCustomProtocol();
+ final String data = im.getData();
+ if (TextUtils.isEmpty(data)) {
+ return null;
+ }
+ if (protocol != Im.PROTOCOL_CUSTOM) {
+ // Try bringing in a well-known host for specific protocols
+ host = ContactsUtils.lookupProviderNameFromId(protocol);
+ }
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+ final String authority = host.toLowerCase();
+ final Uri imUri =
+ new Uri.Builder().scheme(SCHEME_IMTO).authority(authority).appendPath(data).build();
+ final Intent intent = new Intent(Intent.ACTION_SENDTO, imUri);
+ return intent;
+ }
+
+ /**
+ * Returns the proper Intent for an ImDatItem. If available, a secondary intent is stored in the
+ * second Pair slot
+ */
+ public static Pair<Intent, Intent> buildImIntent(Context context, ImDataItem im) {
+ Intent intent = null;
+ Intent secondaryIntent = null;
+ final boolean isEmail = im.isCreatedFromEmail();
+
+ if (!isEmail && !im.isProtocolValid()) {
+ return new Pair<>(null, null);
+ }
+
+ final String data = im.getData();
+ if (TextUtils.isEmpty(data)) {
+ return new Pair<>(null, null);
+ }
+
+ final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
+
+ if (protocol == Im.PROTOCOL_GOOGLE_TALK) {
+ final int chatCapability = im.getChatCapability();
+ if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) {
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+ secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
+ } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) {
+ // Allow Talking and Texting
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+ secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
+ } else {
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+ }
+ } else {
+ // Build an IM Intent
+ intent = getCustomImIntent(im, protocol);
+ }
+ return new Pair<>(intent, secondaryIntent);
+ }
+
+ /**
+ * Determine UserType from directory id and contact id.
+ *
+ * <p>3 types of query
+ *
+ * <p>1. 2 profile query: content://com.android.contacts/phone_lookup_enterprise/1234567890
+ * personal and work contact are mixed into one cursor. no directory id. contact_id indicates if
+ * it's work contact
+ *
+ * <p>2. work local query:
+ * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000000 either
+ * directory_id or contact_id is enough to identify work contact
+ *
+ * <p>3. work remote query:
+ * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000003
+ * contact_id is random. only directory_id is available
+ *
+ * <p>Summary: If directory_id is not null, always use directory_id to identify work contact.
+ * (which is the case here) Otherwise, use contact_id.
+ *
+ * @param directoryId directory id of ContactsProvider query
+ * @param contactId contact id
+ * @return UserType indicates the user type of the contact. A directory id or contact id larger
+ * than a thredshold indicates that the contact is stored in Work Profile, but not in current
+ * user. It's a contract by ContactsProvider and check by Contacts.isEnterpriseDirectoryId and
+ * Contacts.isEnterpriseContactId. Currently, only 2 kinds of users can be detected from the
+ * directoryId and contactId as ContactsProvider can only access current and work user's
+ * contacts
+ */
+ public static @UserType long determineUserType(Long directoryId, Long contactId) {
+ // First check directory id
+ if (directoryId != null) {
+ return DirectoryCompat.isEnterpriseDirectoryId(directoryId)
+ ? USER_TYPE_WORK
+ : USER_TYPE_CURRENT;
+ }
+ // Only check contact id if directory id is null
+ if (contactId != null && contactId != 0L && ContactsCompat.isEnterpriseContactId(contactId)) {
+ return USER_TYPE_WORK;
+ } else {
+ return USER_TYPE_CURRENT;
+ }
+ }
+
+ // TODO find a proper place for the canonical version of these
+ public interface ProviderNames {
+
+ String YAHOO = "Yahoo";
+ String GTALK = "GTalk";
+ String MSN = "MSN";
+ String ICQ = "ICQ";
+ String AIM = "AIM";
+ String XMPP = "XMPP";
+ String JABBER = "JABBER";
+ String SKYPE = "SKYPE";
+ String QQ = "QQ";
+ }
+
+ /**
+ * UserType indicates the user type of the contact. If the contact is from Work User (Work Profile
+ * in Android Multi-User System), it's {@link #USER_TYPE_WORK}, otherwise, {@link
+ * #USER_TYPE_CURRENT}. Please note that current user can be in work profile, where the dialer is
+ * running inside Work Profile.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({USER_TYPE_CURRENT, USER_TYPE_WORK})
+ public @interface UserType {}
+}
diff --git a/java/com/android/contacts/common/GeoUtil.java b/java/com/android/contacts/common/GeoUtil.java
new file mode 100644
index 000000000..50b0cd9e3
--- /dev/null
+++ b/java/com/android/contacts/common/GeoUtil.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common;
+
+import android.app.Application;
+import android.content.Context;
+import com.android.contacts.common.location.CountryDetector;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+import java.util.Locale;
+
+/** Static methods related to Geo. */
+public class GeoUtil {
+
+ /**
+ * Returns the country code of the country the user is currently in. Before calling this method,
+ * make sure that {@link CountryDetector#initialize(Context)} has already been called in {@link
+ * Application#onCreate()}.
+ *
+ * @return The ISO 3166-1 two letters country code of the country the user is in.
+ */
+ public static String getCurrentCountryIso(Context context) {
+ // The {@link CountryDetector} should never return null so this is safe to return as-is.
+ return CountryDetector.getInstance(context).getCurrentCountryIso();
+ }
+
+ public static String getGeocodedLocationFor(Context context, String phoneNumber) {
+ final PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+ final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+ try {
+ final Phonenumber.PhoneNumber structuredPhoneNumber =
+ phoneNumberUtil.parse(phoneNumber, getCurrentCountryIso(context));
+ final Locale locale = context.getResources().getConfiguration().locale;
+ return geocoder.getDescriptionForNumber(structuredPhoneNumber, locale);
+ } catch (NumberParseException e) {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/GroupMetaData.java b/java/com/android/contacts/common/GroupMetaData.java
new file mode 100644
index 000000000..b34f1d629
--- /dev/null
+++ b/java/com/android/contacts/common/GroupMetaData.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 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
+ */
+package com.android.contacts.common;
+
+/**
+ * Meta-data for a contact group. We load all groups associated with the contact's constituent
+ * accounts.
+ */
+public final class GroupMetaData {
+
+ private String mAccountName;
+ private String mAccountType;
+ private String mDataSet;
+ private long mGroupId;
+ private String mTitle;
+ private boolean mDefaultGroup;
+ private boolean mFavorites;
+
+ public GroupMetaData(
+ String accountName,
+ String accountType,
+ String dataSet,
+ long groupId,
+ String title,
+ boolean defaultGroup,
+ boolean favorites) {
+ this.mAccountName = accountName;
+ this.mAccountType = accountType;
+ this.mDataSet = dataSet;
+ this.mGroupId = groupId;
+ this.mTitle = title;
+ this.mDefaultGroup = defaultGroup;
+ this.mFavorites = favorites;
+ }
+
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public String getAccountType() {
+ return mAccountType;
+ }
+
+ public String getDataSet() {
+ return mDataSet;
+ }
+
+ public long getGroupId() {
+ return mGroupId;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public boolean isDefaultGroup() {
+ return mDefaultGroup;
+ }
+
+ public boolean isFavorites() {
+ return mFavorites;
+ }
+}
diff --git a/java/com/android/contacts/common/MoreContactUtils.java b/java/com/android/contacts/common/MoreContactUtils.java
new file mode 100644
index 000000000..028f89971
--- /dev/null
+++ b/java/com/android/contacts/common/MoreContactUtils.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+
+/** Shared static contact utility methods. */
+public class MoreContactUtils {
+
+ private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
+
+ /**
+ * Returns true if two data with mimetypes which represent values in contact entries are
+ * considered equal for collapsing in the GUI. For caller-id, use {@link
+ * android.telephony.PhoneNumberUtils#compare(android.content.Context, String, String)} instead
+ */
+ public static boolean shouldCollapse(
+ CharSequence mimetype1, CharSequence data1, CharSequence mimetype2, CharSequence data2) {
+ // different mimetypes? don't collapse
+ if (!TextUtils.equals(mimetype1, mimetype2)) {
+ return false;
+ }
+
+ // exact same string? good, bail out early
+ if (TextUtils.equals(data1, data2)) {
+ return true;
+ }
+
+ // so if either is null, these two must be different
+ if (data1 == null || data2 == null) {
+ return false;
+ }
+
+ // if this is not about phone numbers, we know this is not a match (of course, some
+ // mimetypes could have more sophisticated matching is the future, e.g. addresses)
+ if (!TextUtils.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, mimetype1)) {
+ return false;
+ }
+
+ return shouldCollapsePhoneNumbers(data1.toString(), data2.toString());
+ }
+
+ // TODO: Move this to PhoneDataItem.shouldCollapse override
+ private static boolean shouldCollapsePhoneNumbers(String number1, String number2) {
+ // Work around to address b/20724444. We want to distinguish between #555, *555 and 555.
+ // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable
+ // number. PhoneNumberUtil already distinguishes between 555 and 55#5.
+ if (number1.contains("#") != number2.contains("#")
+ || number1.contains("*") != number2.contains("*")) {
+ return false;
+ }
+
+ // Now do the full phone number thing. split into parts, separated by waiting symbol
+ // and compare them individually
+ final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING);
+ final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING);
+ if (dataParts1.length != dataParts2.length) {
+ return false;
+ }
+ final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ for (int i = 0; i < dataParts1.length; i++) {
+ // Match phone numbers represented by keypad letters, in which case prefer the
+ // phone number with letters.
+ final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]);
+ final String dataPart2 = dataParts2[i];
+
+ // substrings equal? shortcut, don't parse
+ if (TextUtils.equals(dataPart1, dataPart2)) {
+ continue;
+ }
+
+ // do a full parse of the numbers
+ final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2);
+ switch (result) {
+ case NOT_A_NUMBER:
+ // don't understand the numbers? let's play it safe
+ return false;
+ case NO_MATCH:
+ return false;
+ case EXACT_MATCH:
+ break;
+ case NSN_MATCH:
+ try {
+ // For NANP phone numbers, match when one has +1 and the other does not.
+ // In this case, prefer the +1 version.
+ if (util.parse(dataPart1, null).getCountryCode() == 1) {
+ // At this point, the numbers can be either case 1 or 2 below....
+ //
+ // case 1)
+ // +14155551212 <--- country code 1
+ // 14155551212 <--- 1 is trunk prefix, not country code
+ //
+ // and
+ //
+ // case 2)
+ // +14155551212
+ // 4155551212
+ //
+ // From b/7519057, case 2 needs to be equal. But also that bug, case 3
+ // below should not be equal.
+ //
+ // case 3)
+ // 14155551212
+ // 4155551212
+ //
+ // So in order to make sure transitive equality is valid, case 1 cannot
+ // be equal. Otherwise, transitive equality breaks and the following
+ // would all be collapsed:
+ // 4155551212 |
+ // 14155551212 |----> +14155551212
+ // +14155551212 |
+ //
+ // With transitive equality, the collapsed values should be:
+ // 4155551212 | 14155551212
+ // 14155551212 |----> +14155551212
+ // +14155551212 |
+
+ // Distinguish between case 1 and 2 by checking for trunk prefix '1'
+ // at the start of number 2.
+ if (dataPart2.trim().charAt(0) == '1') {
+ // case 1
+ return false;
+ }
+ break;
+ }
+ } catch (NumberParseException e) {
+ // This is the case where the first number does not have a country code.
+ // examples:
+ // (123) 456-7890 & 123-456-7890 (collapse)
+ // 0049 (8092) 1234 & +49/80921234 (unit test says do not collapse)
+
+ // Check the second number. If it also does not have a country code, then
+ // we should collapse. If it has a country code, then it's a different
+ // number and we should not collapse (this conclusion is based on an
+ // existing unit test).
+ try {
+ util.parse(dataPart2, null);
+ } catch (NumberParseException e2) {
+ // Number 2 also does not have a country. Collapse.
+ break;
+ }
+ }
+ return false;
+ case SHORT_NSN_MATCH:
+ return false;
+ default:
+ throw new IllegalStateException("Unknown result value from phone number " + "library");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates that
+ * are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to how the
+ * target {@link android.graphics.Rect} is calculated in {@link
+ * android.provider.ContactsContract.QuickContact#showQuickContact}.
+ */
+ public static Rect getTargetRectFromView(View view) {
+ final int[] pos = new int[2];
+ view.getLocationOnScreen(pos);
+
+ final Rect rect = new Rect();
+ rect.left = pos[0];
+ rect.top = pos[1];
+ rect.right = pos[0] + view.getWidth();
+ rect.bottom = pos[1] + view.getHeight();
+ return rect;
+ }
+
+ /**
+ * Returns a header view based on the R.layout.list_separator, where the containing {@link
+ * android.widget.TextView} is set using the given textResourceId.
+ */
+ public static TextView createHeaderView(Context context, int textResourceId) {
+ final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null);
+ textView.setText(context.getString(textResourceId));
+ return textView;
+ }
+
+ /**
+ * Set the top padding on the header view dynamically, based on whether the header is in the first
+ * row or not.
+ */
+ public static void setHeaderViewBottomPadding(
+ Context context, TextView textView, boolean isFirstRow) {
+ final int topPadding;
+ if (isFirstRow) {
+ topPadding =
+ (int)
+ context
+ .getResources()
+ .getDimension(R.dimen.frequently_contacted_title_top_margin_when_first_row);
+ } else {
+ topPadding =
+ (int) context.getResources().getDimension(R.dimen.frequently_contacted_title_top_margin);
+ }
+ textView.setPaddingRelative(
+ textView.getPaddingStart(),
+ topPadding,
+ textView.getPaddingEnd(),
+ textView.getPaddingBottom());
+ }
+
+ /**
+ * Returns the intent to launch for the given invitable account type and contact lookup URI. This
+ * will return null if the account type is not invitable (i.e. there is no {@link
+ * AccountType#getInviteContactActivityClassName()} or {@link
+ * AccountType#syncAdapterPackageName}).
+ */
+ public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
+ String syncAdapterPackageName = accountType.syncAdapterPackageName;
+ String className = accountType.getInviteContactActivityClassName();
+ if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
+ return null;
+ }
+ Intent intent = new Intent();
+ intent.setClassName(syncAdapterPackageName, className);
+
+ intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
+
+ // Data is the lookup URI.
+ intent.setData(lookupUri);
+ return intent;
+ }
+}
diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindings.java b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java
new file mode 100644
index 000000000..44be53b3f
--- /dev/null
+++ b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.bindings;
+
+import android.support.annotation.Nullable;
+
+/** Allows the container application to customize the contacts common library. */
+public interface ContactsCommonBindings {
+
+ /** Builds a user agent string for the current application. */
+ @Nullable
+ String getUserAgent();
+}
diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java
new file mode 100644
index 000000000..8958ad997
--- /dev/null
+++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.bindings;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the contacts common
+ * module to get references to the ContactsCommonBindings.
+ */
+public interface ContactsCommonBindingsFactory {
+
+ ContactsCommonBindings newContactsCommonBindings();
+}
diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java
new file mode 100644
index 000000000..f2e21b18e
--- /dev/null
+++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.bindings;
+
+import android.support.annotation.Nullable;
+
+/** Default implementation for contacts common bindings. */
+public class ContactsCommonBindingsStub implements ContactsCommonBindings {
+
+ @Override
+ @Nullable
+ public String getUserAgent() {
+ return null;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/CallCompat.java b/java/com/android/contacts/common/compat/CallCompat.java
new file mode 100644
index 000000000..641f7b1bd
--- /dev/null
+++ b/java/com/android/contacts/common/compat/CallCompat.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+
+/** Compatibility utilities for android.telecom.Call */
+public class CallCompat {
+
+ public static boolean canPullExternalCall(@NonNull android.telecom.Call call) {
+ return VERSION.SDK_INT >= VERSION_CODES.N_MR1
+ && ((call.getDetails().getCallCapabilities() & Details.CAPABILITY_CAN_PULL_CALL)
+ == Details.CAPABILITY_CAN_PULL_CALL);
+ }
+
+ /** android.telecom.Call.Details */
+ public static class Details {
+
+ public static final int PROPERTY_IS_EXTERNAL_CALL = Call.Details.PROPERTY_IS_EXTERNAL_CALL;
+ public static final int PROPERTY_ENTERPRISE_CALL = Call.Details.PROPERTY_ENTERPRISE_CALL;
+ public static final int CAPABILITY_CAN_PULL_CALL = Call.Details.CAPABILITY_CAN_PULL_CALL;
+ public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO =
+ Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO;
+
+ public static final String EXTRA_ANSWERING_DROPS_FOREGROUND_CALL =
+ "android.telecom.extra.ANSWERING_DROPS_FG_CALL";
+ }
+}
diff --git a/java/com/android/contacts/common/compat/CallableCompat.java b/java/com/android/contacts/common/compat/CallableCompat.java
new file mode 100644
index 000000000..5e86f518e
--- /dev/null
+++ b/java/com/android/contacts/common/compat/CallableCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+
+public class CallableCompat {
+
+ // TODO: Use N APIs
+ private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+ Uri.withAppendedPath(Callable.CONTENT_URI, "filter_enterprise");
+
+ public static Uri getContentFilterUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return ENTERPRISE_CONTENT_FILTER_URI;
+ }
+ return Callable.CONTENT_FILTER_URI;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/ContactsCompat.java b/java/com/android/contacts/common/compat/ContactsCompat.java
new file mode 100644
index 000000000..39d0b55d3
--- /dev/null
+++ b/java/com/android/contacts/common/compat/ContactsCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import com.android.dialer.compat.CompatUtils;
+
+/** Compatibility class for {@link ContactsContract.Contacts} */
+public class ContactsCompat {
+
+ // TODO: Use N APIs
+ private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+ Uri.withAppendedPath(Contacts.CONTENT_URI, "filter_enterprise");
+ // Copied from ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE, which is hidden.
+ private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000;
+
+ /** Not instantiable. */
+ private ContactsCompat() {}
+
+ public static Uri getContentUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return ENTERPRISE_CONTENT_FILTER_URI;
+ }
+ return Contacts.CONTENT_FILTER_URI;
+ }
+
+ /**
+ * Return {@code true} if a contact ID is from the contacts provider on the enterprise profile.
+ */
+ public static boolean isEnterpriseContactId(long contactId) {
+ if (CompatUtils.isLollipopCompatible()) {
+ return Contacts.isEnterpriseContactId(contactId);
+ } else {
+ // copied from ContactsContract.Contacts.isEnterpriseContactId
+ return (contactId >= ENTERPRISE_CONTACT_ID_BASE)
+ && (contactId < ContactsContract.Profile.MIN_ID);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/compat/DirectoryCompat.java b/java/com/android/contacts/common/compat/DirectoryCompat.java
new file mode 100644
index 000000000..85f4a4202
--- /dev/null
+++ b/java/com/android/contacts/common/compat/DirectoryCompat.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Directory;
+
+public class DirectoryCompat {
+
+ public static Uri getContentUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return Directory.ENTERPRISE_CONTENT_URI;
+ }
+ return Directory.CONTENT_URI;
+ }
+
+ public static boolean isInvisibleDirectory(long directoryId) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return (directoryId == Directory.LOCAL_INVISIBLE
+ || directoryId == Directory.ENTERPRISE_LOCAL_INVISIBLE);
+ }
+ return directoryId == Directory.LOCAL_INVISIBLE;
+ }
+
+ public static boolean isRemoteDirectoryId(long directoryId) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return Directory.isRemoteDirectoryId(directoryId);
+ }
+ return !(directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE);
+ }
+
+ public static boolean isEnterpriseDirectoryId(long directoryId) {
+ return VERSION.SDK_INT >= VERSION_CODES.N && Directory.isEnterpriseDirectoryId(directoryId);
+ }
+}
diff --git a/java/com/android/contacts/common/compat/PhoneAccountCompat.java b/java/com/android/contacts/common/compat/PhoneAccountCompat.java
new file mode 100644
index 000000000..6a24ec033
--- /dev/null
+++ b/java/com/android/contacts/common/compat/PhoneAccountCompat.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 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
+ */
+package com.android.contacts.common.compat;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.util.Log;
+import com.android.dialer.compat.CompatUtils;
+
+/** Compatiblity class for {@link android.telecom.PhoneAccount} */
+public class PhoneAccountCompat {
+
+ private static final String TAG = PhoneAccountCompat.class.getSimpleName();
+
+ /**
+ * Gets the {@link Icon} associated with the given {@link PhoneAccount}
+ *
+ * @param phoneAccount the PhoneAccount from which to retrieve the Icon
+ * @return the Icon, or null
+ */
+ @Nullable
+ public static Icon getIcon(@Nullable PhoneAccount phoneAccount) {
+ if (phoneAccount == null) {
+ return null;
+ }
+
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return phoneAccount.getIcon();
+ }
+
+ return null;
+ }
+
+ /**
+ * Builds and returns an icon {@code Drawable} to represent this {@code PhoneAccount} in a user
+ * interface.
+ *
+ * @param phoneAccount the PhoneAccount from which to build the icon.
+ * @param context A {@code Context} to use for loading Drawables.
+ * @return An icon for this PhoneAccount, or null
+ */
+ @Nullable
+ public static Drawable createIconDrawable(
+ @Nullable PhoneAccount phoneAccount, @Nullable Context context) {
+ if (phoneAccount == null || context == null) {
+ return null;
+ }
+
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return createIconDrawableMarshmallow(phoneAccount, context);
+ }
+
+ if (CompatUtils.isLollipopMr1Compatible()) {
+ return createIconDrawableLollipopMr1(phoneAccount, context);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Drawable createIconDrawableMarshmallow(
+ PhoneAccount phoneAccount, Context context) {
+ Icon accountIcon = getIcon(phoneAccount);
+ if (accountIcon == null) {
+ return null;
+ }
+ return accountIcon.loadDrawable(context);
+ }
+
+ @Nullable
+ private static Drawable createIconDrawableLollipopMr1(
+ PhoneAccount phoneAccount, Context context) {
+ try {
+ return (Drawable)
+ PhoneAccount.class
+ .getMethod("createIconDrawable", Context.class)
+ .invoke(phoneAccount, context);
+ } catch (ReflectiveOperationException e) {
+ return null;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when attempting to call "
+ + "android.telecom.PhoneAccount#createIconDrawable",
+ t);
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/compat/PhoneCompat.java b/java/com/android/contacts/common/compat/PhoneCompat.java
new file mode 100644
index 000000000..31db7b537
--- /dev/null
+++ b/java/com/android/contacts/common/compat/PhoneCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+public class PhoneCompat {
+
+ // TODO: Use N APIs
+ private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+ Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise");
+
+ public static Uri getContentFilterUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return ENTERPRISE_CONTENT_FILTER_URI;
+ }
+ return Phone.CONTENT_FILTER_URI;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
new file mode 100644
index 000000000..960b340d8
--- /dev/null
+++ b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.telephony.PhoneNumberUtils;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import com.android.dialer.compat.CompatUtils;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+
+/**
+ * This class contains static utility methods extracted from PhoneNumberUtils, and the methods were
+ * added in API level 23. In this way, we could enable the corresponding functionality for pre-M
+ * devices. We need maintain this class and keep it synced with PhoneNumberUtils. Another thing to
+ * keep in mind is that we use com.google.i18n rather than com.android.i18n in here, so we need make
+ * sure the application behavior is preserved.
+ */
+public class PhoneNumberUtilsCompat {
+
+ /** Not instantiable. */
+ private PhoneNumberUtilsCompat() {}
+
+ public static String normalizeNumber(String phoneNumber) {
+ if (CompatUtils.isLollipopCompatible()) {
+ return PhoneNumberUtils.normalizeNumber(phoneNumber);
+ } else {
+ return normalizeNumberInternal(phoneNumber);
+ }
+ }
+
+ /** Implementation copied from {@link PhoneNumberUtils#normalizeNumber} */
+ private static String normalizeNumberInternal(String phoneNumber) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ int len = phoneNumber.length();
+ for (int i = 0; i < len; i++) {
+ char c = phoneNumber.charAt(i);
+ // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)
+ int digit = Character.digit(c, 10);
+ if (digit != -1) {
+ sb.append(digit);
+ } else if (sb.length() == 0 && c == '+') {
+ sb.append(c);
+ } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+ return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber));
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String formatNumber(
+ String phoneNumber, String phoneNumberE164, String defaultCountryIso) {
+ if (CompatUtils.isLollipopCompatible()) {
+ return PhoneNumberUtils.formatNumber(phoneNumber, phoneNumberE164, defaultCountryIso);
+ } else {
+ // This method was deprecated in API level 21, so it's only used on pre-L SDKs.
+ return PhoneNumberUtils.formatNumber(phoneNumber);
+ }
+ }
+
+ public static CharSequence createTtsSpannable(CharSequence phoneNumber) {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return PhoneNumberUtils.createTtsSpannable(phoneNumber);
+ } else {
+ return createTtsSpannableInternal(phoneNumber);
+ }
+ }
+
+ public static TtsSpan createTtsSpan(String phoneNumber) {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return PhoneNumberUtils.createTtsSpan(phoneNumber);
+ } else if (CompatUtils.isLollipopCompatible()) {
+ return createTtsSpanLollipop(phoneNumber);
+ } else {
+ return null;
+ }
+ }
+
+ /** Copied from {@link PhoneNumberUtils#createTtsSpannable} */
+ private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) {
+ if (phoneNumber == null) {
+ return null;
+ }
+ Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber);
+ addTtsSpanInternal(spannable, 0, spannable.length());
+ return spannable;
+ }
+
+ /** Compat method for addTtsSpan, see {@link PhoneNumberUtils#addTtsSpan} */
+ public static void addTtsSpan(Spannable s, int start, int endExclusive) {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ PhoneNumberUtils.addTtsSpan(s, start, endExclusive);
+ } else {
+ addTtsSpanInternal(s, start, endExclusive);
+ }
+ }
+
+ /** Copied from {@link PhoneNumberUtils#addTtsSpan} */
+ private static void addTtsSpanInternal(Spannable s, int start, int endExclusive) {
+ s.setSpan(
+ createTtsSpan(s.subSequence(start, endExclusive).toString()),
+ start,
+ endExclusive,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ /** Copied from {@link PhoneNumberUtils#createTtsSpan} */
+ private static TtsSpan createTtsSpanLollipop(String phoneNumberString) {
+ if (phoneNumberString == null) {
+ return null;
+ }
+
+ // Parse the phone number
+ final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+ PhoneNumber phoneNumber = null;
+ try {
+ // Don't supply a defaultRegion so this fails for non-international numbers because
+ // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already
+ // present
+ phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null);
+ } catch (NumberParseException ignored) {
+ }
+
+ // Build a telephone tts span
+ final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder();
+ if (phoneNumber == null) {
+ // Strip separators otherwise TalkBack will be silent
+ // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel)
+ builder.setNumberParts(splitAtNonNumerics(phoneNumberString));
+ } else {
+ if (phoneNumber.hasCountryCode()) {
+ builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode()));
+ }
+ builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber()));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Split a phone number using spaces, ignoring anything that is not a digit
+ *
+ * @param number A {@code CharSequence} before splitting, e.g., "+20(123)-456#"
+ * @return A {@code String} after splitting, e.g., "20 123 456".
+ */
+ private static String splitAtNonNumerics(CharSequence number) {
+ StringBuilder sb = new StringBuilder(number.length());
+ for (int i = 0; i < number.length(); i++) {
+ sb.append(PhoneNumberUtils.isISODigit(number.charAt(i)) ? number.charAt(i) : " ");
+ }
+ // It is very important to remove extra spaces. At time of writing, any leading or trailing
+ // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS
+ // span to be non-functional!
+ return sb.toString().replaceAll(" +", " ").trim();
+ }
+}
diff --git a/java/com/android/contacts/common/compat/TelephonyManagerCompat.java b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java
new file mode 100644
index 000000000..c8665af51
--- /dev/null
+++ b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import java.lang.reflect.InvocationTargetException;
+
+public class TelephonyManagerCompat {
+
+ // TODO: Use public API for these constants when available
+ public static final String EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE =
+ "android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE";
+ public static final String EVENT_HANDOVER_TO_WIFI_FAILED =
+ "android.telephony.event.EVENT_HANDOVER_TO_WIFI_FAILED";
+ public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD";
+ public static final String EVENT_CALL_REMOTELY_UNHELD =
+ "android.telecom.event.CALL_REMOTELY_UNHELD";
+
+ public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager";
+
+ /**
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @return true if the current device is "voice capable".
+ * <p>"Voice capable" means that this device supports circuit-switched (i.e. voice) phone
+ * calls over the telephony network, and is allowed to display the in-call UI while a cellular
+ * voice call is active. This will be false on "data only" devices which can't make voice
+ * calls and don't support any in-call UI.
+ * <p>Note: the meaning of this flag is subtly different from the
+ * PackageManager.FEATURE_TELEPHONY system feature, which is available on any device with a
+ * telephony radio, even if the device is data-only.
+ */
+ public static boolean isVoiceCapable(@Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return false;
+ }
+ if (CompatUtils.isLollipopMr1Compatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isVoiceCapable")) {
+ // isVoiceCapable was unhidden in L-MR1
+ return telephonyManager.isVoiceCapable();
+ }
+ final int phoneType = telephonyManager.getPhoneType();
+ return phoneType == TelephonyManager.PHONE_TYPE_CDMA
+ || phoneType == TelephonyManager.PHONE_TYPE_GSM;
+ }
+
+ /**
+ * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM
+ * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality)
+ *
+ * <p>Returns 1 if the method or telephonyManager is not available.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ */
+ public static int getPhoneCount(@Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return 1;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getPhoneCount")) {
+ return telephonyManager.getPhoneCount();
+ }
+ return 1;
+ }
+
+ /**
+ * Returns the unique device ID of a subscription, for example, the IMEI for GSM and the MEID for
+ * CDMA phones. Return null if device ID is not available.
+ *
+ * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @param slotId of which deviceID is returned
+ */
+ public static String getDeviceId(@Nullable TelephonyManager telephonyManager, int slotId) {
+ if (telephonyManager == null) {
+ return null;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.class)) {
+ return telephonyManager.getDeviceId(slotId);
+ }
+ return null;
+ }
+
+ /**
+ * Whether the phone supports TTY mode.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @return {@code true} if the device supports TTY mode, and {@code false} otherwise.
+ */
+ public static boolean isTtyModeSupported(@Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return false;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isTtyModeSupported")) {
+ return telephonyManager.isTtyModeSupported();
+ }
+ return false;
+ }
+
+ /**
+ * Whether the phone supports hearing aid compatibility.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @return {@code true} if the device supports hearing aid compatibility, and {@code false}
+ * otherwise.
+ */
+ public static boolean isHearingAidCompatibilitySupported(
+ @Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return false;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELEPHONY_MANAGER_CLASS, "isHearingAidCompatibilitySupported")) {
+ return telephonyManager.isHearingAidCompatibilitySupported();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the URI for the per-account voicemail ringtone set in Phone settings.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+ * retrieve the voicemail ringtone.
+ * @return The URI for the ringtone to play when receiving a voicemail from a specific
+ * PhoneAccount.
+ */
+ @Nullable
+ public static Uri getVoicemailRingtoneUri(
+ TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) {
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ return null;
+ }
+ return telephonyManager.getVoicemailRingtoneUri(accountHandle);
+ }
+
+ /**
+ * Returns whether vibration is set for voicemail notification in Phone settings.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+ * retrieve the voicemail vibration setting.
+ * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise.
+ */
+ public static boolean isVoicemailVibrationEnabled(
+ TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) {
+ return VERSION.SDK_INT < VERSION_CODES.N
+ || telephonyManager.isVoicemailVibrationEnabled(accountHandle);
+ }
+
+ /**
+ * This method uses a new system API to enable or disable visual voicemail. TODO: restrict
+ * to N MR1, not needed in future SDK.
+ */
+ public static void setVisualVoicemailEnabled(
+ TelephonyManager telephonyManager, PhoneAccountHandle handle, boolean enabled) {
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ Assert.fail("setVisualVoicemailEnabled called on pre-NMR1");
+ }
+ try {
+ TelephonyManager.class
+ .getMethod("setVisualVoicemailEnabled", PhoneAccountHandle.class, boolean.class)
+ .invoke(telephonyManager, handle, enabled);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e);
+ }
+ }
+
+ /**
+ * This method uses a new system API to check if visual voicemail is enabled TODO: restrict
+ * to N MR1, not needed in future SDK.
+ */
+ public static boolean isVisualVoicemailEnabled(
+ TelephonyManager telephonyManager, PhoneAccountHandle handle) {
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ Assert.fail("isVisualVoicemailEnabled called on pre-NMR1");
+ }
+ try {
+ return (boolean)
+ TelephonyManager.class
+ .getMethod("isVisualVoicemailEnabled", PhoneAccountHandle.class)
+ .invoke(telephonyManager, handle);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e);
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
new file mode 100644
index 000000000..5687f6fbf
--- /dev/null
+++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+package com.android.contacts.common.compat.telecom;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.dialer.compat.CompatUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Compatibility class for {@link android.telecom.TelecomManager}. */
+public class TelecomManagerCompat {
+
+ public static final String TELECOM_MANAGER_CLASS = "android.telecom.TelecomManager";
+
+ // TODO: remove once this is available in android.telecom.Call
+ // b/33779976
+ public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS =
+ "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
+
+ /**
+ * Places a new outgoing call to the provided address using the system telecom service with the
+ * specified intent.
+ *
+ * @param activity {@link Activity} used to start another activity for the given intent
+ * @param telecomManager the {@link TelecomManager} used to place a call, if possible
+ * @param intent the intent for the call
+ */
+ public static void placeCall(
+ @Nullable Activity activity,
+ @Nullable TelecomManager telecomManager,
+ @Nullable Intent intent) {
+ if (activity == null || telecomManager == null || intent == null) {
+ return;
+ }
+ if (CompatUtils.isMarshmallowCompatible()) {
+ telecomManager.placeCall(intent.getData(), intent.getExtras());
+ return;
+ }
+ activity.startActivityForResult(intent, 0);
+ }
+
+ /**
+ * Get the URI for running an adn query.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @param accountHandle The handle for the account to derive an adn query URI for or {@code null}
+ * to return a URI which will use the default account.
+ * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount} for
+ * the the content retrieve.
+ */
+ public static Uri getAdnUriForPhoneAccount(
+ @Nullable TelecomManager telecomManager, PhoneAccountHandle accountHandle) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getAdnUriForPhoneAccount", PhoneAccountHandle.class))) {
+ return telecomManager.getAdnUriForPhoneAccount(accountHandle);
+ }
+ return Uri.parse("content://icc/adn");
+ }
+
+ /**
+ * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
+ * calls. The returned list includes only those accounts which have been explicitly enabled by the
+ * user.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @return A list of PhoneAccountHandle objects.
+ */
+ public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(
+ @Nullable TelecomManager telecomManager) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getCallCapablePhoneAccounts"))) {
+ return telecomManager.getCallCapablePhoneAccounts();
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Used to determine the currently selected default dialer package.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @return package name for the default dialer package or null if no package has been selected as
+ * the default dialer.
+ */
+ @Nullable
+ public static String getDefaultDialerPackage(@Nullable TelecomManager telecomManager) {
+ if (telecomManager != null && CompatUtils.isDefaultDialerCompatible()) {
+ return telecomManager.getDefaultDialerPackage();
+ }
+ return null;
+ }
+
+ /**
+ * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with
+ * the specified {@code uriScheme}. This PhoneAccount will always be a member of the list which is
+ * returned from invoking {@link TelecomManager#getCallCapablePhoneAccounts()}. The specific
+ * account returned depends on the following priorities:
+ *
+ * <p>1. If the user-selected default PhoneAccount supports the specified scheme, it will be
+ * returned. 2. If there exists only one PhoneAccount that supports the specified scheme, it will
+ * be returned.
+ *
+ * <p>If no PhoneAccount fits the criteria above, this method will return {@code null}.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @param uriScheme The URI scheme.
+ * @return The {@link PhoneAccountHandle} corresponding to the account to be used.
+ */
+ @Nullable
+ public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
+ @Nullable TelecomManager telecomManager, @Nullable String uriScheme) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getDefaultOutgoingPhoneAccount", String.class))) {
+ return telecomManager.getDefaultOutgoingPhoneAccount(uriScheme);
+ }
+ return null;
+ }
+
+ /**
+ * Return the line 1 phone number for given phone account.
+ *
+ * @param telecomManager the {@link TelecomManager} to use in the event that {@link
+ * TelecomManager#getLine1Number(PhoneAccountHandle)} is available
+ * @param telephonyManager the {@link TelephonyManager} to use if TelecomManager#getLine1Number is
+ * unavailable
+ * @param phoneAccountHandle the phoneAccountHandle upon which to check the line one number
+ * @return the line one number
+ */
+ @Nullable
+ public static String getLine1Number(
+ @Nullable TelecomManager telecomManager,
+ @Nullable TelephonyManager telephonyManager,
+ @Nullable PhoneAccountHandle phoneAccountHandle) {
+ if (telecomManager != null && CompatUtils.isMarshmallowCompatible()) {
+ return telecomManager.getLine1Number(phoneAccountHandle);
+ }
+ if (telephonyManager != null) {
+ return telephonyManager.getLine1Number();
+ }
+ return null;
+ }
+
+ /**
+ * Return whether a given phone number is the configured voicemail number for a particular phone
+ * account.
+ *
+ * @param telecomManager the {@link TelecomManager} to use for checking the number.
+ * @param accountHandle The handle for the account to check the voicemail number against
+ * @param number The number to look up.
+ */
+ public static boolean isVoiceMailNumber(
+ @Nullable TelecomManager telecomManager,
+ @Nullable PhoneAccountHandle accountHandle,
+ @Nullable String number) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS,
+ "isVoiceMailNumber",
+ PhoneAccountHandle.class,
+ String.class))) {
+ return telecomManager.isVoiceMailNumber(accountHandle, number);
+ }
+ return PhoneNumberUtils.isVoiceMailNumber(number);
+ }
+
+ /**
+ * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes
+ * resources which can be used in a user interface.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @param account The {@link PhoneAccountHandle}.
+ * @return The {@link PhoneAccount} object or null if it doesn't exist.
+ */
+ @Nullable
+ public static PhoneAccount getPhoneAccount(
+ @Nullable TelecomManager telecomManager, @Nullable PhoneAccountHandle accountHandle) {
+ if (telecomManager != null
+ && (CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getPhoneAccount", PhoneAccountHandle.class))) {
+ return telecomManager.getPhoneAccount(accountHandle);
+ }
+ return null;
+ }
+
+ /**
+ * Return the voicemail number for a given phone account.
+ *
+ * @param telecomManager The {@link TelecomManager} object to use for retrieving the voicemail
+ * number if accountHandle is specified.
+ * @param telephonyManager The {@link TelephonyManager} object to use for retrieving the voicemail
+ * number if accountHandle is null.
+ * @param accountHandle The handle for the phone account.
+ * @return The voicemail number for the phone account, and {@code null} if one has not been
+ * configured.
+ */
+ @Nullable
+ public static String getVoiceMailNumber(
+ @Nullable TelecomManager telecomManager,
+ @Nullable TelephonyManager telephonyManager,
+ @Nullable PhoneAccountHandle accountHandle) {
+ if (telecomManager != null
+ && (CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getVoiceMailNumber", PhoneAccountHandle.class))) {
+ return telecomManager.getVoiceMailNumber(accountHandle);
+ } else if (telephonyManager != null) {
+ return telephonyManager.getVoiceMailNumber();
+ }
+ return null;
+ }
+
+ /**
+ * Processes the specified dial string as an MMI code. MMI codes are any sequence of characters
+ * entered into the dialpad that contain a "*" or "#". Some of these sequences launch special
+ * behavior through handled by Telephony.
+ *
+ * @param telecomManager The {@link TelecomManager} object to use for handling MMI.
+ * @param dialString The digits to dial.
+ * @return {@code true} if the digits were processed as an MMI code, {@code false} otherwise.
+ */
+ public static boolean handleMmi(
+ @Nullable TelecomManager telecomManager,
+ @Nullable String dialString,
+ @Nullable PhoneAccountHandle accountHandle) {
+ if (telecomManager == null || TextUtils.isEmpty(dialString)) {
+ return false;
+ }
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return telecomManager.handleMmi(dialString, accountHandle);
+ }
+
+ Object handleMmiResult =
+ CompatUtils.invokeMethod(
+ telecomManager,
+ "handleMmi",
+ new Class<?>[] {PhoneAccountHandle.class, String.class},
+ new Object[] {accountHandle, dialString});
+ if (handleMmiResult != null) {
+ return (boolean) handleMmiResult;
+ }
+
+ return telecomManager.handleMmi(dialString);
+ }
+
+ /**
+ * Silences the ringer if a ringing call exists. Noop if {@link TelecomManager#silenceRinger()} is
+ * unavailable.
+ *
+ * @param telecomManager the TelecomManager to use to silence the ringer.
+ */
+ public static void silenceRinger(@Nullable TelecomManager telecomManager) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "silenceRinger"))) {
+ telecomManager.silenceRinger();
+ }
+ }
+
+ /**
+ * Returns the current SIM call manager. Apps must be prepared for this method to return null,
+ * indicating that there currently exists no registered SIM call manager.
+ *
+ * @param telecomManager the {@link TelecomManager} to use to fetch the SIM call manager.
+ * @return The phone account handle of the current sim call manager.
+ */
+ @Nullable
+ public static PhoneAccountHandle getSimCallManager(TelecomManager telecomManager) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "getSimCallManager"))) {
+ return telecomManager.getSimCallManager();
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/contacts/common/database/ContactUpdateUtils.java b/java/com/android/contacts/common/database/ContactUpdateUtils.java
new file mode 100644
index 000000000..1a9febc07
--- /dev/null
+++ b/java/com/android/contacts/common/database/ContactUpdateUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common.database;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/** Static methods to update contact information. */
+public class ContactUpdateUtils {
+
+ private static final String TAG = ContactUpdateUtils.class.getSimpleName();
+
+ public static void setSuperPrimary(Context context, long dataId) {
+ if (dataId == -1) {
+ Log.e(TAG, "Invalid arguments for setSuperPrimary request");
+ return;
+ }
+
+ // Update the primary values in the data record.
+ ContentValues values = new ContentValues(2);
+ values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
+ values.put(ContactsContract.Data.IS_PRIMARY, 1);
+
+ context
+ .getContentResolver()
+ .update(
+ ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId),
+ values,
+ null,
+ null);
+ }
+}
diff --git a/java/com/android/contacts/common/database/EmptyCursor.java b/java/com/android/contacts/common/database/EmptyCursor.java
new file mode 100644
index 000000000..c2b24cdf7
--- /dev/null
+++ b/java/com/android/contacts/common/database/EmptyCursor.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common.database;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+
+/**
+ * A cursor that is empty.
+ *
+ * <p>If you want an empty cursor, this class is better than a MatrixCursor because it has less
+ * overhead.
+ */
+public final class EmptyCursor extends AbstractCursor {
+
+ private String[] mColumns;
+
+ public EmptyCursor(String[] columns) {
+ this.mColumns = columns;
+ }
+
+ @Override
+ public int getCount() {
+ return 0;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumns;
+ }
+
+ @Override
+ public String getString(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public short getShort(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public int getInt(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public long getLong(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public float getFloat(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ throw cursorException();
+ }
+
+ private CursorIndexOutOfBoundsException cursorException() {
+ return new CursorIndexOutOfBoundsException("Operation not permitted on an empty cursor.");
+ }
+}
diff --git a/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java
new file mode 100644
index 000000000..d5e61354a
--- /dev/null
+++ b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common.database;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * An {@AsyncQueryHandler} that will never return a null cursor.
+ *
+ * <p>Instead, will return a {@link Cursor} with 0 records.
+ */
+public abstract class NoNullCursorAsyncQueryHandler extends AsyncQueryHandler {
+
+ public NoNullCursorAsyncQueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ public void startQuery(
+ int token,
+ Object cookie,
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String orderBy) {
+ final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection);
+ super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ @Override
+ protected final void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ CookieWithProjection projectionCookie = (CookieWithProjection) cookie;
+
+ super.onQueryComplete(token, projectionCookie.originalCookie, cursor);
+
+ if (cursor == null) {
+ cursor = new EmptyCursor(projectionCookie.projection);
+ }
+ onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor);
+ }
+
+ protected abstract void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor);
+
+ /** Class to add projection to an existing cookie. */
+ private static class CookieWithProjection {
+
+ public final Object originalCookie;
+ public final String[] projection;
+
+ public CookieWithProjection(Object cookie, String[] projection) {
+ this.originalCookie = cookie;
+ this.projection = projection;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/dialog/CallSubjectDialog.java b/java/com/android/contacts/common/dialog/CallSubjectDialog.java
new file mode 100644
index 000000000..d2e3a2357
--- /dev/null
+++ b/java/com/android/contacts/common/dialog/CallSubjectDialog.java
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2015 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
+ */
+
+package com.android.contacts.common.dialog;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.ViewUtil;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements a dialog which prompts for a call subject for an outgoing call. The dialog includes a
+ * pop up list of historical call subjects.
+ */
+public class CallSubjectDialog extends Activity {
+
+ public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count";
+ public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item";
+ /** Activity intent argument bundle keys: */
+ public static final String ARG_PHOTO_ID = "PHOTO_ID";
+
+ public static final String ARG_PHOTO_URI = "PHOTO_URI";
+ public static final String ARG_CONTACT_URI = "CONTACT_URI";
+ public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER";
+ public static final String ARG_IS_BUSINESS = "IS_BUSINESS";
+ public static final String ARG_NUMBER = "NUMBER";
+ public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER";
+ public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL";
+ public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE";
+ private static final int CALL_SUBJECT_LIMIT = 16;
+ private static final int CALL_SUBJECT_HISTORY_SIZE = 5;
+ private int mAnimationDuration;
+ private Charset mMessageEncoding;
+ private View mBackgroundView;
+ private View mDialogView;
+ private QuickContactBadge mContactPhoto;
+ private TextView mNameView;
+ private TextView mNumberView;
+ private EditText mCallSubjectView;
+ private TextView mCharacterLimitView;
+ private View mHistoryButton;
+ private View mSendAndCallButton;
+ private ListView mSubjectList;
+
+ private int mLimit = CALL_SUBJECT_LIMIT;
+ /** Handles changes to the text in the subject box. Ensures the character limit is updated. */
+ private final TextWatcher mTextWatcher =
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // no-op
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateCharacterLimit();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // no-op
+ }
+ };
+
+ private int mPhotoSize;
+ private SharedPreferences mPrefs;
+ private List<String> mSubjectHistory;
+ /** Handles displaying the list of past call subjects. */
+ private final View.OnClickListener mHistoryOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView);
+ showCallHistory(mSubjectList.getVisibility() == View.GONE);
+ }
+ };
+ /**
+ * Handles auto-hiding the call history when user clicks in the call subject field to give it
+ * focus.
+ */
+ private final View.OnClickListener mCallSubjectClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mSubjectList.getVisibility() == View.VISIBLE) {
+ showCallHistory(false);
+ }
+ }
+ };
+
+ private long mPhotoID;
+ private Uri mPhotoUri;
+ private Uri mContactUri;
+ private String mNameOrNumber;
+ private boolean mIsBusiness;
+ private String mNumber;
+ private String mDisplayNumber;
+ private String mNumberLabel;
+ private PhoneAccountHandle mPhoneAccountHandle;
+ /** Handles starting a call with a call subject specified. */
+ private final View.OnClickListener mSendAndCallOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String subject = mCallSubjectView.getText().toString();
+ Intent intent =
+ new CallIntentBuilder(mNumber, CallInitiationType.Type.CALL_SUBJECT_DIALOG)
+ .setPhoneAccountHandle(mPhoneAccountHandle)
+ .setCallSubject(subject)
+ .build();
+
+ TelecomManagerCompat.placeCall(
+ CallSubjectDialog.this,
+ (TelecomManager) getSystemService(Context.TELECOM_SERVICE),
+ intent);
+
+ mSubjectHistory.add(subject);
+ saveSubjectHistory(mSubjectHistory);
+ finish();
+ }
+ };
+ /** Click listener which handles user clicks outside of the dialog. */
+ private View.OnClickListener mBackgroundListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ };
+ /**
+ * Item click listener which handles user clicks on the items in the list view. Dismisses the
+ * activity, returning the subject to the caller and closing the activity with the {@link
+ * Activity#RESULT_OK} result code.
+ */
+ private AdapterView.OnItemClickListener mItemClickListener =
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) {
+ mCallSubjectView.setText(mSubjectHistory.get(position));
+ showCallHistory(false);
+ }
+ };
+
+ /**
+ * Show the call subject dialog given a phone number to dial (e.g. from the dialpad).
+ *
+ * @param activity The activity.
+ * @param number The number to dial.
+ */
+ public static void start(Activity activity, String number) {
+ start(
+ activity,
+ -1 /* photoId */,
+ null /* photoUri */,
+ null /* contactUri */,
+ number /* nameOrNumber */,
+ false /* isBusiness */,
+ number /* number */,
+ null /* displayNumber */,
+ null /* numberLabel */,
+ null /* phoneAccountHandle */);
+ }
+
+ /**
+ * Creates a call subject dialog.
+ *
+ * @param activity The current activity.
+ * @param photoId The photo ID (used to populate contact photo).
+ * @param photoUri The photo Uri (used to populate contact photo).
+ * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo).
+ * @param nameOrNumber The name or number of the callee.
+ * @param isBusiness {@code true} if a business is being called (used for contact photo).
+ * @param number The raw number to dial.
+ * @param displayNumber The number to dial, formatted for display.
+ * @param numberLabel The label for the number (if from a contact).
+ * @param phoneAccountHandle The phone account handle.
+ */
+ public static void start(
+ Activity activity,
+ long photoId,
+ Uri photoUri,
+ Uri contactUri,
+ String nameOrNumber,
+ boolean isBusiness,
+ String number,
+ String displayNumber,
+ String numberLabel,
+ PhoneAccountHandle phoneAccountHandle) {
+ Bundle arguments = new Bundle();
+ arguments.putLong(ARG_PHOTO_ID, photoId);
+ arguments.putParcelable(ARG_PHOTO_URI, photoUri);
+ arguments.putParcelable(ARG_CONTACT_URI, contactUri);
+ arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber);
+ arguments.putBoolean(ARG_IS_BUSINESS, isBusiness);
+ arguments.putString(ARG_NUMBER, number);
+ arguments.putString(ARG_DISPLAY_NUMBER, displayNumber);
+ arguments.putString(ARG_NUMBER_LABEL, numberLabel);
+ arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ start(activity, arguments);
+ }
+
+ /**
+ * Shows the call subject dialog given a Bundle containing all the arguments required to display
+ * the dialog (e.g. from Quick Contacts).
+ *
+ * @param activity The activity.
+ * @param arguments The arguments bundle.
+ */
+ public static void start(Activity activity, Bundle arguments) {
+ Intent intent = new Intent(activity, CallSubjectDialog.class);
+ intent.putExtras(arguments);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Loads the subject history from shared preferences.
+ *
+ * @param prefs Shared preferences.
+ * @return List of subject history strings.
+ */
+ public static List<String> loadSubjectHistory(SharedPreferences prefs) {
+ int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0);
+ List<String> subjects = new ArrayList(historySize);
+
+ for (int ix = 0; ix < historySize; ix++) {
+ String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null);
+ if (!TextUtils.isEmpty(historyItem)) {
+ subjects.add(historyItem);
+ }
+ }
+
+ return subjects;
+ }
+
+ /**
+ * Creates the dialog, inflating the layout and populating it with the name and phone number.
+ *
+ * @param savedInstanceState The last saved instance state of the Fragment, or null if this is a
+ * freshly created Fragment.
+ * @return Dialog instance.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mPhotoSize =
+ getResources().getDimensionPixelSize(R.dimen.call_subject_dialog_contact_photo_size);
+ readArguments();
+ loadConfiguration();
+ mSubjectHistory = loadSubjectHistory(mPrefs);
+
+ setContentView(R.layout.dialog_call_subject);
+ getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ mBackgroundView = findViewById(R.id.call_subject_dialog);
+ mBackgroundView.setOnClickListener(mBackgroundListener);
+ mDialogView = findViewById(R.id.dialog_view);
+ mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
+ mNameView = (TextView) findViewById(R.id.name);
+ mNumberView = (TextView) findViewById(R.id.number);
+ mCallSubjectView = (EditText) findViewById(R.id.call_subject);
+ mCallSubjectView.addTextChangedListener(mTextWatcher);
+ mCallSubjectView.setOnClickListener(mCallSubjectClickListener);
+ InputFilter[] filters = new InputFilter[1];
+ filters[0] = new InputFilter.LengthFilter(mLimit);
+ mCallSubjectView.setFilters(filters);
+ mCharacterLimitView = (TextView) findViewById(R.id.character_limit);
+ mHistoryButton = findViewById(R.id.history_button);
+ mHistoryButton.setOnClickListener(mHistoryOnClickListener);
+ mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE);
+ mSendAndCallButton = findViewById(R.id.send_and_call_button);
+ mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener);
+ mSubjectList = (ListView) findViewById(R.id.subject_list);
+ mSubjectList.setOnItemClickListener(mItemClickListener);
+ mSubjectList.setVisibility(View.GONE);
+
+ updateContactInfo();
+ updateCharacterLimit();
+ }
+
+ /** Populates the contact info fields based on the current contact information. */
+ private void updateContactInfo() {
+ if (mContactUri != null) {
+ setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness);
+ } else {
+ mContactPhoto.setVisibility(View.GONE);
+ }
+ mNameView.setText(mNameOrNumber);
+ if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) {
+ mNumberView.setVisibility(View.VISIBLE);
+ mNumberView.setText(
+ getString(R.string.call_subject_type_and_number, mNumberLabel, mDisplayNumber));
+ } else {
+ mNumberView.setVisibility(View.GONE);
+ mNumberView.setText(null);
+ }
+ }
+
+ /** Reads arguments from the fragment arguments and populates the necessary instance variables. */
+ private void readArguments() {
+ Bundle arguments = getIntent().getExtras();
+ if (arguments == null) {
+ LogUtil.e("CallSubjectDialog.readArguments", "arguments cannot be null");
+ return;
+ }
+ mPhotoID = arguments.getLong(ARG_PHOTO_ID);
+ mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI);
+ mContactUri = arguments.getParcelable(ARG_CONTACT_URI);
+ mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER);
+ mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS);
+ mNumber = arguments.getString(ARG_NUMBER);
+ mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER);
+ mNumberLabel = arguments.getString(ARG_NUMBER_LABEL);
+ mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE);
+ }
+
+ /**
+ * Updates the character limit display, coloring the text RED when the limit is reached or
+ * exceeded.
+ */
+ private void updateCharacterLimit() {
+ String subjectText = mCallSubjectView.getText().toString();
+ final int length;
+
+ // If a message encoding is specified, use that to count bytes in the message.
+ if (mMessageEncoding != null) {
+ length = subjectText.getBytes(mMessageEncoding).length;
+ } else {
+ // No message encoding specified, so just count characters entered.
+ length = subjectText.length();
+ }
+
+ mCharacterLimitView.setText(getString(R.string.call_subject_limit, length, mLimit));
+ if (length >= mLimit) {
+ mCharacterLimitView.setTextColor(
+ getResources().getColor(R.color.call_subject_limit_exceeded));
+ } else {
+ mCharacterLimitView.setTextColor(
+ getResources().getColor(R.color.dialer_secondary_text_color));
+ }
+ }
+
+ /** Sets the photo on the quick contact photo. */
+ private void setPhoto(
+ long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) {
+ mContactPhoto.assignContactUri(contactUri);
+ if (CompatUtils.isLollipopCompatible()) {
+ mContactPhoto.setOverlay(null);
+ }
+
+ int contactType;
+ if (isBusiness) {
+ contactType = ContactPhotoManager.TYPE_BUSINESS;
+ } else {
+ contactType = ContactPhotoManager.TYPE_DEFAULT;
+ }
+
+ String lookupKey = null;
+ if (contactUri != null) {
+ lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
+ }
+
+ ContactPhotoManager.DefaultImageRequest request =
+ new ContactPhotoManager.DefaultImageRequest(
+ displayName, lookupKey, contactType, true /* isCircular */);
+
+ if (photoId == 0 && photoUri != null) {
+ ContactPhotoManager.getInstance(this)
+ .loadPhoto(
+ mContactPhoto,
+ photoUri,
+ mPhotoSize,
+ false /* darkTheme */,
+ true /* isCircular */,
+ request);
+ } else {
+ ContactPhotoManager.getInstance(this)
+ .loadThumbnail(
+ mContactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request);
+ }
+ }
+
+ /**
+ * Saves the subject history list to shared prefs, removing older items so that there are only
+ * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most.
+ *
+ * @param history The history.
+ */
+ private void saveSubjectHistory(List<String> history) {
+ // Remove oldest subject(s).
+ while (history.size() > CALL_SUBJECT_HISTORY_SIZE) {
+ history.remove(0);
+ }
+
+ SharedPreferences.Editor editor = mPrefs.edit();
+ int historyCount = 0;
+ for (String subject : history) {
+ if (!TextUtils.isEmpty(subject)) {
+ editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, subject);
+ historyCount++;
+ }
+ }
+ editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount);
+ editor.apply();
+ }
+
+ /** Hide software keyboard for the given {@link View}. */
+ public void hideSoftKeyboard(Context context, View view) {
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ /**
+ * Hides or shows the call history list.
+ *
+ * @param show {@code true} if the call history should be shown, {@code false} otherwise.
+ */
+ private void showCallHistory(final boolean show) {
+ // Bail early if the visibility has not changed.
+ if ((show && mSubjectList.getVisibility() == View.VISIBLE)
+ || (!show && mSubjectList.getVisibility() == View.GONE)) {
+ return;
+ }
+
+ final int dialogStartingBottom = mDialogView.getBottom();
+ if (show) {
+ // Showing the subject list; bind the list of history items to the list and show it.
+ ArrayAdapter<String> adapter =
+ new ArrayAdapter<String>(
+ CallSubjectDialog.this, R.layout.call_subject_history_list_item, mSubjectHistory);
+ mSubjectList.setAdapter(adapter);
+ mSubjectList.setVisibility(View.VISIBLE);
+ } else {
+ // Hiding the subject list.
+ mSubjectList.setVisibility(View.GONE);
+ }
+
+ // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout
+ // states.
+ ViewUtil.doOnPreDraw(
+ mBackgroundView,
+ true,
+ new Runnable() {
+ @Override
+ public void run() {
+ // Determine the amount the dialog has shifted due to the relayout.
+ int shiftAmount = dialogStartingBottom - mDialogView.getBottom();
+
+ // If the dialog needs to be shifted, do that now.
+ if (shiftAmount != 0) {
+ // Start animation in translated state and animate to translationY 0.
+ mDialogView.setTranslationY(shiftAmount);
+ mDialogView
+ .animate()
+ .translationY(0)
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setDuration(mAnimationDuration)
+ .start();
+ }
+
+ if (show) {
+ // Show the subject list.
+ mSubjectList.setTranslationY(mSubjectList.getHeight());
+
+ mSubjectList
+ .animate()
+ .translationY(0)
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setDuration(mAnimationDuration)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mSubjectList.setVisibility(View.VISIBLE);
+ }
+ })
+ .start();
+ } else {
+ // Hide the subject list.
+ mSubjectList.setTranslationY(0);
+
+ mSubjectList
+ .animate()
+ .translationY(mSubjectList.getHeight())
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setDuration(mAnimationDuration)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mSubjectList.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ }
+ })
+ .start();
+ }
+ }
+ });
+ }
+
+ /**
+ * Loads the message encoding and maximum message length from the phone account extras for the
+ * current phone account.
+ */
+ private void loadConfiguration() {
+ // Only attempt to load configuration from the phone account extras if the SDK is N or
+ // later. If we've got a prior SDK the default encoding and message length will suffice.
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ return;
+ }
+
+ if (mPhoneAccountHandle == null) {
+ return;
+ }
+
+ TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
+ final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle);
+
+ Bundle phoneAccountExtras = account.getExtras();
+ if (phoneAccountExtras == null) {
+ return;
+ }
+
+ // Get limit, if provided; otherwise default to existing value.
+ mLimit = phoneAccountExtras.getInt(PhoneAccount.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit);
+
+ // Get charset; default to none (e.g. count characters 1:1).
+ String charsetName =
+ phoneAccountExtras.getString(PhoneAccount.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING);
+
+ if (!TextUtils.isEmpty(charsetName)) {
+ try {
+ mMessageEncoding = Charset.forName(charsetName);
+ } catch (java.nio.charset.UnsupportedCharsetException uce) {
+ // Character set was invalid; log warning and fallback to none.
+ LogUtil.e("CallSubjectDialog.loadConfiguration", "invalid charset: " + charsetName);
+ mMessageEncoding = null;
+ }
+ } else {
+ // No character set specified, so count characters 1:1.
+ mMessageEncoding = null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java
new file mode 100644
index 000000000..e96496cda
--- /dev/null
+++ b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common.dialog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import com.android.contacts.common.R;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Dialog that clears the frequently contacted list after confirming with the user. */
+public class ClearFrequentsDialog extends DialogFragment {
+
+ /** Preferred way to show this dialog */
+ public static void show(FragmentManager fragmentManager) {
+ ClearFrequentsDialog dialog = new ClearFrequentsDialog();
+ dialog.show(fragmentManager, "clearFrequents");
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity().getApplicationContext();
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final OnClickListener okListener =
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (!PermissionsUtil.hasContactsPermissions(context)) {
+ return;
+ }
+
+ final ProgressDialog progressDialog =
+ ProgressDialog.show(
+ getContext(),
+ getString(R.string.clearFrequentsProgress_title),
+ null,
+ true,
+ true);
+
+ final AsyncTask<Void, Void, Void> task =
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ resolver.delete(
+ ContactsContract.DataUsageFeedback.DELETE_USAGE_URI, null, null);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ progressDialog.dismiss();
+ }
+ };
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ };
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clearFrequentsConfirmation_title)
+ .setMessage(R.string.clearFrequentsConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, okListener)
+ .setCancelable(true)
+ .create();
+ }
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java
new file mode 100644
index 000000000..2607ad19a
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+import com.android.contacts.common.list.DirectoryPartition;
+import java.util.List;
+
+/** An interface for adding extended phone directories. */
+public interface PhoneDirectoryExtender {
+ /**
+ * Return a list of extended directories to add. May return null if no directories are to be
+ * added.
+ */
+ List<DirectoryPartition> getExtendedDirectories(Context context);
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java
new file mode 100644
index 000000000..84649f1ed
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+
+/** Accessor for the phone directory extender singleton. */
+public final class PhoneDirectoryExtenderAccessor {
+
+ private static PhoneDirectoryExtender instance;
+
+ private PhoneDirectoryExtenderAccessor() {}
+
+ @NonNull
+ public static PhoneDirectoryExtender get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof PhoneDirectoryExtenderFactory) {
+ instance = ((PhoneDirectoryExtenderFactory) application).newPhoneDirectoryExtender();
+ }
+
+ if (instance == null) {
+ instance = new PhoneDirectoryExtenderStub();
+ }
+ return instance;
+ }
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java
new file mode 100644
index 000000000..9750ee300
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.support.annotation.NonNull;
+
+/**
+ * This interface should be implemented by the Application subclass. It allows the contacts module
+ * to get references to the PhoneDirectoryExtender.
+ */
+public interface PhoneDirectoryExtenderFactory {
+
+ @NonNull
+ PhoneDirectoryExtender newPhoneDirectoryExtender();
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java
new file mode 100644
index 000000000..95f971533
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+import com.android.contacts.common.list.DirectoryPartition;
+import java.util.Collections;
+import java.util.List;
+
+/** No-op implementation for phone directory extender. */
+class PhoneDirectoryExtenderStub implements PhoneDirectoryExtender {
+
+ @Override
+ public List<DirectoryPartition> getExtendedDirectories(Context context) {
+ return Collections.emptyList();
+ }
+}
diff --git a/java/com/android/contacts/common/format/FormatUtils.java b/java/com/android/contacts/common/format/FormatUtils.java
new file mode 100644
index 000000000..727c15b83
--- /dev/null
+++ b/java/com/android/contacts/common/format/FormatUtils.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.contacts.common.format;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.support.annotation.VisibleForTesting;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import java.util.Arrays;
+
+/** Assorted utility methods related to text formatting in Contacts. */
+public class FormatUtils {
+
+ /**
+ * Finds the earliest point in buffer1 at which the first part of buffer2 matches. For example,
+ * overlapPoint("abcd", "cdef") == 2.
+ */
+ public static int overlapPoint(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+ if (buffer1 == null || buffer2 == null) {
+ return -1;
+ }
+ return overlapPoint(
+ Arrays.copyOfRange(buffer1.data, 0, buffer1.sizeCopied),
+ Arrays.copyOfRange(buffer2.data, 0, buffer2.sizeCopied));
+ }
+
+ /**
+ * Finds the earliest point in string1 at which the first part of string2 matches. For example,
+ * overlapPoint("abcd", "cdef") == 2.
+ */
+ @VisibleForTesting
+ public static int overlapPoint(String string1, String string2) {
+ if (string1 == null || string2 == null) {
+ return -1;
+ }
+ return overlapPoint(string1.toCharArray(), string2.toCharArray());
+ }
+
+ /**
+ * Finds the earliest point in array1 at which the first part of array2 matches. For example,
+ * overlapPoint("abcd", "cdef") == 2.
+ */
+ public static int overlapPoint(char[] array1, char[] array2) {
+ if (array1 == null || array2 == null) {
+ return -1;
+ }
+ int count1 = array1.length;
+ int count2 = array2.length;
+
+ // Ignore matching tails of the two arrays.
+ while (count1 > 0 && count2 > 0 && array1[count1 - 1] == array2[count2 - 1]) {
+ count1--;
+ count2--;
+ }
+
+ int size = count2;
+ for (int i = 0; i < count1; i++) {
+ if (i + size > count1) {
+ size = count1 - i;
+ }
+ int j;
+ for (j = 0; j < size; j++) {
+ if (array1[i + j] != array2[j]) {
+ break;
+ }
+ }
+ if (j == size) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Applies the given style to a range of the input CharSequence.
+ *
+ * @param style The style to apply (see the style constants in {@link Typeface}).
+ * @param input The CharSequence to style.
+ * @param start Starting index of the range to style (will be clamped to be a minimum of 0).
+ * @param end Ending index of the range to style (will be clamped to a maximum of the input
+ * length).
+ * @param flags Bitmask for configuring behavior of the span. See {@link android.text.Spanned}.
+ * @return The styled CharSequence.
+ */
+ public static CharSequence applyStyleToSpan(
+ int style, CharSequence input, int start, int end, int flags) {
+ // Enforce bounds of the char sequence.
+ start = Math.max(0, start);
+ end = Math.min(input.length(), end);
+ SpannableString text = new SpannableString(input);
+ text.setSpan(new StyleSpan(style), start, end, flags);
+ return text;
+ }
+
+ @VisibleForTesting
+ public static void copyToCharArrayBuffer(String text, CharArrayBuffer buffer) {
+ if (text != null) {
+ char[] data = buffer.data;
+ if (data == null || data.length < text.length()) {
+ buffer.data = text.toCharArray();
+ } else {
+ text.getChars(0, text.length(), data, 0);
+ }
+ buffer.sizeCopied = text.length();
+ } else {
+ buffer.sizeCopied = 0;
+ }
+ }
+
+ /** Returns a String that represents the content of the given {@link CharArrayBuffer}. */
+ @VisibleForTesting
+ public static String charArrayBufferToString(CharArrayBuffer buffer) {
+ return new String(buffer.data, 0, buffer.sizeCopied);
+ }
+
+ /**
+ * Finds the index of the first word that starts with the given prefix.
+ *
+ * <p>If not found, returns -1.
+ *
+ * @param text the text in which to search for the prefix
+ * @param prefix the text to find, in upper case letters
+ */
+ public static int indexOfWordPrefix(CharSequence text, String prefix) {
+ if (prefix == null || text == null) {
+ return -1;
+ }
+
+ int textLength = text.length();
+ int prefixLength = prefix.length();
+
+ if (prefixLength == 0 || textLength < prefixLength) {
+ return -1;
+ }
+
+ int i = 0;
+ while (i < textLength) {
+ // Skip non-word characters
+ while (i < textLength && !Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+
+ if (i + prefixLength > textLength) {
+ return -1;
+ }
+
+ // Compare the prefixes
+ int j;
+ for (j = 0; j < prefixLength; j++) {
+ if (Character.toUpperCase(text.charAt(i + j)) != prefix.charAt(j)) {
+ break;
+ }
+ }
+ if (j == prefixLength) {
+ return i;
+ }
+
+ // Skip this word
+ while (i < textLength && Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/java/com/android/contacts/common/format/TextHighlighter.java b/java/com/android/contacts/common/format/TextHighlighter.java
new file mode 100644
index 000000000..30c03fdf3
--- /dev/null
+++ b/java/com/android/contacts/common/format/TextHighlighter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.format;
+
+import android.text.SpannableString;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+
+/** Highlights the text in a text field. */
+public class TextHighlighter {
+
+ private static final boolean DEBUG = false;
+ private final String TAG = TextHighlighter.class.getSimpleName();
+ private int mTextStyle;
+
+ private CharacterStyle mTextStyleSpan;
+
+ public TextHighlighter(int textStyle) {
+ mTextStyle = textStyle;
+ mTextStyleSpan = getStyleSpan();
+ }
+
+ /**
+ * Sets the text on the given text view, highlighting the word that matches the given prefix.
+ *
+ * @param view the view on which to set the text
+ * @param text the string to use as the text
+ * @param prefix the prefix to look for
+ */
+ public void setPrefixText(TextView view, String text, String prefix) {
+ view.setText(applyPrefixHighlight(text, prefix));
+ }
+
+ private CharacterStyle getStyleSpan() {
+ return new StyleSpan(mTextStyle);
+ }
+
+ /**
+ * Applies highlight span to the text.
+ *
+ * @param text Text sequence to be highlighted.
+ * @param start Start position of the highlight sequence.
+ * @param end End position of the highlight sequence.
+ */
+ public void applyMaskingHighlight(SpannableString text, int start, int end) {
+ /** Sets text color of the masked locations to be highlighted. */
+ text.setSpan(getStyleSpan(), start, end, 0);
+ }
+
+ /**
+ * Returns a CharSequence which highlights the given prefix if found in the given text.
+ *
+ * @param text the text to which to apply the highlight
+ * @param prefix the prefix to look for
+ */
+ public CharSequence applyPrefixHighlight(CharSequence text, String prefix) {
+ if (prefix == null) {
+ return text;
+ }
+
+ // Skip non-word characters at the beginning of prefix.
+ int prefixStart = 0;
+ while (prefixStart < prefix.length()
+ && !Character.isLetterOrDigit(prefix.charAt(prefixStart))) {
+ prefixStart++;
+ }
+ final String trimmedPrefix = prefix.substring(prefixStart);
+
+ int index = FormatUtils.indexOfWordPrefix(text, trimmedPrefix);
+ if (index != -1) {
+ final SpannableString result = new SpannableString(text);
+ result.setSpan(mTextStyleSpan, index, index + trimmedPrefix.length(), 0 /* flags */);
+ return result;
+ } else {
+ return text;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/format/testing/SpannedTestUtils.java b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java
new file mode 100644
index 000000000..293d9d5ad
--- /dev/null
+++ b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.format.testing;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+import junit.framework.Assert;
+
+/** Utility class to check the value of spanned text in text views. */
+@SmallTest
+public class SpannedTestUtils {
+
+ /**
+ * Checks that the text contained in the text view matches the given HTML text.
+ *
+ * @param expectedHtmlText the expected text to be in the text view
+ * @param textView the text view from which to get the text
+ */
+ public static void checkHtmlText(String expectedHtmlText, TextView textView) {
+ String actualHtmlText = Html.toHtml((Spanned) textView.getText());
+ if (TextUtils.isEmpty(expectedHtmlText)) {
+ // If the text is empty, it does not add the <p></p> bits to it.
+ Assert.assertEquals("", actualHtmlText);
+ } else {
+ Assert.assertEquals("<p dir=ltr>" + expectedHtmlText + "</p>\n", actualHtmlText);
+ }
+ }
+
+ /**
+ * Assert span exists in the correct location.
+ *
+ * @param seq The spannable string to check.
+ * @param start The starting index.
+ * @param end The ending index.
+ */
+ public static void assertPrefixSpan(CharSequence seq, int start, int end) {
+ Assert.assertTrue(seq instanceof Spanned);
+ Spanned spannable = (Spanned) seq;
+
+ if (start > 0) {
+ Assert.assertEquals(0, getNumForegroundColorSpansBetween(spannable, 0, start - 1));
+ }
+ Assert.assertEquals(1, getNumForegroundColorSpansBetween(spannable, start, end));
+ Assert.assertEquals(
+ 0, getNumForegroundColorSpansBetween(spannable, end + 1, spannable.length() - 1));
+ }
+
+ private static int getNumForegroundColorSpansBetween(Spanned value, int start, int end) {
+ return value.getSpans(start, end, StyleSpan.class).length;
+ }
+
+ /**
+ * Asserts that the given character sequence is not a Spanned object and text is correct.
+ *
+ * @param seq The sequence to check.
+ * @param expected The expected text.
+ */
+ public static void assertNotSpanned(CharSequence seq, String expected) {
+ Assert.assertFalse(seq instanceof Spanned);
+ Assert.assertEquals(expected, seq);
+ }
+
+ public static int getNextTransition(SpannableString seq, int start) {
+ return seq.nextSpanTransition(start, seq.length(), StyleSpan.class);
+ }
+}
diff --git a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
new file mode 100644
index 000000000..7e1839c1e
--- /dev/null
+++ b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.contacts.common.lettertiles;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.dialer.common.Assert;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A drawable that encapsulates all the functionality needed to display a letter tile to represent a
+ * contact image.
+ */
+public class LetterTileDrawable extends Drawable {
+
+ /**
+ * ContactType indicates the avatar type of the contact. For a person or for the default when no
+ * name is provided, it is {@link #TYPE_DEFAULT}, otherwise, for a business it is {@link
+ * #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL})
+ public @interface ContactType {}
+
+ /** Contact type constants */
+ public static final int TYPE_PERSON = 1;
+ public static final int TYPE_BUSINESS = 2;
+ public static final int TYPE_VOICEMAIL = 3;
+ @ContactType public static final int TYPE_DEFAULT = TYPE_PERSON;
+
+ /**
+ * Shape indicates the letter tile shape. It can be either a {@link #SHAPE_CIRCLE}, otherwise, it
+ * is a {@link #SHAPE_RECTANGLE}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SHAPE_CIRCLE, SHAPE_RECTANGLE})
+ public @interface Shape {}
+
+ /** Shape constants */
+ public static final int SHAPE_CIRCLE = 1;
+
+ public static final int SHAPE_RECTANGLE = 2;
+
+ /** 54% opacity */
+ private static final int ALPHA = 138;
+
+ /** Reusable components to avoid new allocations */
+ private static final Paint sPaint = new Paint();
+
+ private static final Rect sRect = new Rect();
+ private static final char[] sFirstChar = new char[1];
+ /** Letter tile */
+ private static TypedArray sColors;
+
+ private static int sDefaultColor;
+ private static int sTileFontColor;
+ private static float sLetterToTileRatio;
+ private static Bitmap sDefaultPersonAvatar;
+ private static Bitmap sDefaultBusinessAvatar;
+ private static Bitmap sDefaultVoicemailAvatar;
+ private static final String TAG = LetterTileDrawable.class.getSimpleName();
+ private final Paint mPaint;
+ private int mContactType = TYPE_DEFAULT;
+ private float mScale = 1.0f;
+ private float mOffset = 0.0f;
+ private boolean mIsCircle = false;
+
+ private int mColor;
+ private Character mLetter = null;
+
+ private boolean mAvatarWasVoicemailOrBusiness = false;
+ private String mDisplayName;
+
+ public LetterTileDrawable(final Resources res) {
+ if (sColors == null) {
+ sColors = res.obtainTypedArray(R.array.letter_tile_colors);
+ sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+ sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+ sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
+ sDefaultPersonAvatar =
+ BitmapFactory.decodeResource(
+ res, R.drawable.product_logo_avatar_anonymous_white_color_120);
+ sDefaultBusinessAvatar =
+ BitmapFactory.decodeResource(res, R.drawable.ic_business_white_120dp);
+ sDefaultVoicemailAvatar = BitmapFactory.decodeResource(res, R.drawable.ic_voicemail_avatar);
+ sPaint.setTypeface(
+ Typeface.create(res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
+ sPaint.setTextAlign(Align.CENTER);
+ sPaint.setAntiAlias(true);
+ }
+ mPaint = new Paint();
+ mPaint.setFilterBitmap(true);
+ mPaint.setDither(true);
+ mColor = sDefaultColor;
+ }
+
+ private static Bitmap getBitmapForContactType(int contactType) {
+ switch (contactType) {
+ case TYPE_BUSINESS:
+ return sDefaultBusinessAvatar;
+ case TYPE_VOICEMAIL:
+ return sDefaultVoicemailAvatar;
+ case TYPE_PERSON:
+ default:
+ return sDefaultPersonAvatar;
+ }
+ }
+
+ private static boolean isEnglishLetter(final char c) {
+ return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+ // Draw letter tile.
+ drawLetterTile(canvas);
+ }
+
+ /**
+ * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
+ */
+ private void drawBitmap(
+ final Bitmap bitmap, final int width, final int height, final Canvas canvas) {
+ // The bitmap should be drawn in the middle of the canvas without changing its width to
+ // height ratio.
+ final Rect destRect = copyBounds();
+
+ // Crop the destination bounds into a square, scaled and offset as appropriate
+ final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
+
+ destRect.set(
+ destRect.centerX() - halfLength,
+ (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
+ destRect.centerX() + halfLength,
+ (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
+
+ // Source rectangle remains the entire bounds of the source bitmap.
+ sRect.set(0, 0, width, height);
+
+ sPaint.setTextAlign(Align.CENTER);
+ sPaint.setAntiAlias(true);
+ sPaint.setAlpha(ALPHA);
+
+ canvas.drawBitmap(bitmap, sRect, destRect, sPaint);
+ }
+
+ private void drawLetterTile(final Canvas canvas) {
+ // Draw background color.
+ sPaint.setColor(mColor);
+
+ sPaint.setAlpha(mPaint.getAlpha());
+ final Rect bounds = getBounds();
+ final int minDimension = Math.min(bounds.width(), bounds.height());
+
+ if (mIsCircle) {
+ canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
+ } else {
+ canvas.drawRect(bounds, sPaint);
+ }
+
+ // Draw letter/digit only if the first character is an english letter or there's a override
+
+ if (mLetter != null) {
+ // Draw letter or digit.
+ sFirstChar[0] = mLetter;
+
+ // Scale text by canvas bounds and user selected scaling factor
+ sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
+ sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+ sPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
+ sPaint.setColor(sTileFontColor);
+ sPaint.setAlpha(ALPHA);
+
+ // Draw the letter in the canvas, vertically shifted up or down by the user-defined
+ // offset
+ canvas.drawText(
+ sFirstChar,
+ 0,
+ 1,
+ bounds.centerX(),
+ bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(),
+ sPaint);
+ } else {
+ // Draw the default image if there is no letter/digit to be drawn
+ final Bitmap bitmap = getBitmapForContactType(mContactType);
+ drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), canvas);
+ }
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public LetterTileDrawable setColor(int color) {
+ mColor = color;
+ return this;
+ }
+
+ /** Returns a deterministic color based on the provided contact identifier string. */
+ private int pickColor(final String identifier) {
+ if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
+ return sDefaultColor;
+ }
+ // String.hashCode() implementation is not supposed to change across java versions, so
+ // this should guarantee the same email address always maps to the same color.
+ // The email should already have been normalized by the ContactRequest.
+ final int color = Math.abs(identifier.hashCode()) % sColors.length();
+ return sColors.getColor(color, sDefaultColor);
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return android.graphics.PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ if (mIsCircle) {
+ outline.setOval(getBounds());
+ } else {
+ outline.setRect(getBounds());
+ }
+
+ outline.setAlpha(1);
+ }
+
+ /**
+ * Scale the drawn letter tile to a ratio of its default size
+ *
+ * @param scale The ratio the letter tile should be scaled to as a percentage of its default size,
+ * from a scale of 0 to 2.0f. The default is 1.0f.
+ */
+ public LetterTileDrawable setScale(float scale) {
+ mScale = scale;
+ return this;
+ }
+
+ /**
+ * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
+ *
+ * @param offset The provided offset must be within the range of -0.5f to 0.5f. If set to -0.5f,
+ * the letter will be shifted upwards by 0.5 times the height of the canvas it is being drawn
+ * on, which means it will be drawn with the center of the letter starting at the top edge of
+ * the canvas. If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of
+ * the canvas it is being drawn on, which means it will be drawn with the center of the letter
+ * starting at the bottom edge of the canvas. The default is 0.0f.
+ */
+ public LetterTileDrawable setOffset(float offset) {
+ Assert.checkArgument(offset >= -0.5f && offset <= 0.5f);
+ mOffset = offset;
+ return this;
+ }
+
+ public LetterTileDrawable setLetter(Character letter) {
+ mLetter = letter;
+ return this;
+ }
+
+ public Character getLetter() {
+ return this.mLetter;
+ }
+
+ private LetterTileDrawable setLetterAndColorFromContactDetails(
+ final String displayName, final String identifier) {
+ if (displayName != null && displayName.length() > 0 && isEnglishLetter(displayName.charAt(0))) {
+ mLetter = Character.toUpperCase(displayName.charAt(0));
+ } else {
+ mLetter = null;
+ }
+ mColor = pickColor(identifier);
+ return this;
+ }
+
+ public LetterTileDrawable setContactType(@ContactType int contactType) {
+ mContactType = contactType;
+ return this;
+ }
+
+ @ContactType
+ public int getContactType() {
+ return this.mContactType;
+ }
+
+ public LetterTileDrawable setIsCircular(boolean isCircle) {
+ mIsCircle = isCircle;
+ return this;
+ }
+
+ /**
+ * Creates a canonical letter tile for use across dialer fragments.
+ *
+ * @param displayName The display name to produce the letter in the tile. Null values or numbers
+ * yield no letter.
+ * @param identifierForTileColor The string used to produce the tile color.
+ * @param shape The shape of the tile.
+ * @param contactType The type of contact, e.g. TYPE_VOICEMAIL.
+ * @return this
+ */
+ public LetterTileDrawable setCanonicalDialerLetterTileDetails(
+ @Nullable final String displayName,
+ @Nullable final String identifierForTileColor,
+ @Shape final int shape,
+ final int contactType) {
+ setContactType(contactType);
+ /**
+ * During hangup, we lose the call state for special types of contacts, like voicemail. To help
+ * callers avoid extraneous LetterTileDrawable allocations, we keep track of the special case
+ * until we encounter a new display name.
+ */
+ if (contactType == TYPE_VOICEMAIL || contactType == TYPE_BUSINESS) {
+ this.mAvatarWasVoicemailOrBusiness = true;
+ } else if (displayName != null && !displayName.equals(mDisplayName)) {
+ this.mAvatarWasVoicemailOrBusiness = false;
+ }
+ this.mDisplayName = displayName;
+ if (shape == SHAPE_CIRCLE) {
+ this.setIsCircular(true);
+ } else {
+ this.setIsCircular(false);
+ }
+
+ /**
+ * To preserve style, we don't use contactType to set the tile icon. In the future, when all
+ * callers surface this detail, we can use this to better style the tile icon.
+ */
+ if (mAvatarWasVoicemailOrBusiness) {
+ this.setLetterAndColorFromContactDetails(null, displayName);
+ return this;
+ } else {
+ if (identifierForTileColor != null) {
+ this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor);
+ return this;
+ } else {
+ this.setLetterAndColorFromContactDetails(displayName, displayName);
+ return this;
+ }
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java
new file mode 100644
index 000000000..601abf528
--- /dev/null
+++ b/java/com/android/contacts/common/list/AutoScrollListView.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+/**
+ * A ListView that can be asked to scroll (smoothly or otherwise) to a specific position. This class
+ * takes advantage of similar functionality that exists in {@link ListView} and enhances it.
+ */
+public class AutoScrollListView extends ListView {
+
+ /** Position the element at about 1/3 of the list height */
+ private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f;
+
+ private int mRequestedScrollPosition = -1;
+ private boolean mSmoothScrollRequested;
+
+ public AutoScrollListView(Context context) {
+ super(context);
+ }
+
+ public AutoScrollListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Brings the specified position to view by optionally performing a jump-scroll maneuver: first it
+ * jumps to some position near the one requested and then does a smooth scroll to the requested
+ * position. This creates an impression of full smooth scrolling without actually traversing the
+ * entire list. If smooth scrolling is not requested, instantly positions the requested item at a
+ * preferred offset.
+ */
+ public void requestPositionToScreen(int position, boolean smoothScroll) {
+ mRequestedScrollPosition = position;
+ mSmoothScrollRequested = smoothScroll;
+ requestLayout();
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+ if (mRequestedScrollPosition == -1) {
+ return;
+ }
+
+ final int position = mRequestedScrollPosition;
+ mRequestedScrollPosition = -1;
+
+ int firstPosition = getFirstVisiblePosition() + 1;
+ int lastPosition = getLastVisiblePosition();
+ if (position >= firstPosition && position <= lastPosition) {
+ return; // Already on screen
+ }
+
+ final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP);
+ if (!mSmoothScrollRequested) {
+ setSelectionFromTop(position, offset);
+
+ // Since we have changed the scrolling position, we need to redo child layout
+ // Calling "requestLayout" in the middle of a layout pass has no effect,
+ // so we call layoutChildren explicitly
+ super.layoutChildren();
+
+ } else {
+ // We will first position the list a couple of screens before or after
+ // the new selection and then scroll smoothly to it.
+ int twoScreens = (lastPosition - firstPosition) * 2;
+ int preliminaryPosition;
+ if (position < firstPosition) {
+ preliminaryPosition = position + twoScreens;
+ if (preliminaryPosition >= getCount()) {
+ preliminaryPosition = getCount() - 1;
+ }
+ if (preliminaryPosition < firstPosition) {
+ setSelection(preliminaryPosition);
+ super.layoutChildren();
+ }
+ } else {
+ preliminaryPosition = position - twoScreens;
+ if (preliminaryPosition < 0) {
+ preliminaryPosition = 0;
+ }
+ if (preliminaryPosition > lastPosition) {
+ setSelection(preliminaryPosition);
+ super.layoutChildren();
+ }
+ }
+
+ smoothScrollToPositionFromTop(position, offset);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ // Workaround for b/31160338 and b/32778636.
+ if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N
+ || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
+ layoutChildren();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactEntry.java b/java/com/android/contacts/common/list/ContactEntry.java
new file mode 100644
index 000000000..e33165e45
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactEntry.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.net.Uri;
+import android.provider.ContactsContract.PinnedPositions;
+import android.text.TextUtils;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/** Class to hold contact information */
+public class ContactEntry {
+
+ public static final ContactEntry BLANK_ENTRY = new ContactEntry();
+ private static final int UNSET_DISPLAY_ORDER_PREFERENCE = -1;
+ /** Primary name for a Contact */
+ public String namePrimary;
+ /** Alternative name for a Contact, e.g. last name first */
+ public String nameAlternative;
+ /**
+ * The user's preference on name display order, last name first or first time first. {@see
+ * ContactsPreferences}
+ */
+ public int nameDisplayOrder = UNSET_DISPLAY_ORDER_PREFERENCE;
+
+ public String phoneLabel;
+ public String phoneNumber;
+ public Uri photoUri;
+ public Uri lookupUri;
+ public String lookupKey;
+ public long id;
+ public int pinned = PinnedPositions.UNPINNED;
+ public boolean isFavorite = false;
+ public boolean isDefaultNumber = false;
+
+ public String getPreferredDisplayName() {
+ if (nameDisplayOrder == UNSET_DISPLAY_ORDER_PREFERENCE
+ || nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+ || TextUtils.isEmpty(nameAlternative)) {
+ return namePrimary;
+ }
+ return nameAlternative;
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactEntryListAdapter.java b/java/com/android/contacts/common/list/ContactEntryListAdapter.java
new file mode 100644
index 000000000..18bbae382
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -0,0 +1,742 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.QuickContactBadge;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.util.SearchUtil;
+import com.android.dialer.compat.CompatUtils;
+import java.util.HashSet;
+
+/**
+ * Common base class for various contact-related lists, e.g. contact list, phone number list etc.
+ */
+public abstract class ContactEntryListAdapter extends IndexerListAdapter {
+
+ /**
+ * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be included in the
+ * search.
+ */
+ public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
+
+ private static final String TAG = "ContactEntryListAdapter";
+ private int mDisplayOrder;
+ private int mSortOrder;
+
+ private boolean mDisplayPhotos;
+ private boolean mCircularPhotos = true;
+ private boolean mQuickContactEnabled;
+ private boolean mAdjustSelectionBoundsEnabled;
+
+ /** The root view of the fragment that this adapter is associated with. */
+ private View mFragmentRootView;
+
+ private ContactPhotoManager mPhotoLoader;
+
+ private String mQueryString;
+ private String mUpperCaseQueryString;
+ private boolean mSearchMode;
+ private int mDirectorySearchMode;
+ private int mDirectoryResultLimit = Integer.MAX_VALUE;
+
+ private boolean mEmptyListEnabled = true;
+
+ private boolean mSelectionVisible;
+
+ private ContactListFilter mFilter;
+ private boolean mDarkTheme = false;
+
+ /** Resource used to provide header-text for default filter. */
+ private CharSequence mDefaultFilterHeaderText;
+
+ public ContactEntryListAdapter(Context context) {
+ super(context);
+ setDefaultFilterHeaderText(R.string.local_search_label);
+ addPartitions();
+ }
+
+ /**
+ * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of image
+ * loading requests that get cancelled on cursor changes.
+ */
+ protected void setFragmentRootView(View fragmentRootView) {
+ mFragmentRootView = fragmentRootView;
+ }
+
+ protected void setDefaultFilterHeaderText(int resourceId) {
+ mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+ view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ final ContactListItemView view = (ContactListItemView) itemView;
+ view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+ bindWorkProfileIcon(view, partition);
+ }
+
+ @Override
+ protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
+ return new ContactListPinnedHeaderView(context, null, parent);
+ }
+
+ @Override
+ protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
+ ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
+ }
+
+ protected void addPartitions() {
+ addPartition(createDefaultDirectoryPartition());
+ }
+
+ protected DirectoryPartition createDefaultDirectoryPartition() {
+ DirectoryPartition partition = new DirectoryPartition(true, true);
+ partition.setDirectoryId(Directory.DEFAULT);
+ partition.setDirectoryType(getContext().getString(R.string.contactsList));
+ partition.setPriorityDirectory(true);
+ partition.setPhotoSupported(true);
+ partition.setLabel(mDefaultFilterHeaderText.toString());
+ return partition;
+ }
+
+ /**
+ * Remove all directories after the default directory. This is typically used when contacts list
+ * screens are asked to exit the search mode and thus need to remove all remote directory results
+ * for the search.
+ *
+ * <p>This code assumes that the default directory and directories before that should not be
+ * deleted (e.g. Join screen has "suggested contacts" directory before the default director, and
+ * we should not remove the directory).
+ */
+ public void removeDirectoriesAfterDefault() {
+ final int partitionCount = getPartitionCount();
+ for (int i = partitionCount - 1; i >= 0; i--) {
+ final Partition partition = getPartition(i);
+ if ((partition instanceof DirectoryPartition)
+ && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
+ break;
+ } else {
+ removePartition(i);
+ }
+ }
+ }
+
+ protected int getPartitionByDirectoryId(long id) {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ if (((DirectoryPartition) partition).getDirectoryId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ protected DirectoryPartition getDirectoryById(long id) {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ if (directoryPartition.getDirectoryId() == id) {
+ return directoryPartition;
+ }
+ }
+ }
+ return null;
+ }
+
+ public abstract void configureLoader(CursorLoader loader, long directoryId);
+
+ /** Marks all partitions as "loading" */
+ public void onDataReload() {
+ boolean notify = false;
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ if (!directoryPartition.isLoading()) {
+ notify = true;
+ }
+ directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+ }
+ }
+ if (notify) {
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void clearPartitions() {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+ }
+ }
+ super.clearPartitions();
+ }
+
+ public boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ public void setSearchMode(boolean flag) {
+ mSearchMode = flag;
+ }
+
+ public String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString) {
+ mQueryString = queryString;
+ if (TextUtils.isEmpty(queryString)) {
+ mUpperCaseQueryString = null;
+ } else {
+ mUpperCaseQueryString = SearchUtil.cleanStartAndEndOfSearchQuery(queryString.toUpperCase());
+ }
+ }
+
+ public String getUpperCaseQueryString() {
+ return mUpperCaseQueryString;
+ }
+
+ public int getDirectorySearchMode() {
+ return mDirectorySearchMode;
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ public int getDirectoryResultLimit() {
+ return mDirectoryResultLimit;
+ }
+
+ public void setDirectoryResultLimit(int limit) {
+ this.mDirectoryResultLimit = limit;
+ }
+
+ public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
+ final int limit = directoryPartition.getResultLimit();
+ return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
+ }
+
+ public int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ public void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ }
+
+ protected ContactPhotoManager getPhotoLoader() {
+ return mPhotoLoader;
+ }
+
+ public void setPhotoLoader(ContactPhotoManager photoLoader) {
+ mPhotoLoader = photoLoader;
+ }
+
+ public boolean getDisplayPhotos() {
+ return mDisplayPhotos;
+ }
+
+ public void setDisplayPhotos(boolean displayPhotos) {
+ mDisplayPhotos = displayPhotos;
+ }
+
+ public boolean getCircularPhotos() {
+ return mCircularPhotos;
+ }
+
+ public boolean isSelectionVisible() {
+ return mSelectionVisible;
+ }
+
+ public void setSelectionVisible(boolean flag) {
+ this.mSelectionVisible = flag;
+ }
+
+ public boolean isQuickContactEnabled() {
+ return mQuickContactEnabled;
+ }
+
+ public void setQuickContactEnabled(boolean quickContactEnabled) {
+ mQuickContactEnabled = quickContactEnabled;
+ }
+
+ public boolean isAdjustSelectionBoundsEnabled() {
+ return mAdjustSelectionBoundsEnabled;
+ }
+
+ public void setAdjustSelectionBoundsEnabled(boolean enabled) {
+ mAdjustSelectionBoundsEnabled = enabled;
+ }
+
+ public void setProfileExists(boolean exists) {
+ // Stick the "ME" header for the profile
+ if (exists) {
+ setSectionHeader(R.string.user_profile_contacts_list_header, /* # of ME */ 1);
+ }
+ }
+
+ private void setSectionHeader(int resId, int numberOfItems) {
+ SectionIndexer indexer = getIndexer();
+ if (indexer != null) {
+ ((ContactsSectionIndexer) indexer)
+ .setProfileAndFavoritesHeader(getContext().getString(resId), numberOfItems);
+ }
+ }
+
+ public void setDarkTheme(boolean value) {
+ mDarkTheme = value;
+ }
+
+ /** Updates partitions according to the directory meta-data contained in the supplied cursor. */
+ public void changeDirectories(Cursor cursor) {
+ if (cursor.getCount() == 0) {
+ // Directory table must have at least local directory, without which this adapter will
+ // enter very weird state.
+ Log.e(
+ TAG,
+ "Directory search loader returned an empty cursor, which implies we have "
+ + "no directory entries.",
+ new RuntimeException());
+ return;
+ }
+ HashSet<Long> directoryIds = new HashSet<Long>();
+
+ int idColumnIndex = cursor.getColumnIndex(Directory._ID);
+ int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
+ int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
+ int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
+
+ // TODO preserve the order of partition to match those of the cursor
+ // Phase I: add new directories
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idColumnIndex);
+ directoryIds.add(id);
+ if (getPartitionByDirectoryId(id) == -1) {
+ DirectoryPartition partition = new DirectoryPartition(false, true);
+ partition.setDirectoryId(id);
+ if (DirectoryCompat.isRemoteDirectoryId(id)) {
+ if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
+ partition.setLabel(mContext.getString(R.string.directory_search_label_work));
+ } else {
+ partition.setLabel(mContext.getString(R.string.directory_search_label));
+ }
+ } else {
+ if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
+ partition.setLabel(mContext.getString(R.string.list_filter_phones_work));
+ } else {
+ partition.setLabel(mDefaultFilterHeaderText.toString());
+ }
+ }
+ partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
+ partition.setDisplayName(cursor.getString(displayNameColumnIndex));
+ int photoSupport = cursor.getInt(photoSupportColumnIndex);
+ partition.setPhotoSupported(
+ photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
+ || photoSupport == Directory.PHOTO_SUPPORT_FULL);
+ addPartition(partition);
+ }
+ }
+
+ // Phase II: remove deleted directories
+ int count = getPartitionCount();
+ for (int i = count; --i >= 0; ) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ long id = ((DirectoryPartition) partition).getDirectoryId();
+ if (!directoryIds.contains(id)) {
+ removePartition(i);
+ }
+ }
+ }
+
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void changeCursor(int partitionIndex, Cursor cursor) {
+ if (partitionIndex >= getPartitionCount()) {
+ // There is no partition for this data
+ return;
+ }
+
+ Partition partition = getPartition(partitionIndex);
+ if (partition instanceof DirectoryPartition) {
+ ((DirectoryPartition) partition).setStatus(DirectoryPartition.STATUS_LOADED);
+ }
+
+ if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
+ mPhotoLoader.refreshCache();
+ }
+
+ super.changeCursor(partitionIndex, cursor);
+
+ if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
+ updateIndexer(cursor);
+ }
+
+ // When the cursor changes, cancel any pending asynchronous photo loads.
+ mPhotoLoader.cancelPendingRequests(mFragmentRootView);
+ }
+
+ public void changeCursor(Cursor cursor) {
+ changeCursor(0, cursor);
+ }
+
+ /** Updates the indexer, which is used to produce section headers. */
+ private void updateIndexer(Cursor cursor) {
+ if (cursor == null || cursor.isClosed()) {
+ setIndexer(null);
+ return;
+ }
+
+ Bundle bundle = cursor.getExtras();
+ if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)
+ && bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
+ String[] sections = bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+ int[] counts = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+
+ if (getExtraStartingSection()) {
+ // Insert an additional unnamed section at the top of the list.
+ String[] allSections = new String[sections.length + 1];
+ int[] allCounts = new int[counts.length + 1];
+ for (int i = 0; i < sections.length; i++) {
+ allSections[i + 1] = sections[i];
+ allCounts[i + 1] = counts[i];
+ }
+ allCounts[0] = 1;
+ allSections[0] = "";
+ setIndexer(new ContactsSectionIndexer(allSections, allCounts));
+ } else {
+ setIndexer(new ContactsSectionIndexer(sections, counts));
+ }
+ } else {
+ setIndexer(null);
+ }
+ }
+
+ protected boolean getExtraStartingSection() {
+ return false;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ // We need a separate view type for each item type, plus another one for
+ // each type with header, plus one for "other".
+ return getItemViewTypeCount() * 2 + 1;
+ }
+
+ @Override
+ public int getItemViewType(int partitionIndex, int position) {
+ int type = super.getItemViewType(partitionIndex, position);
+ if (!isUserProfile(position)
+ && isSectionHeaderDisplayEnabled()
+ && partitionIndex == getIndexedPartition()) {
+ Placement placement = getItemPlacementInSection(position);
+ return placement.firstInSection ? type : getItemViewTypeCount() + type;
+ } else {
+ return type;
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ // TODO
+ // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+ // return true;
+ // }
+
+ if (!mEmptyListEnabled) {
+ return false;
+ } else if (isSearchMode()) {
+ return TextUtils.isEmpty(getQueryString());
+ } else {
+ return super.isEmpty();
+ }
+ }
+
+ public boolean isLoading() {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition && ((DirectoryPartition) partition).isLoading()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Changes visibility parameters for the default directory partition. */
+ public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
+ int defaultPartitionIndex = -1;
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition
+ && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
+ defaultPartitionIndex = i;
+ break;
+ }
+ }
+ if (defaultPartitionIndex != -1) {
+ setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
+ setHasHeader(defaultPartitionIndex, hasHeader);
+ }
+ }
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View view = inflater.inflate(R.layout.directory_header, parent, false);
+ if (!getPinnedPartitionHeadersEnabled()) {
+ // If the headers are unpinned, there is no need for their background
+ // color to be non-transparent. Setting this transparent reduces maintenance for
+ // non-pinned headers. We don't need to bother synchronizing the activity's
+ // background color with the header background color.
+ view.setBackground(null);
+ }
+ return view;
+ }
+
+ protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) {
+ final Partition partition = getPartition(partitionId);
+ if (partition instanceof DirectoryPartition) {
+ final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ final long directoryId = directoryPartition.getDirectoryId();
+ final long userType = ContactsUtils.determineUserType(directoryId, null);
+ view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK);
+ }
+ }
+
+ @Override
+ protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
+ Partition partition = getPartition(partitionIndex);
+ if (!(partition instanceof DirectoryPartition)) {
+ return;
+ }
+
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ long directoryId = directoryPartition.getDirectoryId();
+ TextView labelTextView = (TextView) view.findViewById(R.id.label);
+ TextView displayNameTextView = (TextView) view.findViewById(R.id.display_name);
+ labelTextView.setText(directoryPartition.getLabel());
+ if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ displayNameTextView.setText(null);
+ } else {
+ String directoryName = directoryPartition.getDisplayName();
+ String displayName =
+ !TextUtils.isEmpty(directoryName) ? directoryName : directoryPartition.getDirectoryType();
+ displayNameTextView.setText(displayName);
+ }
+
+ final Resources res = getContext().getResources();
+ final int headerPaddingTop =
+ partitionIndex == 1 && getPartition(0).isEmpty()
+ ? 0
+ : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
+ // There should be no extra padding at the top of the first directory header
+ view.setPaddingRelative(
+ view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), view.getPaddingBottom());
+ }
+
+ /** Checks whether the contact entry at the given position represents the user's profile. */
+ protected boolean isUserProfile(int position) {
+ // The profile only ever appears in the first position if it is present. So if the position
+ // is anything beyond 0, it can't be the profile.
+ boolean isUserProfile = false;
+ if (position == 0) {
+ int partition = getPartitionForPosition(position);
+ if (partition >= 0) {
+ // Save the old cursor position - the call to getItem() may modify the cursor
+ // position.
+ int offset = getCursor(partition).getPosition();
+ Cursor cursor = (Cursor) getItem(position);
+ if (cursor != null) {
+ int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
+ if (profileColumnIndex != -1) {
+ isUserProfile = cursor.getInt(profileColumnIndex) == 1;
+ }
+ // Restore the old cursor position.
+ cursor.moveToPosition(offset);
+ }
+ }
+ }
+ return isUserProfile;
+ }
+
+ public boolean isPhotoSupported(int partitionIndex) {
+ Partition partition = getPartition(partitionIndex);
+ if (partition instanceof DirectoryPartition) {
+ return ((DirectoryPartition) partition).isPhotoSupported();
+ }
+ return true;
+ }
+
+ /** Returns the currently selected filter. */
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ mFilter = filter;
+ }
+
+ // TODO: move sharable logic (bindXX() methods) to here with extra arguments
+
+ /**
+ * Loads the photo for the quick contact view and assigns the contact uri.
+ *
+ * @param photoIdColumn Index of the photo id column
+ * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
+ * @param contactIdColumn Index of the contact id column
+ * @param lookUpKeyColumn Index of the lookup key column
+ * @param displayNameColumn Index of the display name column
+ */
+ protected void bindQuickContact(
+ final ContactListItemView view,
+ int partitionIndex,
+ Cursor cursor,
+ int photoIdColumn,
+ int photoUriColumn,
+ int contactIdColumn,
+ int lookUpKeyColumn,
+ int displayNameColumn) {
+ long photoId = 0;
+ if (!cursor.isNull(photoIdColumn)) {
+ photoId = cursor.getLong(photoIdColumn);
+ }
+
+ QuickContactBadge quickContact = view.getQuickContact();
+ quickContact.assignContactUri(
+ getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume
+ // that only Dialer will use this QuickContact badge. This means prioritizing the phone
+ // mimetype here is reasonable.
+ quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ }
+
+ if (photoId != 0 || photoUriColumn == -1) {
+ getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, null);
+ } else {
+ final String photoUriString = cursor.getString(photoUriColumn);
+ final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+ DefaultImageRequest request = null;
+ if (photoUri == null) {
+ request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, lookUpKeyColumn);
+ }
+ getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, request);
+ }
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ // Whenever bindViewId() is called, the values passed into setId() are stable or
+ // stable-ish. For example, when one contact is modified we don't expect a second
+ // contact's Contact._ID values to change.
+ return true;
+ }
+
+ protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
+ // Set a semi-stable id, so that talkback won't get confused when the list gets
+ // refreshed. There is little harm in inserting the same ID twice.
+ long contactId = cursor.getLong(idColumn);
+ view.setId((int) (contactId % Integer.MAX_VALUE));
+ }
+
+ protected Uri getContactUri(
+ int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) {
+ long contactId = cursor.getLong(contactIdColumn);
+ String lookupKey = cursor.getString(lookUpKeyColumn);
+ long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId();
+ Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+ if (uri != null && directoryId != Directory.DEFAULT) {
+ uri =
+ uri.buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
+ .build();
+ }
+ return uri;
+ }
+
+ /**
+ * Retrieves the lookup key and display name from a cursor, and returns a {@link
+ * DefaultImageRequest} containing these contact details
+ *
+ * @param cursor Contacts cursor positioned at the current row to retrieve contact details for
+ * @param displayNameColumn Column index of the display name
+ * @param lookupKeyColumn Column index of the lookup key
+ * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
+ * display name and lookup key of the contact.
+ */
+ public DefaultImageRequest getDefaultImageRequestFromCursor(
+ Cursor cursor, int displayNameColumn, int lookupKeyColumn) {
+ final String displayName = cursor.getString(displayNameColumn);
+ final String lookupKey = cursor.getString(lookupKeyColumn);
+ return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java
new file mode 100644
index 000000000..a8d9b55ba
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -0,0 +1,862 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListView;
+import com.android.common.widget.CompositeCursorAdapter.Partition;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactListViewUtils;
+import java.util.Locale;
+
+/** Common base class for various contact-related list fragments. */
+public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter> extends Fragment
+ implements OnItemClickListener,
+ OnScrollListener,
+ OnFocusChangeListener,
+ OnTouchListener,
+ OnItemLongClickListener,
+ LoaderCallbacks<Cursor> {
+ private static final String TAG = "ContactEntryListFragment";
+ private static final String KEY_LIST_STATE = "liststate";
+ private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
+ private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
+ private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
+ private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled";
+ private static final String KEY_INCLUDE_PROFILE = "includeProfile";
+ private static final String KEY_SEARCH_MODE = "searchMode";
+ private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
+ private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition";
+ private static final String KEY_QUERY_STRING = "queryString";
+ private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode";
+ private static final String KEY_SELECTION_VISIBLE = "selectionVisible";
+ private static final String KEY_DARK_THEME = "darkTheme";
+ private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility";
+ private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit";
+
+ private static final String DIRECTORY_ID_ARG_KEY = "directoryId";
+
+ private static final int DIRECTORY_LOADER_ID = -1;
+
+ private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
+ private static final int DIRECTORY_SEARCH_MESSAGE = 1;
+
+ private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
+ private static final int STATUS_NOT_LOADED = 0;
+ private static final int STATUS_LOADING = 1;
+ private static final int STATUS_LOADED = 2;
+ protected boolean mUserProfileExists;
+ private boolean mSectionHeaderDisplayEnabled;
+ private boolean mPhotoLoaderEnabled;
+ private boolean mQuickContactEnabled = true;
+ private boolean mAdjustSelectionBoundsEnabled = true;
+ private boolean mIncludeProfile;
+ private boolean mSearchMode;
+ private boolean mVisibleScrollbarEnabled;
+ private boolean mShowEmptyListForEmptyQuery;
+ private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition();
+ private String mQueryString;
+ private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
+ private boolean mSelectionVisible;
+ private boolean mLegacyCompatibility;
+ private boolean mEnabled = true;
+ private T mAdapter;
+ private View mView;
+ private ListView mListView;
+ /** Used to save the scrolling state of the list when the fragment is not recreated. */
+ private int mListViewTopIndex;
+
+ private int mListViewTopOffset;
+ /** Used for keeping track of the scroll state of the list. */
+ private Parcelable mListState;
+
+ private int mDisplayOrder;
+ private int mSortOrder;
+ private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT;
+ private ContactPhotoManager mPhotoManager;
+ private ContactsPreferences mContactsPrefs;
+ private boolean mForceLoad;
+ private boolean mDarkTheme;
+ private int mDirectoryListStatus = STATUS_NOT_LOADED;
+
+ /**
+ * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by
+ * a change notification (true)
+ */
+ private boolean mLoadPriorityDirectoriesOnly;
+
+ private Context mContext;
+
+ private LoaderManager mLoaderManager;
+
+ private Handler mDelayedDirectorySearchHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+ loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+ }
+ }
+ };
+ private ContactsPreferences.ChangeListener mPreferencesChangeListener =
+ new ContactsPreferences.ChangeListener() {
+ @Override
+ public void onChange() {
+ loadPreferences();
+ reloadData();
+ }
+ };
+
+ protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
+
+ protected abstract T createListAdapter();
+
+ /**
+ * @param position Please note that the position is already adjusted for header views, so "0"
+ * means the first list item below header views.
+ */
+ protected abstract void onItemClick(int position, long id);
+
+ /**
+ * @param position Please note that the position is already adjusted for header views, so "0"
+ * means the first list item below header views.
+ */
+ protected boolean onItemLongClick(int position, long id) {
+ return false;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ setContext(activity);
+ setLoaderManager(super.getLoaderManager());
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ /** Sets a context for the fragment in the unit test environment. */
+ public void setContext(Context context) {
+ mContext = context;
+ configurePhotoLoader();
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (mEnabled != enabled) {
+ mEnabled = enabled;
+ if (mAdapter != null) {
+ if (mEnabled) {
+ reloadData();
+ } else {
+ mAdapter.clearPartitions();
+ }
+ }
+ }
+ }
+
+ @Override
+ public LoaderManager getLoaderManager() {
+ return mLoaderManager;
+ }
+
+ /** Overrides a loader manager for use in unit tests. */
+ public void setLoaderManager(LoaderManager loaderManager) {
+ mLoaderManager = loaderManager;
+ }
+
+ public T getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public View getView() {
+ return mView;
+ }
+
+ public ListView getListView() {
+ return mListView;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled);
+ outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
+ outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
+ outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
+ outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile);
+ outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
+ outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
+ outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition);
+ outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode);
+ outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible);
+ outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility);
+ outState.putString(KEY_QUERY_STRING, mQueryString);
+ outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit);
+ outState.putBoolean(KEY_DARK_THEME, mDarkTheme);
+
+ if (mListView != null) {
+ outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ restoreSavedState(savedState);
+ mAdapter = createListAdapter();
+ mContactsPrefs = new ContactsPreferences(mContext);
+ }
+
+ public void restoreSavedState(Bundle savedState) {
+ if (savedState == null) {
+ return;
+ }
+
+ mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED);
+ mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
+ mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
+ mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
+ mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE);
+ mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
+ mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
+ mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION);
+ mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE);
+ mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE);
+ mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY);
+ mQueryString = savedState.getString(KEY_QUERY_STRING);
+ mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT);
+ mDarkTheme = savedState.getBoolean(KEY_DARK_THEME);
+
+ // Retrieve list state. This will be applied in onLoadFinished
+ mListState = savedState.getParcelable(KEY_LIST_STATE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
+
+ mForceLoad = loadPreferences();
+
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ mLoadPriorityDirectoriesOnly = true;
+
+ startLoading();
+ }
+
+ protected void startLoading() {
+ if (mAdapter == null) {
+ // The method was called before the fragment was started
+ return;
+ }
+
+ configureAdapter();
+ int partitionCount = mAdapter.getPartitionCount();
+ for (int i = 0; i < partitionCount; i++) {
+ Partition partition = mAdapter.getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
+ if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {
+ startLoadingDirectoryPartition(i);
+ }
+ }
+ } else {
+ getLoaderManager().initLoader(i, null, this);
+ }
+ }
+
+ // Next time this method is called, we should start loading non-priority directories
+ mLoadPriorityDirectoriesOnly = false;
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (id == DIRECTORY_LOADER_ID) {
+ DirectoryListLoader loader = new DirectoryListLoader(mContext);
+ loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
+ loader.setLocalInvisibleDirectoryEnabled(
+ ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
+ return loader;
+ } else {
+ CursorLoader loader = createCursorLoader(mContext);
+ long directoryId =
+ args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
+ ? args.getLong(DIRECTORY_ID_ARG_KEY)
+ : Directory.DEFAULT;
+ mAdapter.configureLoader(loader, directoryId);
+ return loader;
+ }
+ }
+
+ public CursorLoader createCursorLoader(Context context) {
+ return new CursorLoader(context, null, null, null, null, null) {
+ @Override
+ protected Cursor onLoadInBackground() {
+ try {
+ return super.onLoadInBackground();
+ } catch (RuntimeException e) {
+ // We don't even know what the projection should be, so no point trying to
+ // return an empty MatrixCursor with the correct projection here.
+ Log.w(TAG, "RuntimeException while trying to query ContactsProvider.");
+ return null;
+ }
+ }
+ };
+ }
+
+ private void startLoadingDirectoryPartition(int partitionIndex) {
+ DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex);
+ partition.setStatus(DirectoryPartition.STATUS_LOADING);
+ long directoryId = partition.getDirectoryId();
+ if (mForceLoad) {
+ if (directoryId == Directory.DEFAULT) {
+ loadDirectoryPartition(partitionIndex, partition);
+ } else {
+ loadDirectoryPartitionDelayed(partitionIndex, partition);
+ }
+ } else {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
+ getLoaderManager().initLoader(partitionIndex, args, this);
+ }
+ }
+
+ /**
+ * Queues up a delayed request to search the specified directory. Since directory search will
+ * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing
+ * before sending a directory request.
+ */
+ private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
+ mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
+ Message msg =
+ mDelayedDirectorySearchHandler.obtainMessage(
+ DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
+ mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
+ }
+
+ /** Loads the directory partition. */
+ protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
+ getLoaderManager().restartLoader(partitionIndex, args, this);
+ }
+
+ /** Cancels all queued directory loading requests. */
+ private void removePendingDirectorySearchRequests() {
+ mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (!mEnabled) {
+ return;
+ }
+
+ int loaderId = loader.getId();
+ if (loaderId == DIRECTORY_LOADER_ID) {
+ mDirectoryListStatus = STATUS_LOADED;
+ mAdapter.changeDirectories(data);
+ startLoading();
+ } else {
+ onPartitionLoaded(loaderId, data);
+ if (isSearchMode()) {
+ int directorySearchMode = getDirectorySearchMode();
+ if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
+ if (mDirectoryListStatus == STATUS_NOT_LOADED) {
+ mDirectoryListStatus = STATUS_LOADING;
+ getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
+ } else {
+ startLoading();
+ }
+ }
+ } else {
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {}
+
+ protected void onPartitionLoaded(int partitionIndex, Cursor data) {
+ if (partitionIndex >= mAdapter.getPartitionCount()) {
+ // When we get unsolicited data, ignore it. This could happen
+ // when we are switching from search mode to the default mode.
+ return;
+ }
+
+ mAdapter.changeCursor(partitionIndex, data);
+ setProfileHeader();
+
+ if (!isLoading()) {
+ completeRestoreInstanceState();
+ }
+ }
+
+ public boolean isLoading() {
+ if (mAdapter != null && mAdapter.isLoading()) {
+ return true;
+ }
+
+ return isLoadingDirectoryList();
+
+ }
+
+ public boolean isLoadingDirectoryList() {
+ return isSearchMode()
+ && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
+ && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mContactsPrefs.unregisterChangeListener();
+ mAdapter.clearPartitions();
+ }
+
+ protected void reloadData() {
+ removePendingDirectorySearchRequests();
+ mAdapter.onDataReload();
+ mLoadPriorityDirectoriesOnly = true;
+ mForceLoad = true;
+ startLoading();
+ }
+
+ /**
+ * Shows a view at the top of the list with a pseudo local profile prompting the user to add a
+ * local profile. Default implementation does nothing.
+ */
+ protected void setProfileHeader() {
+ mUserProfileExists = false;
+ }
+
+ /** Provides logic that dismisses this fragment. The default implementation does nothing. */
+ protected void finish() {}
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ if (mSectionHeaderDisplayEnabled != flag) {
+ mSectionHeaderDisplayEnabled = flag;
+ if (mAdapter != null) {
+ mAdapter.setSectionHeaderDisplayEnabled(flag);
+ }
+ configureVerticalScrollbar();
+ }
+ }
+
+ public boolean isVisibleScrollbarEnabled() {
+ return mVisibleScrollbarEnabled;
+ }
+
+ public void setVisibleScrollbarEnabled(boolean flag) {
+ if (mVisibleScrollbarEnabled != flag) {
+ mVisibleScrollbarEnabled = flag;
+ configureVerticalScrollbar();
+ }
+ }
+
+ private void configureVerticalScrollbar() {
+ boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled();
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(hasScrollbar);
+ mListView.setFastScrollAlwaysVisible(hasScrollbar);
+ mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ }
+ }
+
+ public boolean isPhotoLoaderEnabled() {
+ return mPhotoLoaderEnabled;
+ }
+
+ public void setPhotoLoaderEnabled(boolean flag) {
+ mPhotoLoaderEnabled = flag;
+ configurePhotoLoader();
+ }
+
+ public void setQuickContactEnabled(boolean flag) {
+ this.mQuickContactEnabled = flag;
+ }
+
+ public void setAdjustSelectionBoundsEnabled(boolean flag) {
+ mAdjustSelectionBoundsEnabled = flag;
+ }
+
+ public final boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ /**
+ * Enter/exit search mode. This is method is tightly related to the current query, and should only
+ * be called by {@link #setQueryString}.
+ *
+ * <p>Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it.
+ */
+ protected void setSearchMode(boolean flag) {
+ if (mSearchMode != flag) {
+ mSearchMode = flag;
+ setSectionHeaderDisplayEnabled(!mSearchMode);
+
+ if (!flag) {
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+ }
+
+ if (mAdapter != null) {
+ mAdapter.setSearchMode(flag);
+
+ mAdapter.clearPartitions();
+ if (!flag) {
+ // If we are switching from search to regular display, remove all directory
+ // partitions after default one, assuming they are remote directories which
+ // should be cleaned up on exiting the search mode.
+ mAdapter.removeDirectoriesAfterDefault();
+ }
+ mAdapter.configureDefaultPartition(false, flag);
+ }
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(!flag);
+ }
+ }
+ }
+
+ public final String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString) {
+ if (!TextUtils.equals(mQueryString, queryString)) {
+ if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) {
+ if (TextUtils.isEmpty(mQueryString)) {
+ // Restore the adapter if the query used to be empty.
+ mListView.setAdapter(mAdapter);
+ } else if (TextUtils.isEmpty(queryString)) {
+ // Instantly clear the list view if the new query is empty.
+ mListView.setAdapter(null);
+ }
+ }
+
+ mQueryString = queryString;
+ setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery);
+
+ if (mAdapter != null) {
+ mAdapter.setQueryString(queryString);
+ reloadData();
+ }
+ }
+ }
+
+ public void setShowEmptyListForNullQuery(boolean show) {
+ mShowEmptyListForEmptyQuery = show;
+ }
+
+ public boolean getShowEmptyListForNullQuery() {
+ return mShowEmptyListForEmptyQuery;
+ }
+
+ public int getDirectoryLoaderId() {
+ return DIRECTORY_LOADER_ID;
+ }
+
+ public int getDirectorySearchMode() {
+ return mDirectorySearchMode;
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ protected int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ protected void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ if (mAdapter != null) {
+ mAdapter.setContactNameDisplayOrder(displayOrder);
+ }
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ if (mAdapter != null) {
+ mAdapter.setSortOrder(sortOrder);
+ }
+ }
+
+ public void setDirectoryResultLimit(int limit) {
+ mDirectoryResultLimit = limit;
+ }
+
+ protected boolean loadPreferences() {
+ boolean changed = false;
+ if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
+ setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+ changed = true;
+ }
+
+ if (getSortOrder() != mContactsPrefs.getSortOrder()) {
+ setSortOrder(mContactsPrefs.getSortOrder());
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ onCreateView(inflater, container);
+
+ boolean searchMode = isSearchMode();
+ mAdapter.setSearchMode(searchMode);
+ mAdapter.configureDefaultPartition(false, searchMode);
+ mAdapter.setPhotoLoader(mPhotoManager);
+ mListView.setAdapter(mAdapter);
+
+ if (!isSearchMode()) {
+ mListView.setFocusableInTouchMode(true);
+ mListView.requestFocus();
+ }
+
+ return mView;
+ }
+
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ mView = inflateView(inflater, container);
+
+ mListView = (ListView) mView.findViewById(android.R.id.list);
+ if (mListView == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " + "'android.R.id.list'");
+ }
+
+ View emptyView = mView.findViewById(android.R.id.empty);
+ if (emptyView != null) {
+ mListView.setEmptyView(emptyView);
+ }
+
+ mListView.setOnItemClickListener(this);
+ mListView.setOnItemLongClickListener(this);
+ mListView.setOnFocusChangeListener(this);
+ mListView.setOnTouchListener(this);
+ mListView.setFastScrollEnabled(!isSearchMode());
+
+ // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
+ // them when an A-Z headers is visible.
+ mListView.setDividerHeight(0);
+
+ // We manually save/restore the listview state
+ mListView.setSaveEnabled(false);
+
+ configureVerticalScrollbar();
+ configurePhotoLoader();
+
+ getAdapter().setFragmentRootView(getView());
+
+ ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView);
+ }
+
+ @Override
+ public void onHiddenChanged(boolean hidden) {
+ super.onHiddenChanged(hidden);
+ if (getActivity() != null && getView() != null && !hidden) {
+ // If the padding was last applied when in a hidden state, it may have been applied
+ // incorrectly. Therefore we need to reapply it.
+ ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView());
+ }
+ }
+
+ protected void configurePhotoLoader() {
+ if (isPhotoLoaderEnabled() && mContext != null) {
+ if (mPhotoManager == null) {
+ mPhotoManager = ContactPhotoManager.getInstance(mContext);
+ }
+ if (mListView != null) {
+ mListView.setOnScrollListener(this);
+ }
+ if (mAdapter != null) {
+ mAdapter.setPhotoLoader(mPhotoManager);
+ }
+ }
+ }
+
+ protected void configureAdapter() {
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.setQuickContactEnabled(mQuickContactEnabled);
+ mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
+ mAdapter.setQueryString(mQueryString);
+ mAdapter.setDirectorySearchMode(mDirectorySearchMode);
+ mAdapter.setPinnedPartitionHeadersEnabled(false);
+ mAdapter.setContactNameDisplayOrder(mDisplayOrder);
+ mAdapter.setSortOrder(mSortOrder);
+ mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
+ mAdapter.setSelectionVisible(mSelectionVisible);
+ mAdapter.setDirectoryResultLimit(mDirectoryResultLimit);
+ mAdapter.setDarkTheme(mDarkTheme);
+ }
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+ mPhotoManager.pause();
+ } else if (isPhotoLoaderEnabled()) {
+ mPhotoManager.resume();
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ hideSoftKeyboard();
+
+ int adjPosition = position - mListView.getHeaderViewsCount();
+ if (adjPosition >= 0) {
+ onItemClick(adjPosition, id);
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ int adjPosition = position - mListView.getHeaderViewsCount();
+
+ if (adjPosition >= 0) {
+ return onItemLongClick(adjPosition, id);
+ }
+ return false;
+ }
+
+ private void hideSoftKeyboard() {
+ // Hide soft keyboard, if visible
+ InputMethodManager inputMethodManager =
+ (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+ }
+
+ /** Dismisses the soft keyboard when the list takes focus. */
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (view == mListView && hasFocus) {
+ hideSoftKeyboard();
+ }
+ }
+
+ /** Dismisses the soft keyboard when the list is touched. */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (view == mListView) {
+ hideSoftKeyboard();
+ }
+ return false;
+ }
+
+ @Override
+ public void onPause() {
+ // Save the scrolling state of the list view
+ mListViewTopIndex = mListView.getFirstVisiblePosition();
+ View v = mListView.getChildAt(0);
+ mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
+
+ super.onPause();
+ removePendingDirectorySearchRequests();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // Restore the selection of the list view. See b/19982820.
+ // This has to be done manually because if the list view has its emptyView set,
+ // the scrolling state will be reset when clearPartitions() is called on the adapter.
+ mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset);
+ }
+
+ /** Restore the list state after the adapter is populated. */
+ protected void completeRestoreInstanceState() {
+ if (mListState != null) {
+ mListView.onRestoreInstanceState(mListState);
+ mListState = null;
+ }
+ }
+
+ public void setDarkTheme(boolean value) {
+ mDarkTheme = value;
+ if (mAdapter != null) {
+ mAdapter.setDarkTheme(value);
+ }
+ }
+
+ private int getDefaultVerticalScrollbarPosition() {
+ final Locale locale = Locale.getDefault();
+ final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
+ switch (layoutDirection) {
+ case View.LAYOUT_DIRECTION_RTL:
+ return View.SCROLLBAR_POSITION_LEFT;
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ return View.SCROLLBAR_POSITION_RIGHT;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListAdapter.java b/java/com/android/contacts/common/list/ContactListAdapter.java
new file mode 100644
index 000000000..6cd311811
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListAdapter.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippets;
+import android.view.ViewGroup;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.R;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. Also
+ * includes support for including the {@link ContactsContract.Profile} record in the list.
+ */
+public abstract class ContactListAdapter extends ContactEntryListAdapter {
+
+ private CharSequence mUnknownNameText;
+
+ public ContactListAdapter(Context context) {
+ super(context);
+
+ mUnknownNameText = context.getText(R.string.missing_name);
+ }
+
+ protected static Uri buildSectionIndexerUri(Uri uri) {
+ return uri.buildUpon().appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build();
+ }
+
+ public Uri getContactUri(int partitionIndex, Cursor cursor) {
+ long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+ String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
+ Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+ long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId();
+ if (uri != null && directoryId != Directory.DEFAULT) {
+ uri =
+ uri.buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
+ .build();
+ }
+ return uri;
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setQuickContactEnabled(isQuickContactEnabled());
+ view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+ view.setActivatedStateSupported(isSelectionVisible());
+ return view;
+ }
+
+ protected void bindSectionHeaderAndDivider(
+ ContactListItemView view, int position, Cursor cursor) {
+ view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+ if (isSectionHeaderDisplayEnabled()) {
+ Placement placement = getItemPlacementInSection(position);
+ view.setSectionHeader(placement.sectionHeader);
+ } else {
+ view.setSectionHeader(null);
+ }
+ }
+
+ protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+ if (!isPhotoSupported(partitionIndex)) {
+ view.removePhotoView();
+ return;
+ }
+
+ // Set the photo, if available
+ long photoId = 0;
+ if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) {
+ photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID);
+ }
+
+ if (photoId != 0) {
+ getPhotoLoader()
+ .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null);
+ } else {
+ final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
+ final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+ DefaultImageRequest request = null;
+ if (photoUri == null) {
+ request =
+ getDefaultImageRequestFromCursor(
+ cursor, ContactQuery.CONTACT_DISPLAY_NAME, ContactQuery.CONTACT_LOOKUP_KEY);
+ }
+ getPhotoLoader()
+ .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request);
+ }
+ }
+
+ protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, ContactQuery.CONTACT_DISPLAY_NAME);
+ // Note: we don't show phonetic any more (See issue 5265330)
+
+ bindViewId(view, cursor, ContactQuery.CONTACT_ID);
+ }
+
+ protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) {
+ view.showPresenceAndStatusMessage(
+ cursor, ContactQuery.CONTACT_PRESENCE_STATUS, ContactQuery.CONTACT_CONTACT_STATUS);
+ }
+
+ protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+ view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET);
+ }
+
+ @Override
+ public void changeCursor(int partitionIndex, Cursor cursor) {
+ super.changeCursor(partitionIndex, cursor);
+
+ if (cursor == null || !cursor.moveToFirst()) {
+ return;
+ }
+
+ // hasProfile tells whether the first row is a profile
+ final boolean hasProfile = cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1;
+
+ // Add ME profile on top of favorites
+ cursor.moveToFirst();
+ setProfileExists(hasProfile);
+ }
+
+ /** @return Projection useful for children. */
+ protected final String[] getProjection(boolean forSearch) {
+ final int sortOrder = getContactNameDisplayOrder();
+ if (forSearch) {
+ if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ return ContactQuery.FILTER_PROJECTION_PRIMARY;
+ } else {
+ return ContactQuery.FILTER_PROJECTION_ALTERNATIVE;
+ }
+ } else {
+ if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ return ContactQuery.CONTACT_PROJECTION_PRIMARY;
+ } else {
+ return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE;
+ }
+ }
+ }
+
+ protected static class ContactQuery {
+
+ public static final int CONTACT_ID = 0;
+ public static final int CONTACT_DISPLAY_NAME = 1;
+ public static final int CONTACT_PRESENCE_STATUS = 2;
+ public static final int CONTACT_CONTACT_STATUS = 3;
+ public static final int CONTACT_PHOTO_ID = 4;
+ public static final int CONTACT_PHOTO_URI = 5;
+ public static final int CONTACT_LOOKUP_KEY = 6;
+ public static final int CONTACT_IS_USER_PROFILE = 7;
+ public static final int CONTACT_PHONETIC_NAME = 8;
+ public static final int CONTACT_STARRED = 9;
+ public static final int CONTACT_SNIPPET = 10;
+ private static final String[] CONTACT_PROJECTION_PRIMARY =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_PRIMARY, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ };
+ private static final String[] CONTACT_PROJECTION_ALTERNATIVE =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ };
+ private static final String[] FILTER_PROJECTION_PRIMARY =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_PRIMARY, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ SearchSnippets.SNIPPET, // 10
+ };
+ private static final String[] FILTER_PROJECTION_ALTERNATIVE =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ SearchSnippets.SNIPPET, // 10
+ };
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListFilter.java b/java/com/android/contacts/common/list/ContactListFilter.java
new file mode 100644
index 000000000..1a03bb64c
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListFilter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+/** Contact list filter parameters. */
+public final class ContactListFilter implements Comparable<ContactListFilter>, Parcelable {
+
+ public static final int FILTER_TYPE_DEFAULT = -1;
+ public static final int FILTER_TYPE_ALL_ACCOUNTS = -2;
+ public static final int FILTER_TYPE_CUSTOM = -3;
+ public static final int FILTER_TYPE_STARRED = -4;
+ public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5;
+ public static final int FILTER_TYPE_SINGLE_CONTACT = -6;
+
+ public static final int FILTER_TYPE_ACCOUNT = 0;
+ public static final Parcelable.Creator<ContactListFilter> CREATOR =
+ new Parcelable.Creator<ContactListFilter>() {
+ @Override
+ public ContactListFilter createFromParcel(Parcel source) {
+ int filterType = source.readInt();
+ String accountName = source.readString();
+ String accountType = source.readString();
+ String dataSet = source.readString();
+ return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+ }
+
+ @Override
+ public ContactListFilter[] newArray(int size) {
+ return new ContactListFilter[size];
+ }
+ };
+ /**
+ * Obsolete filter which had been used in Honeycomb. This may be stored in {@link
+ * SharedPreferences}, but should be replaced with ALL filter when it is found.
+ *
+ * <p>TODO: "group" filter and relevant variables are all obsolete. Remove them.
+ */
+ private static final int FILTER_TYPE_GROUP = 1;
+
+ private static final String KEY_FILTER_TYPE = "filter.type";
+ private static final String KEY_ACCOUNT_NAME = "filter.accountName";
+ private static final String KEY_ACCOUNT_TYPE = "filter.accountType";
+ private static final String KEY_DATA_SET = "filter.dataSet";
+ public final int filterType;
+ public final String accountType;
+ public final String accountName;
+ public final String dataSet;
+ public final Drawable icon;
+ private String mId;
+
+ public ContactListFilter(
+ int filterType, String accountType, String accountName, String dataSet, Drawable icon) {
+ this.filterType = filterType;
+ this.accountType = accountType;
+ this.accountName = accountName;
+ this.dataSet = dataSet;
+ this.icon = icon;
+ }
+
+ public static ContactListFilter createFilterWithType(int filterType) {
+ return new ContactListFilter(filterType, null, null, null, null);
+ }
+
+ public static ContactListFilter createAccountFilter(
+ String accountType, String accountName, String dataSet, Drawable icon) {
+ return new ContactListFilter(
+ ContactListFilter.FILTER_TYPE_ACCOUNT, accountType, accountName, dataSet, icon);
+ }
+
+ /**
+ * Store the given {@link ContactListFilter} to preferences. If the requested filter is of type
+ * {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because it is a
+ * temporary state.
+ */
+ public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) {
+ if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+ return;
+ }
+ prefs
+ .edit()
+ .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType)
+ .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName)
+ .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType)
+ .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet)
+ .apply();
+ }
+
+ /**
+ * Try to obtain ContactListFilter object saved in SharedPreference. If there's no info there,
+ * return ALL filter instead.
+ */
+ public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) {
+ ContactListFilter filter = restoreFromPreferences(prefs);
+ if (filter == null) {
+ filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+ }
+ // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode"
+ // should also not be stored in preferences anymore since it is a temporary state.
+ if (filter.filterType == FILTER_TYPE_GROUP || filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+ filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+ }
+ return filter;
+ }
+
+ private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) {
+ int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT);
+ if (filterType == FILTER_TYPE_DEFAULT) {
+ return null;
+ }
+
+ String accountName = prefs.getString(KEY_ACCOUNT_NAME, null);
+ String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null);
+ String dataSet = prefs.getString(KEY_DATA_SET, null);
+ return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+ }
+
+ public static final String filterTypeToString(int filterType) {
+ switch (filterType) {
+ case FILTER_TYPE_DEFAULT:
+ return "FILTER_TYPE_DEFAULT";
+ case FILTER_TYPE_ALL_ACCOUNTS:
+ return "FILTER_TYPE_ALL_ACCOUNTS";
+ case FILTER_TYPE_CUSTOM:
+ return "FILTER_TYPE_CUSTOM";
+ case FILTER_TYPE_STARRED:
+ return "FILTER_TYPE_STARRED";
+ case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY";
+ case FILTER_TYPE_SINGLE_CONTACT:
+ return "FILTER_TYPE_SINGLE_CONTACT";
+ case FILTER_TYPE_ACCOUNT:
+ return "FILTER_TYPE_ACCOUNT";
+ default:
+ return "(unknown)";
+ }
+ }
+
+ /** Returns true if this filter is based on data and may become invalid over time. */
+ public boolean isValidationRequired() {
+ return filterType == FILTER_TYPE_ACCOUNT;
+ }
+
+ @Override
+ public String toString() {
+ switch (filterType) {
+ case FILTER_TYPE_DEFAULT:
+ return "default";
+ case FILTER_TYPE_ALL_ACCOUNTS:
+ return "all_accounts";
+ case FILTER_TYPE_CUSTOM:
+ return "custom";
+ case FILTER_TYPE_STARRED:
+ return "starred";
+ case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ return "with_phones";
+ case FILTER_TYPE_SINGLE_CONTACT:
+ return "single";
+ case FILTER_TYPE_ACCOUNT:
+ return "account: "
+ + accountType
+ + (dataSet != null ? "/" + dataSet : "")
+ + " "
+ + accountName;
+ }
+ return super.toString();
+ }
+
+ @Override
+ public int compareTo(ContactListFilter another) {
+ int res = accountName.compareTo(another.accountName);
+ if (res != 0) {
+ return res;
+ }
+
+ res = accountType.compareTo(another.accountType);
+ if (res != 0) {
+ return res;
+ }
+
+ return filterType - another.filterType;
+ }
+
+ @Override
+ public int hashCode() {
+ int code = filterType;
+ if (accountType != null) {
+ code = code * 31 + accountType.hashCode();
+ code = code * 31 + accountName.hashCode();
+ }
+ if (dataSet != null) {
+ code = code * 31 + dataSet.hashCode();
+ }
+ return code;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof ContactListFilter)) {
+ return false;
+ }
+
+ ContactListFilter otherFilter = (ContactListFilter) other;
+ return filterType == otherFilter.filterType
+ && TextUtils.equals(accountName, otherFilter.accountName)
+ && TextUtils.equals(accountType, otherFilter.accountType)
+ && TextUtils.equals(dataSet, otherFilter.dataSet);
+
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(filterType);
+ dest.writeString(accountName);
+ dest.writeString(accountType);
+ dest.writeString(dataSet);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Returns a string that can be used as a stable persistent identifier for this filter. */
+ public String getId() {
+ if (mId == null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(filterType);
+ if (accountType != null) {
+ sb.append('-').append(accountType);
+ }
+ if (dataSet != null) {
+ sb.append('/').append(dataSet);
+ }
+ if (accountName != null) {
+ sb.append('-').append(accountName.replace('-', '_'));
+ }
+ mId = sb.toString();
+ }
+ return mId;
+ }
+
+ /**
+ * Adds the account query parameters to the given {@code uriBuilder}.
+ *
+ * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT}.
+ */
+ public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) {
+ if (filterType != FILTER_TYPE_ACCOUNT) {
+ throw new IllegalStateException("filterType must be FILTER_TYPE_ACCOUNT");
+ }
+ uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
+ uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
+ if (!TextUtils.isEmpty(dataSet)) {
+ uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet);
+ }
+ return uriBuilder;
+ }
+
+ public String toDebugString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")");
+ if (filterType == FILTER_TYPE_ACCOUNT) {
+ builder
+ .append(", accountType: " + accountType)
+ .append(", accountName: " + accountName)
+ .append(", dataSet: " + dataSet);
+ }
+ builder.append(", icon: " + icon + "]");
+ return builder.toString();
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListFilterController.java b/java/com/android/contacts/common/list/ContactListFilterController.java
new file mode 100644
index 000000000..d2168f3f2
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListFilterController.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Manages {@link ContactListFilter}. All methods must be called from UI thread. */
+public abstract class ContactListFilterController {
+
+ // singleton to cache the filter controller
+ private static ContactListFilterControllerImpl sFilterController = null;
+
+ public static ContactListFilterController getInstance(Context context) {
+ // We may need to synchronize this in the future if background task will call this.
+ if (sFilterController == null) {
+ sFilterController = new ContactListFilterControllerImpl(context);
+ }
+ return sFilterController;
+ }
+
+ public abstract void addListener(ContactListFilterListener listener);
+
+ public abstract void removeListener(ContactListFilterListener listener);
+
+ /** Return the currently-active filter. */
+ public abstract ContactListFilter getFilter();
+
+ /**
+ * @param filter the filter
+ * @param persistent True when the given filter should be saved soon. False when the filter should
+ * not be saved. The latter case may happen when some Intent requires a certain type of UI
+ * (e.g. single contact) temporarily.
+ */
+ public abstract void setContactListFilter(ContactListFilter filter, boolean persistent);
+
+ public abstract void selectCustomFilter();
+
+ /**
+ * Checks if the current filter is valid and reset the filter if not. It may happen when an
+ * account is removed while the filter points to the account with {@link
+ * ContactListFilter#FILTER_TYPE_ACCOUNT} type, for example. It may also happen if the current
+ * filter is {@link ContactListFilter#FILTER_TYPE_SINGLE_CONTACT}, in which case, we should switch
+ * to the last saved filter in {@link SharedPreferences}.
+ */
+ public abstract void checkFilterValidity(boolean notifyListeners);
+
+ public interface ContactListFilterListener {
+
+ void onContactListFilterChanged();
+ }
+}
+
+/**
+ * Stores the {@link ContactListFilter} selected by the user and saves it to {@link
+ * SharedPreferences} if necessary.
+ */
+class ContactListFilterControllerImpl extends ContactListFilterController {
+
+ private final Context mContext;
+ private final List<ContactListFilterListener> mListeners =
+ new ArrayList<ContactListFilterListener>();
+ private ContactListFilter mFilter;
+
+ public ContactListFilterControllerImpl(Context context) {
+ mContext = context;
+ mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences());
+ checkFilterValidity(true /* notify listeners */);
+ }
+
+ @Override
+ public void addListener(ContactListFilterListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(ContactListFilterListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ private SharedPreferences getSharedPreferences() {
+ return PreferenceManager.getDefaultSharedPreferences(mContext);
+ }
+
+ @Override
+ public void setContactListFilter(ContactListFilter filter, boolean persistent) {
+ setContactListFilter(filter, persistent, true);
+ }
+
+ private void setContactListFilter(
+ ContactListFilter filter, boolean persistent, boolean notifyListeners) {
+ if (!filter.equals(mFilter)) {
+ mFilter = filter;
+ if (persistent) {
+ ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter);
+ }
+ if (notifyListeners && !mListeners.isEmpty()) {
+ notifyContactListFilterChanged();
+ }
+ }
+ }
+
+ @Override
+ public void selectCustomFilter() {
+ setContactListFilter(
+ ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_CUSTOM), true);
+ }
+
+ private void notifyContactListFilterChanged() {
+ for (ContactListFilterListener listener : mListeners) {
+ listener.onContactListFilterChanged();
+ }
+ }
+
+ @Override
+ public void checkFilterValidity(boolean notifyListeners) {
+ if (mFilter == null) {
+ return;
+ }
+
+ switch (mFilter.filterType) {
+ case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT:
+ setContactListFilter(
+ ContactListFilter.restoreDefaultPreferences(getSharedPreferences()),
+ false,
+ notifyListeners);
+ break;
+ case ContactListFilter.FILTER_TYPE_ACCOUNT:
+ if (!filterAccountExists()) {
+ // The current account filter points to invalid account. Use "all" filter
+ // instead.
+ setContactListFilter(
+ ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS),
+ true,
+ notifyListeners);
+ }
+ }
+ }
+
+ /** @return true if the Account for the current filter exists. */
+ private boolean filterAccountExists() {
+ final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+ final AccountWithDataSet filterAccount =
+ new AccountWithDataSet(mFilter.accountName, mFilter.accountType, mFilter.dataSet);
+ return accountTypeManager.contains(filterAccount, false);
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListItemView.java b/java/com/android/contacts/common/list/ContactListItemView.java
new file mode 100644
index 000000000..76842483a
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListItemView.java
@@ -0,0 +1,1513 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippets;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView.SelectionBoundsAdjuster;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.format.TextHighlighter;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.contacts.common.util.SearchUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.ViewUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A custom view for an item in the contact list. The view contains the contact's photo, a set of
+ * text views (for name, status, etc...) and icons for presence and call. The view uses no XML file
+ * for layout and all the measurements and layouts are done in the onMeasure and onLayout methods.
+ *
+ * <p>The layout puts the contact's photo on the right side of the view, the call icon (if present)
+ * to the left of the photo, the text lines are aligned to the left and the presence icon (if
+ * present) is set to the left of the status line.
+ *
+ * <p>The layout also supports a header (used as a header of a group of contacts) that is above the
+ * contact's data and a divider between contact view.
+ */
+public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster {
+ private static final Pattern SPLIT_PATTERN =
+ Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
+ static final char SNIPPET_START_MATCH = '[';
+ static final char SNIPPET_END_MATCH = ']';
+ /** A helper used to highlight a prefix in a text field. */
+ private final TextHighlighter mTextHighlighter;
+ // Style values for layout and appearance
+ // The initialized values are defaults if none is provided through xml.
+ private int mPreferredHeight = 0;
+ private int mGapBetweenImageAndText = 0;
+ private int mGapBetweenLabelAndData = 0;
+ private int mPresenceIconMargin = 4;
+ private int mPresenceIconSize = 16;
+ private int mTextIndent = 0;
+ private int mTextOffsetTop;
+ private int mNameTextViewTextSize;
+ private int mHeaderWidth;
+ private Drawable mActivatedBackgroundDrawable;
+ private int mVideoCallIconSize = 32;
+ private int mVideoCallIconMargin = 16;
+ // Set in onLayout. Represent left and right position of the View on the screen.
+ private int mLeftOffset;
+ private int mRightOffset;
+ /** Used with {@link #mLabelView}, specifying the width ratio between label and data. */
+ private int mLabelViewWidthWeight = 3;
+ /** Used with {@link #mDataView}, specifying the width ratio between label and data. */
+ private int mDataViewWidthWeight = 5;
+
+ private ArrayList<HighlightSequence> mNameHighlightSequence;
+ private ArrayList<HighlightSequence> mNumberHighlightSequence;
+ // Highlighting prefix for names.
+ private String mHighlightedPrefix;
+ /** Used to notify listeners when a video call icon is clicked. */
+ private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener;
+ /** Indicates whether to show the "video call" icon, used to initiate a video call. */
+ private boolean mShowVideoCallIcon = false;
+ /** Indicates whether the view should leave room for the "video call" icon. */
+ private boolean mSupportVideoCallIcon = false;
+
+ private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */);
+ // Header layout data
+ private TextView mHeaderTextView;
+ private boolean mIsSectionHeaderEnabled;
+ // The views inside the contact view
+ private boolean mQuickContactEnabled = true;
+ private QuickContactBadge mQuickContact;
+ private ImageView mPhotoView;
+ private TextView mNameTextView;
+ private TextView mLabelView;
+ private TextView mDataView;
+ private TextView mSnippetView;
+ private TextView mStatusView;
+ private ImageView mPresenceIcon;
+ private ImageView mVideoCallIcon;
+ private ImageView mWorkProfileIcon;
+ private ColorStateList mSecondaryTextColor;
+ private int mDefaultPhotoViewSize = 0;
+ /**
+ * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
+ * to align other data in this View.
+ */
+ private int mPhotoViewWidth;
+ /**
+ * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
+ */
+ private int mPhotoViewHeight;
+ /**
+ * Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of
+ * the photo should have horizontal padding on those left assuming there is a photo.
+ */
+ private boolean mKeepHorizontalPaddingForPhotoView;
+ /** Only effective when {@link #mPhotoView} is null. */
+ private boolean mKeepVerticalPaddingForPhotoView;
+ /**
+ * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
+ * False indicates those values should be updated before being used in position calculation.
+ */
+ private boolean mPhotoViewWidthAndHeightAreReady = false;
+
+ private int mNameTextViewHeight;
+ private int mNameTextViewTextColor = Color.BLACK;
+ private int mPhoneticNameTextViewHeight;
+ private int mLabelViewHeight;
+ private int mDataViewHeight;
+ private int mSnippetTextViewHeight;
+ private int mStatusTextViewHeight;
+ private int mCheckBoxWidth;
+ // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
+ // same row.
+ private int mLabelAndDataViewMaxHeight;
+ private boolean mActivatedStateSupported;
+ private boolean mAdjustSelectionBoundsEnabled = true;
+ private Rect mBoundsWithoutHeader = new Rect();
+ private CharSequence mUnknownNameText;
+ private int mPosition;
+
+ public ContactListItemView(Context context) {
+ super(context);
+
+ mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+ mNameHighlightSequence = new ArrayList<HighlightSequence>();
+ mNumberHighlightSequence = new ArrayList<HighlightSequence>();
+ }
+
+ public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
+ this(context, attrs);
+
+ mSupportVideoCallIcon = supportVideoCallIcon;
+ }
+
+ public ContactListItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a;
+
+ if (R.styleable.ContactListItemView != null) {
+ // Read all style values
+ a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+ mPreferredHeight =
+ a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
+ mActivatedBackgroundDrawable =
+ a.getDrawable(R.styleable.ContactListItemView_activated_background);
+
+ mGapBetweenImageAndText =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
+ mGapBetweenImageAndText);
+ mGapBetweenLabelAndData =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
+ mGapBetweenLabelAndData);
+ mPresenceIconMargin =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin);
+ mPresenceIconSize =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
+ mDefaultPhotoViewSize =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
+ mTextIndent =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
+ mTextOffsetTop =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
+ mDataViewWidthWeight =
+ a.getInteger(
+ R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight);
+ mLabelViewWidthWeight =
+ a.getInteger(
+ R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight);
+ mNameTextViewTextColor =
+ a.getColor(
+ R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor);
+ mNameTextViewTextSize =
+ (int)
+ a.getDimension(
+ R.styleable.ContactListItemView_list_item_name_text_size,
+ (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
+ mVideoCallIconSize =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_video_call_icon_size, mVideoCallIconSize);
+ mVideoCallIconMargin =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_video_call_icon_margin,
+ mVideoCallIconMargin);
+
+ setPaddingRelative(
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0),
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0),
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0),
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0));
+
+ a.recycle();
+ }
+
+ mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+
+ if (R.styleable.Theme != null) {
+ a = getContext().obtainStyledAttributes(R.styleable.Theme);
+ mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
+ a.recycle();
+ }
+
+ mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
+
+ if (mActivatedBackgroundDrawable != null) {
+ mActivatedBackgroundDrawable.setCallback(this);
+ }
+
+ mNameHighlightSequence = new ArrayList<HighlightSequence>();
+ mNumberHighlightSequence = new ArrayList<HighlightSequence>();
+
+ setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
+ }
+
+ public static final PhotoPosition getDefaultPhotoPosition(boolean opposite) {
+ final Locale locale = Locale.getDefault();
+ final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
+ switch (layoutDirection) {
+ case View.LAYOUT_DIRECTION_RTL:
+ return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT);
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT);
+ }
+ }
+
+ /**
+ * Helper method for splitting a string into tokens. The lists passed in are populated with the
+ * tokens and offsets into the content of each token. The tokenization function parses e-mail
+ * addresses as a single token; otherwise it splits on any non-alphanumeric character.
+ *
+ * @param content Content to split.
+ * @return List of token strings.
+ */
+ private static List<String> split(String content) {
+ final Matcher matcher = SPLIT_PATTERN.matcher(content);
+ final ArrayList<String> tokens = new ArrayList<>();
+ while (matcher.find()) {
+ tokens.add(matcher.group());
+ }
+ return tokens;
+ }
+
+ public void setUnknownNameText(CharSequence unknownNameText) {
+ mUnknownNameText = unknownNameText;
+ }
+
+ public void setQuickContactEnabled(boolean flag) {
+ mQuickContactEnabled = flag;
+ }
+
+ /**
+ * Sets whether the video calling icon is shown. For the video calling icon to be shown, {@link
+ * #mSupportVideoCallIcon} must be {@code true}.
+ *
+ * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false}
+ * otherwise.
+ * @param listener Listener to notify when the video calling icon is clicked.
+ * @param position The position in the adapater of the video calling icon.
+ */
+ public void setShowVideoCallIcon(
+ boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position) {
+ mShowVideoCallIcon = showVideoCallIcon;
+ mPhoneNumberListAdapterListener = listener;
+ mPosition = position;
+
+ if (mShowVideoCallIcon) {
+ if (mVideoCallIcon == null) {
+ mVideoCallIcon = new ImageView(getContext());
+ addView(mVideoCallIcon);
+ }
+ mVideoCallIcon.setContentDescription(
+ getContext().getString(R.string.description_search_video_call));
+ mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call);
+ mVideoCallIcon.setScaleType(ScaleType.CENTER);
+ mVideoCallIcon.setVisibility(View.VISIBLE);
+ mVideoCallIcon.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Inform the adapter that the video calling icon was clicked.
+ if (mPhoneNumberListAdapterListener != null) {
+ mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition);
+ }
+ }
+ });
+ } else {
+ if (mVideoCallIcon != null) {
+ mVideoCallIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ /**
+ * Sets whether the view supports a video calling icon. This is independent of whether the view is
+ * actually showing an icon. Support for the video calling icon ensures that the layout leaves
+ * space for the video icon, should it be shown.
+ *
+ * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false}
+ * otherwise.
+ */
+ public void setSupportVideoCallIcon(boolean supportVideoCallIcon) {
+ mSupportVideoCallIcon = supportVideoCallIcon;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // We will match parent's width and wrap content vertically, but make sure
+ // height is no less than listPreferredItemHeight.
+ final int specWidth = resolveSize(0, widthMeasureSpec);
+ final int preferredHeight = mPreferredHeight;
+
+ mNameTextViewHeight = 0;
+ mPhoneticNameTextViewHeight = 0;
+ mLabelViewHeight = 0;
+ mDataViewHeight = 0;
+ mLabelAndDataViewMaxHeight = 0;
+ mSnippetTextViewHeight = 0;
+ mStatusTextViewHeight = 0;
+ mCheckBoxWidth = 0;
+
+ ensurePhotoViewSize();
+
+ // Width each TextView is able to use.
+ int effectiveWidth;
+ // All the other Views will honor the photo, so available width for them may be shrunk.
+ if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
+ effectiveWidth =
+ specWidth
+ - getPaddingLeft()
+ - getPaddingRight()
+ - (mPhotoViewWidth + mGapBetweenImageAndText);
+ } else {
+ effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
+ }
+
+ if (mIsSectionHeaderEnabled) {
+ effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText;
+ }
+
+ if (mSupportVideoCallIcon) {
+ effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin);
+ }
+
+ // Go over all visible text views and measure actual width of each of them.
+ // Also calculate their heights to get the total height for this entire view.
+
+ if (isVisible(mNameTextView)) {
+ // Calculate width for name text - this parallels similar measurement in onLayout.
+ int nameTextWidth = effectiveWidth;
+ if (mPhotoPosition != PhotoPosition.LEFT) {
+ nameTextWidth -= mTextIndent;
+ }
+ mNameTextView.measure(
+ MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mNameTextViewHeight = mNameTextView.getMeasuredHeight();
+ }
+
+ // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
+ // we should ellipsize both using appropriate ratio.
+ final int dataWidth;
+ final int labelWidth;
+ if (isVisible(mDataView)) {
+ if (isVisible(mLabelView)) {
+ final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
+ dataWidth =
+ ((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
+ labelWidth =
+ ((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
+ } else {
+ dataWidth = effectiveWidth;
+ labelWidth = 0;
+ }
+ } else {
+ dataWidth = 0;
+ if (isVisible(mLabelView)) {
+ labelWidth = effectiveWidth;
+ } else {
+ labelWidth = 0;
+ }
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.measure(
+ MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mDataViewHeight = mDataView.getMeasuredHeight();
+ }
+
+ if (isVisible(mLabelView)) {
+ mLabelView.measure(
+ MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mLabelViewHeight = mLabelView.getMeasuredHeight();
+ }
+ mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.measure(
+ MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
+ }
+
+ // Status view height is the biggest of the text view and the presence icon
+ if (isVisible(mPresenceIcon)) {
+ mPresenceIcon.measure(
+ MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
+ mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
+ }
+
+ if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) {
+ mVideoCallIcon.measure(
+ MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY));
+ }
+
+ if (isVisible(mWorkProfileIcon)) {
+ mWorkProfileIcon.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
+ }
+
+ if (isVisible(mStatusView)) {
+ // Presence and status are in a same row, so status will be affected by icon size.
+ final int statusWidth;
+ if (isVisible(mPresenceIcon)) {
+ statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin);
+ } else {
+ statusWidth = effectiveWidth;
+ }
+ mStatusView.measure(
+ MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
+ }
+
+ // Calculate height including padding.
+ int height =
+ (mNameTextViewHeight
+ + mPhoneticNameTextViewHeight
+ + mLabelAndDataViewMaxHeight
+ + mSnippetTextViewHeight
+ + mStatusTextViewHeight);
+
+ // Make sure the height is at least as high as the photo
+ height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
+
+ // Make sure height is at least the preferred height
+ height = Math.max(height, preferredHeight);
+
+ // Measure the header if it is visible.
+ if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) {
+ mHeaderTextView.measure(
+ MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ }
+
+ setMeasuredDimension(specWidth, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int height = bottom - top;
+ final int width = right - left;
+
+ // Determine the vertical bounds by laying out the header first.
+ int topBound = 0;
+ int bottomBound = height;
+ int leftBound = getPaddingLeft();
+ int rightBound = width - getPaddingRight();
+
+ final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
+
+ // Put the section header on the left side of the contact view.
+ if (mIsSectionHeaderEnabled) {
+ // Align the text view all the way left, to be consistent with Contacts.
+ if (isLayoutRtl) {
+ rightBound = width;
+ } else {
+ leftBound = 0;
+ }
+ if (mHeaderTextView != null) {
+ int headerHeight = mHeaderTextView.getMeasuredHeight();
+ int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop;
+
+ mHeaderTextView.layout(
+ isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
+ headerTopBound,
+ isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
+ headerTopBound + headerHeight);
+ }
+ if (isLayoutRtl) {
+ rightBound -= mHeaderWidth;
+ } else {
+ leftBound += mHeaderWidth;
+ }
+ }
+
+ mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound);
+ mLeftOffset = left + leftBound;
+ mRightOffset = left + rightBound;
+ if (mIsSectionHeaderEnabled) {
+ if (isLayoutRtl) {
+ rightBound -= mGapBetweenImageAndText;
+ } else {
+ leftBound += mGapBetweenImageAndText;
+ }
+ }
+
+ if (mActivatedStateSupported && isActivated()) {
+ mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
+ }
+
+ final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ // Photo is the left most view. All the other Views should on the right of the photo.
+ if (photoView != null) {
+ // Center the photo vertically
+ final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight);
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ } else if (mKeepHorizontalPaddingForPhotoView) {
+ // Draw nothing but keep the padding.
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ }
+ } else {
+ // Photo is the right most view. Right bound should be adjusted that way.
+ if (photoView != null) {
+ // Center the photo vertically
+ final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight);
+ rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+ } else if (mKeepHorizontalPaddingForPhotoView) {
+ // Draw nothing but keep the padding.
+ rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+ }
+
+ // Add indent between left-most padding and texts.
+ leftBound += mTextIndent;
+ }
+
+ if (mSupportVideoCallIcon) {
+ // Place the video call button at the end of the list (e.g. take into account RTL mode).
+ if (isVisible(mVideoCallIcon)) {
+ // Center the video icon vertically
+ final int videoIconTop = topBound + (bottomBound - topBound - mVideoCallIconSize) / 2;
+
+ if (!isLayoutRtl) {
+ // When photo is on left, video icon is placed on the right edge.
+ mVideoCallIcon.layout(
+ rightBound - mVideoCallIconSize,
+ videoIconTop,
+ rightBound,
+ videoIconTop + mVideoCallIconSize);
+ } else {
+ // When photo is on right, video icon is placed on the left edge.
+ mVideoCallIcon.layout(
+ leftBound,
+ videoIconTop,
+ leftBound + mVideoCallIconSize,
+ videoIconTop + mVideoCallIconSize);
+ }
+ }
+
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ rightBound -= (mVideoCallIconSize + mVideoCallIconMargin);
+ } else {
+ leftBound += mVideoCallIconSize + mVideoCallIconMargin;
+ }
+ }
+
+ // Center text vertically, then apply the top offset.
+ final int totalTextHeight =
+ mNameTextViewHeight
+ + mPhoneticNameTextViewHeight
+ + mLabelAndDataViewMaxHeight
+ + mSnippetTextViewHeight
+ + mStatusTextViewHeight;
+ int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop;
+
+ // Work Profile icon align top
+ int workProfileIconWidth = 0;
+ if (isVisible(mWorkProfileIcon)) {
+ workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
+ final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ // When photo is on left, label is placed on the right edge of the list item.
+ mWorkProfileIcon.layout(
+ rightBound - workProfileIconWidth - distanceFromEnd,
+ textTopBound,
+ rightBound - distanceFromEnd,
+ textTopBound + mNameTextViewHeight);
+ } else {
+ // When photo is on right, label is placed on the left of data view.
+ mWorkProfileIcon.layout(
+ leftBound + distanceFromEnd,
+ textTopBound,
+ leftBound + workProfileIconWidth + distanceFromEnd,
+ textTopBound + mNameTextViewHeight);
+ }
+ }
+
+ // Layout all text view and presence icon
+ // Put name TextView first
+ if (isVisible(mNameTextView)) {
+ final int distanceFromEnd =
+ workProfileIconWidth
+ + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ mNameTextView.layout(
+ leftBound,
+ textTopBound,
+ rightBound - distanceFromEnd,
+ textTopBound + mNameTextViewHeight);
+ } else {
+ mNameTextView.layout(
+ leftBound + distanceFromEnd,
+ textTopBound,
+ rightBound,
+ textTopBound + mNameTextViewHeight);
+ }
+ }
+
+ if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
+ textTopBound += mNameTextViewHeight;
+ }
+
+ // Presence and status
+ if (isLayoutRtl) {
+ int statusRightBound = rightBound;
+ if (isVisible(mPresenceIcon)) {
+ int iconWidth = mPresenceIcon.getMeasuredWidth();
+ mPresenceIcon.layout(
+ rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
+ statusRightBound -= (iconWidth + mPresenceIconMargin);
+ }
+
+ if (isVisible(mStatusView)) {
+ mStatusView.layout(
+ leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight);
+ }
+ } else {
+ int statusLeftBound = leftBound;
+ if (isVisible(mPresenceIcon)) {
+ int iconWidth = mPresenceIcon.getMeasuredWidth();
+ mPresenceIcon.layout(
+ leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight);
+ statusLeftBound += (iconWidth + mPresenceIconMargin);
+ }
+
+ if (isVisible(mStatusView)) {
+ mStatusView.layout(
+ statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
+ }
+ }
+
+ if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
+ textTopBound += mStatusTextViewHeight;
+ }
+
+ // Rest of text views
+ int dataLeftBound = leftBound;
+
+ // Label and Data align bottom.
+ if (isVisible(mLabelView)) {
+ if (!isLayoutRtl) {
+ mLabelView.layout(
+ dataLeftBound,
+ textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
+ } else {
+ dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+ mLabelView.layout(
+ rightBound - mLabelView.getMeasuredWidth(),
+ textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
+ }
+ }
+
+ if (isVisible(mDataView)) {
+ if (!isLayoutRtl) {
+ mDataView.layout(
+ dataLeftBound,
+ textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ } else {
+ mDataView.layout(
+ rightBound - mDataView.getMeasuredWidth(),
+ textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ }
+ }
+ if (isVisible(mLabelView) || isVisible(mDataView)) {
+ textTopBound += mLabelAndDataViewMaxHeight;
+ }
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.layout(
+ leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight);
+ }
+ }
+
+ @Override
+ public void adjustListItemSelectionBounds(Rect bounds) {
+ if (mAdjustSelectionBoundsEnabled) {
+ bounds.top += mBoundsWithoutHeader.top;
+ bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
+ bounds.left = mBoundsWithoutHeader.left;
+ bounds.right = mBoundsWithoutHeader.right;
+ }
+ }
+
+ protected boolean isVisible(View view) {
+ return view != null && view.getVisibility() == View.VISIBLE;
+ }
+
+ /** Extracts width and height from the style */
+ private void ensurePhotoViewSize() {
+ if (!mPhotoViewWidthAndHeightAreReady) {
+ mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
+ if (!mQuickContactEnabled && mPhotoView == null) {
+ if (!mKeepHorizontalPaddingForPhotoView) {
+ mPhotoViewWidth = 0;
+ }
+ if (!mKeepVerticalPaddingForPhotoView) {
+ mPhotoViewHeight = 0;
+ }
+ }
+
+ mPhotoViewWidthAndHeightAreReady = true;
+ }
+ }
+
+ protected int getDefaultPhotoViewSize() {
+ return mDefaultPhotoViewSize;
+ }
+
+ /**
+ * Gets a LayoutParam that corresponds to the default photo size.
+ *
+ * @return A new LayoutParam.
+ */
+ private LayoutParams getDefaultPhotoLayoutParams() {
+ LayoutParams params = generateDefaultLayoutParams();
+ params.width = getDefaultPhotoViewSize();
+ params.height = params.width;
+ return params;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mActivatedStateSupported) {
+ mActivatedBackgroundDrawable.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mActivatedStateSupported) {
+ mActivatedBackgroundDrawable.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ if (mActivatedStateSupported && isActivated()) {
+ mActivatedBackgroundDrawable.draw(canvas);
+ }
+
+ super.dispatchDraw(canvas);
+ }
+
+ /** Sets section header or makes it invisible if the title is null. */
+ public void setSectionHeader(String title) {
+ if (!TextUtils.isEmpty(title)) {
+ if (mHeaderTextView == null) {
+ mHeaderTextView = new TextView(getContext());
+ mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle);
+ mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
+ addView(mHeaderTextView);
+ }
+ setMarqueeText(mHeaderTextView, title);
+ mHeaderTextView.setVisibility(View.VISIBLE);
+ mHeaderTextView.setAllCaps(true);
+ } else if (mHeaderTextView != null) {
+ mHeaderTextView.setVisibility(View.GONE);
+ }
+ }
+
+ public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
+ mIsSectionHeaderEnabled = isSectionHeaderEnabled;
+ }
+
+ /** Returns the quick contact badge, creating it if necessary. */
+ public QuickContactBadge getQuickContact() {
+ if (!mQuickContactEnabled) {
+ throw new IllegalStateException("QuickContact is disabled for this view");
+ }
+ if (mQuickContact == null) {
+ mQuickContact = new QuickContactBadge(getContext());
+ if (CompatUtils.isLollipopCompatible()) {
+ mQuickContact.setOverlay(null);
+ }
+ mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
+ if (mNameTextView != null) {
+ mQuickContact.setContentDescription(
+ getContext()
+ .getString(R.string.description_quick_contact_for, mNameTextView.getText()));
+ }
+
+ addView(mQuickContact);
+ mPhotoViewWidthAndHeightAreReady = false;
+ }
+ return mQuickContact;
+ }
+
+ /** Returns the photo view, creating it if necessary. */
+ public ImageView getPhotoView() {
+ if (mPhotoView == null) {
+ mPhotoView = new ImageView(getContext());
+ mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
+ // Quick contact style used above will set a background - remove it
+ mPhotoView.setBackground(null);
+ addView(mPhotoView);
+ mPhotoViewWidthAndHeightAreReady = false;
+ }
+ return mPhotoView;
+ }
+
+ /** Removes the photo view. */
+ public void removePhotoView() {
+ removePhotoView(false, true);
+ }
+
+ /**
+ * Removes the photo view.
+ *
+ * @param keepHorizontalPadding True means data on the right side will have padding on left,
+ * pretending there is still a photo view.
+ * @param keepVerticalPadding True means the View will have some height enough for accommodating a
+ * photo view.
+ */
+ public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
+ mPhotoViewWidthAndHeightAreReady = false;
+ mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
+ mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
+ if (mPhotoView != null) {
+ removeView(mPhotoView);
+ mPhotoView = null;
+ }
+ if (mQuickContact != null) {
+ removeView(mQuickContact);
+ mQuickContact = null;
+ }
+ }
+
+ /**
+ * Sets a word prefix that will be highlighted if encountered in fields like name and search
+ * snippet. This will disable the mask highlighting for names.
+ *
+ * <p>NOTE: must be all upper-case
+ */
+ public void setHighlightedPrefix(String upperCasePrefix) {
+ mHighlightedPrefix = upperCasePrefix;
+ }
+
+ /** Clears previously set highlight sequences for the view. */
+ public void clearHighlightSequences() {
+ mNameHighlightSequence.clear();
+ mNumberHighlightSequence.clear();
+ mHighlightedPrefix = null;
+ }
+
+ /**
+ * Adds a highlight sequence to the name highlighter.
+ *
+ * @param start The start position of the highlight sequence.
+ * @param end The end position of the highlight sequence.
+ */
+ public void addNameHighlightSequence(int start, int end) {
+ mNameHighlightSequence.add(new HighlightSequence(start, end));
+ }
+
+ /**
+ * Adds a highlight sequence to the number highlighter.
+ *
+ * @param start The start position of the highlight sequence.
+ * @param end The end position of the highlight sequence.
+ */
+ public void addNumberHighlightSequence(int start, int end) {
+ mNumberHighlightSequence.add(new HighlightSequence(start, end));
+ }
+
+ /** Returns the text view for the contact name, creating it if necessary. */
+ public TextView getNameTextView() {
+ if (mNameTextView == null) {
+ mNameTextView = new TextView(getContext());
+ mNameTextView.setSingleLine(true);
+ mNameTextView.setEllipsize(getTextEllipsis());
+ mNameTextView.setTextColor(mNameTextViewTextColor);
+ mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
+ // Manually call setActivated() since this view may be added after the first
+ // setActivated() call toward this whole item view.
+ mNameTextView.setActivated(isActivated());
+ mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+ mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ mNameTextView.setId(R.id.cliv_name_textview);
+ if (CompatUtils.isLollipopCompatible()) {
+ mNameTextView.setElegantTextHeight(false);
+ }
+ addView(mNameTextView);
+ }
+ return mNameTextView;
+ }
+
+ /** Adds or updates a text view for the data label. */
+ public void setLabel(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mLabelView != null) {
+ mLabelView.setVisibility(View.GONE);
+ }
+ } else {
+ getLabelView();
+ setMarqueeText(mLabelView, text);
+ mLabelView.setVisibility(VISIBLE);
+ }
+ }
+
+ /** Returns the text view for the data label, creating it if necessary. */
+ public TextView getLabelView() {
+ if (mLabelView == null) {
+ mLabelView = new TextView(getContext());
+ mLabelView.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+ mLabelView.setSingleLine(true);
+ mLabelView.setEllipsize(getTextEllipsis());
+ mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ mLabelView.setAllCaps(true);
+ } else {
+ mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+ }
+ mLabelView.setActivated(isActivated());
+ mLabelView.setId(R.id.cliv_label_textview);
+ addView(mLabelView);
+ }
+ return mLabelView;
+ }
+
+ /**
+ * Sets phone number for a list item. This takes care of number highlighting if the highlight mask
+ * exists.
+ */
+ public void setPhoneNumber(String text) {
+ if (text == null) {
+ if (mDataView != null) {
+ mDataView.setVisibility(View.GONE);
+ }
+ } else {
+ getDataView();
+
+ // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
+ // mDataView. Make sure that determination of the highlight sequences are done only
+ // after number formatting.
+
+ // Sets phone number texts for display after highlighting it, if applicable.
+ // CharSequence textToSet = text;
+ final SpannableString textToSet = new SpannableString(text);
+
+ if (mNumberHighlightSequence.size() != 0) {
+ final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
+ mTextHighlighter.applyMaskingHighlight(
+ textToSet, highlightSequence.start, highlightSequence.end);
+ }
+
+ setMarqueeText(mDataView, textToSet);
+ mDataView.setVisibility(VISIBLE);
+
+ // We have a phone number as "mDataView" so make it always LTR and VIEW_START
+ mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
+ mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ }
+ }
+
+ private void setMarqueeText(TextView textView, CharSequence text) {
+ if (getTextEllipsis() == TruncateAt.MARQUEE) {
+ // To show MARQUEE correctly (with END effect during non-active state), we need
+ // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
+ final SpannableString spannable = new SpannableString(text);
+ spannable.setSpan(
+ TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ textView.setText(spannable);
+ } else {
+ textView.setText(text);
+ }
+ }
+
+ /** Returns the text view for the data text, creating it if necessary. */
+ public TextView getDataView() {
+ if (mDataView == null) {
+ mDataView = new TextView(getContext());
+ mDataView.setSingleLine(true);
+ mDataView.setEllipsize(getTextEllipsis());
+ mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
+ mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ mDataView.setActivated(isActivated());
+ mDataView.setId(R.id.cliv_data_view);
+ if (CompatUtils.isLollipopCompatible()) {
+ mDataView.setElegantTextHeight(false);
+ }
+ addView(mDataView);
+ }
+ return mDataView;
+ }
+
+ /** Adds or updates a text view for the search snippet. */
+ public void setSnippet(String text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mSnippetView != null) {
+ mSnippetView.setVisibility(View.GONE);
+ }
+ } else {
+ mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
+ mSnippetView.setVisibility(VISIBLE);
+ if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
+ // Give the text-to-speech engine a hint that it's a phone number
+ mSnippetView.setContentDescription(PhoneNumberUtilsCompat.createTtsSpannable(text));
+ } else {
+ mSnippetView.setContentDescription(null);
+ }
+ }
+ }
+
+ /** Returns the text view for the search snippet, creating it if necessary. */
+ public TextView getSnippetView() {
+ if (mSnippetView == null) {
+ mSnippetView = new TextView(getContext());
+ mSnippetView.setSingleLine(true);
+ mSnippetView.setEllipsize(getTextEllipsis());
+ mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+ mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ mSnippetView.setActivated(isActivated());
+ addView(mSnippetView);
+ }
+ return mSnippetView;
+ }
+
+ /** Returns the text view for the status, creating it if necessary. */
+ public TextView getStatusView() {
+ if (mStatusView == null) {
+ mStatusView = new TextView(getContext());
+ mStatusView.setSingleLine(true);
+ mStatusView.setEllipsize(getTextEllipsis());
+ mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+ mStatusView.setTextColor(mSecondaryTextColor);
+ mStatusView.setActivated(isActivated());
+ mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ addView(mStatusView);
+ }
+ return mStatusView;
+ }
+
+ /** Adds or updates a text view for the status. */
+ public void setStatus(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mStatusView != null) {
+ mStatusView.setVisibility(View.GONE);
+ }
+ } else {
+ getStatusView();
+ setMarqueeText(mStatusView, text);
+ mStatusView.setVisibility(VISIBLE);
+ }
+ }
+
+ /** Adds or updates the presence icon view. */
+ public void setPresence(Drawable icon) {
+ if (icon != null) {
+ if (mPresenceIcon == null) {
+ mPresenceIcon = new ImageView(getContext());
+ addView(mPresenceIcon);
+ }
+ mPresenceIcon.setImageDrawable(icon);
+ mPresenceIcon.setScaleType(ScaleType.CENTER);
+ mPresenceIcon.setVisibility(View.VISIBLE);
+ } else {
+ if (mPresenceIcon != null) {
+ mPresenceIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ /**
+ * Set to display work profile icon or not
+ *
+ * @param enabled set to display work profile icon or not
+ */
+ public void setWorkProfileIconEnabled(boolean enabled) {
+ if (mWorkProfileIcon != null) {
+ mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ } else if (enabled) {
+ mWorkProfileIcon = new ImageView(getContext());
+ addView(mWorkProfileIcon);
+ mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
+ mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
+ mWorkProfileIcon.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private TruncateAt getTextEllipsis() {
+ return TruncateAt.MARQUEE;
+ }
+
+ public void showDisplayName(Cursor cursor, int nameColumnIndex) {
+ CharSequence name = cursor.getString(nameColumnIndex);
+ setDisplayName(name);
+
+ // Since the quick contact content description is derived from the display name and there is
+ // no guarantee that when the quick contact is initialized the display name is already set,
+ // do it here too.
+ if (mQuickContact != null) {
+ mQuickContact.setContentDescription(
+ getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText()));
+ }
+ }
+
+ public void setDisplayName(CharSequence name) {
+ if (!TextUtils.isEmpty(name)) {
+ // Chooses the available highlighting method for highlighting.
+ if (mHighlightedPrefix != null) {
+ name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
+ } else if (mNameHighlightSequence.size() != 0) {
+ final SpannableString spannableName = new SpannableString(name);
+ for (HighlightSequence highlightSequence : mNameHighlightSequence) {
+ mTextHighlighter.applyMaskingHighlight(
+ spannableName, highlightSequence.start, highlightSequence.end);
+ }
+ name = spannableName;
+ }
+ } else {
+ name = mUnknownNameText;
+ }
+ setMarqueeText(getNameTextView(), name);
+
+ if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
+ // Give the text-to-speech engine a hint that it's a phone number
+ mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
+ mNameTextView.setContentDescription(
+ PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
+ } else {
+ // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
+ // of the name into two separate parts.
+ mNameTextView.setContentDescription(name.toString());
+ }
+ }
+
+ public void hideDisplayName() {
+ if (mNameTextView != null) {
+ removeView(mNameTextView);
+ mNameTextView = null;
+ }
+ }
+
+ /** Sets the proper icon (star or presence or nothing) and/or status message. */
+ public void showPresenceAndStatusMessage(
+ Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) {
+ Drawable icon = null;
+ int presence = 0;
+ if (!cursor.isNull(presenceColumnIndex)) {
+ presence = cursor.getInt(presenceColumnIndex);
+ icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
+ }
+ setPresence(icon);
+
+ String statusMessage = null;
+ if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
+ statusMessage = cursor.getString(contactStatusColumnIndex);
+ }
+ // If there is no status message from the contact, but there was a presence value, then use
+ // the default status message string
+ if (statusMessage == null && presence != 0) {
+ statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
+ }
+ setStatus(statusMessage);
+ }
+
+ /** Shows search snippet. */
+ public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
+ if (cursor.getColumnCount() <= summarySnippetColumnIndex
+ || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
+ setSnippet(null);
+ return;
+ }
+
+ String snippet = cursor.getString(summarySnippetColumnIndex);
+
+ // Do client side snippeting if provider didn't do it
+ final Bundle extras = cursor.getExtras();
+ if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
+
+ final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
+
+ String displayName = null;
+ int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
+ if (displayNameIndex >= 0) {
+ displayName = cursor.getString(displayNameIndex);
+ }
+
+ snippet = updateSnippet(snippet, query, displayName);
+
+ } else {
+ if (snippet != null) {
+ int from = 0;
+ int to = snippet.length();
+ int start = snippet.indexOf(SNIPPET_START_MATCH);
+ if (start == -1) {
+ snippet = null;
+ } else {
+ int firstNl = snippet.lastIndexOf('\n', start);
+ if (firstNl != -1) {
+ from = firstNl + 1;
+ }
+ int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
+ if (end != -1) {
+ int lastNl = snippet.indexOf('\n', end);
+ if (lastNl != -1) {
+ to = lastNl;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = from; i < to; i++) {
+ char c = snippet.charAt(i);
+ if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
+ sb.append(c);
+ }
+ }
+ snippet = sb.toString();
+ }
+ }
+ }
+
+ setSnippet(snippet);
+ }
+
+ /**
+ * Used for deferred snippets from the database. The contents come back as large strings which
+ * need to be extracted for display.
+ *
+ * @param snippet The snippet from the database.
+ * @param query The search query substring.
+ * @param displayName The contact display name.
+ * @return The proper snippet to display.
+ */
+ private String updateSnippet(String snippet, String query, String displayName) {
+
+ if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
+ return null;
+ }
+ query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
+
+ // If the display name already contains the query term, return empty - snippets should
+ // not be needed in that case.
+ if (!TextUtils.isEmpty(displayName)) {
+ final String lowerDisplayName = displayName.toLowerCase();
+ final List<String> nameTokens = split(lowerDisplayName);
+ for (String nameToken : nameTokens) {
+ if (nameToken.startsWith(query)) {
+ return null;
+ }
+ }
+ }
+
+ // The snippet may contain multiple data lines.
+ // Show the first line that matches the query.
+ final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
+
+ if (matched != null && matched.line != null) {
+ // Tokenize for long strings since the match may be at the end of it.
+ // Skip this part for short strings since the whole string will be displayed.
+ // Most contact strings are short so the snippetize method will be called infrequently.
+ final int lengthThreshold =
+ getResources().getInteger(R.integer.snippet_length_before_tokenize);
+ if (matched.line.length() > lengthThreshold) {
+ return snippetize(matched.line, matched.startIndex, lengthThreshold);
+ } else {
+ return matched.line;
+ }
+ }
+
+ // No match found.
+ return null;
+ }
+
+ private String snippetize(String line, int matchIndex, int maxLength) {
+ // Show up to maxLength characters. But we only show full tokens so show the last full token
+ // up to maxLength characters. So as many starting tokens as possible before trying ending
+ // tokens.
+ int remainingLength = maxLength;
+ int tempRemainingLength = remainingLength;
+
+ // Start the end token after the matched query.
+ int index = matchIndex;
+ int endTokenIndex = index;
+
+ // Find the match token first.
+ while (index < line.length()) {
+ if (!Character.isLetterOrDigit(line.charAt(index))) {
+ endTokenIndex = index;
+ remainingLength = tempRemainingLength;
+ break;
+ }
+ tempRemainingLength--;
+ index++;
+ }
+
+ // Find as much content before the match.
+ index = matchIndex - 1;
+ tempRemainingLength = remainingLength;
+ int startTokenIndex = matchIndex;
+ while (index > -1 && tempRemainingLength > 0) {
+ if (!Character.isLetterOrDigit(line.charAt(index))) {
+ startTokenIndex = index;
+ remainingLength = tempRemainingLength;
+ }
+ tempRemainingLength--;
+ index--;
+ }
+
+ index = endTokenIndex;
+ tempRemainingLength = remainingLength;
+ // Find remaining content at after match.
+ while (index < line.length() && tempRemainingLength > 0) {
+ if (!Character.isLetterOrDigit(line.charAt(index))) {
+ endTokenIndex = index;
+ }
+ tempRemainingLength--;
+ index++;
+ }
+ // Append ellipse if there is content before or after.
+ final StringBuilder sb = new StringBuilder();
+ if (startTokenIndex > 0) {
+ sb.append("...");
+ }
+ sb.append(line.substring(startTokenIndex, endTokenIndex));
+ if (endTokenIndex < line.length()) {
+ sb.append("...");
+ }
+ return sb.toString();
+ }
+
+ public void setActivatedStateSupported(boolean flag) {
+ this.mActivatedStateSupported = flag;
+ }
+
+ public void setAdjustSelectionBoundsEnabled(boolean enabled) {
+ mAdjustSelectionBoundsEnabled = enabled;
+ }
+
+ @Override
+ public void requestLayout() {
+ // We will assume that once measured this will not need to resize
+ // itself, so there is no need to pass the layout request to the parent
+ // view (ListView).
+ forceLayout();
+ }
+
+ public void setPhotoPosition(PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+ }
+
+ /**
+ * Set drawable resources directly for the drawable resource of the photo view.
+ *
+ * @param drawableId Id of drawable resource.
+ */
+ public void setDrawableResource(int drawableId) {
+ ImageView photo = getPhotoView();
+ photo.setScaleType(ImageView.ScaleType.CENTER);
+ final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
+ final int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
+ if (CompatUtils.isLollipopCompatible()) {
+ photo.setImageDrawable(drawable);
+ photo.setImageTintList(ColorStateList.valueOf(iconColor));
+ } else {
+ final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
+ DrawableCompat.setTint(drawableWrapper, iconColor);
+ photo.setImageDrawable(drawableWrapper);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final float x = event.getX();
+ final float y = event.getY();
+ // If the touch event's coordinates are not within the view's header, then delegate
+ // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
+ // and ignore the touch event.
+ if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
+ return super.onTouchEvent(event);
+ } else {
+ return true;
+ }
+ }
+
+ private final boolean pointIsInView(float localX, float localY) {
+ return localX >= mLeftOffset
+ && localX < mRightOffset
+ && localY >= 0
+ && localY < (getBottom() - getTop());
+ }
+
+ /**
+ * Where to put contact photo. This affects the other Views' layout or look-and-feel.
+ *
+ * <p>TODO: replace enum with int constants
+ */
+ public enum PhotoPosition {
+ LEFT,
+ RIGHT
+ }
+
+ protected static class HighlightSequence {
+
+ private final int start;
+ private final int end;
+
+ HighlightSequence(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java
new file mode 100644
index 000000000..1f3e2bfe3
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+import com.android.contacts.common.R;
+
+/** A custom view for the pinned section header shown at the top of the contact list. */
+public class ContactListPinnedHeaderView extends TextView {
+
+ public ContactListPinnedHeaderView(Context context, AttributeSet attrs, View parent) {
+ super(context, attrs);
+
+ if (R.styleable.ContactListItemView == null) {
+ return;
+ }
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+ int backgroundColor =
+ a.getColor(R.styleable.ContactListItemView_list_item_background_color, Color.WHITE);
+ int textOffsetTop =
+ a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_text_offset_top, 0);
+ int paddingStartOffset =
+ a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_padding_left, 0);
+ int textWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
+ int widthIncludingPadding = paddingStartOffset + textWidth;
+ a.recycle();
+
+ setBackgroundColor(backgroundColor);
+ setTextAppearance(getContext(), R.style.SectionHeaderStyle);
+ setLayoutParams(new LayoutParams(textWidth, LayoutParams.WRAP_CONTENT));
+ setLayoutDirection(parent.getLayoutDirection());
+ setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
+
+ // Apply text top offset. Multiply by two, because we are implementing this by padding for a
+ // vertically centered view, rather than adjusting the position directly via a layout.
+ setPaddingRelative(
+ 0, getPaddingTop() + (textOffsetTop * 2), getPaddingEnd(), getPaddingBottom());
+ }
+
+ /** Sets section header or makes it invisible if the title is null. */
+ public void setSectionHeaderTitle(String title) {
+ if (!TextUtils.isEmpty(title)) {
+ setText(title);
+ } else {
+ setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactTileView.java b/java/com/android/contacts/common/list/ContactTileView.java
new file mode 100644
index 000000000..9273b0583
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactTileView.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.R;
+
+/** A ContactTile displays a contact's picture and name */
+public abstract class ContactTileView extends FrameLayout {
+
+ private static final String TAG = ContactTileView.class.getSimpleName();
+ protected Listener mListener;
+ private Uri mLookupUri;
+ private ImageView mPhoto;
+ private TextView mName;
+ private ContactPhotoManager mPhotoManager = null;
+
+ public ContactTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mName = (TextView) findViewById(R.id.contact_tile_name);
+ mPhoto = (ImageView) findViewById(R.id.contact_tile_image);
+
+ OnClickListener listener = createClickListener();
+ setOnClickListener(listener);
+ }
+
+ protected OnClickListener createClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onContactSelected(
+ getLookupUri(), MoreContactUtils.getTargetRectFromView(ContactTileView.this));
+ }
+ };
+ }
+
+ public void setPhotoManager(ContactPhotoManager photoManager) {
+ mPhotoManager = photoManager;
+ }
+
+ /**
+ * Populates the data members to be displayed from the fields in {@link
+ * com.android.contacts.common.list.ContactEntry}
+ */
+ public void loadFromContact(ContactEntry entry) {
+
+ if (entry != null) {
+ mName.setText(getNameForView(entry));
+ mLookupUri = entry.lookupUri;
+
+ setVisibility(View.VISIBLE);
+
+ if (mPhotoManager != null) {
+ DefaultImageRequest request = getDefaultImageRequest(entry.namePrimary, entry.lookupKey);
+ configureViewForImage(entry.photoUri == null);
+ if (mPhoto != null) {
+ mPhotoManager.loadPhoto(
+ mPhoto,
+ entry.photoUri,
+ getApproximateImageSize(),
+ isDarkTheme(),
+ isContactPhotoCircular(),
+ request);
+
+
+ }
+ } else {
+ Log.w(TAG, "contactPhotoManager not set");
+ }
+ } else {
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ /**
+ * Returns the string that should actually be displayed as the contact's name. Subclasses can
+ * override this to return formatted versions of the name - i.e. first name only.
+ */
+ protected String getNameForView(ContactEntry contactEntry) {
+ return contactEntry.namePrimary;
+ }
+
+ /**
+ * Implemented by subclasses to estimate the size of the picture. This can return -1 if only a
+ * thumbnail is shown anyway
+ */
+ protected abstract int getApproximateImageSize();
+
+ protected abstract boolean isDarkTheme();
+
+ /**
+ * Implemented by subclasses to reconfigure the view's layout and subviews, based on whether or
+ * not the contact has a user-defined photo.
+ *
+ * @param isDefaultImage True if the contact does not have a user-defined contact photo (which
+ * means a default contact image will be applied by the {@link ContactPhotoManager}
+ */
+ protected void configureViewForImage(boolean isDefaultImage) {
+ // No-op by default.
+ }
+
+ /**
+ * Implemented by subclasses to allow them to return a {@link DefaultImageRequest} with the
+ * various image parameters defined to match their own layouts.
+ *
+ * @param displayName The display name of the contact
+ * @param lookupKey The lookup key of the contact
+ * @return A {@link DefaultImageRequest} object with each field configured by the subclass as
+ * desired, or {@code null}.
+ */
+ protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) {
+ return new DefaultImageRequest(displayName, lookupKey, isContactPhotoCircular());
+ }
+
+ /**
+ * Whether contact photo should be displayed as a circular image. Implemented by subclasses so
+ * they can change which drawables to fetch.
+ */
+ protected boolean isContactPhotoCircular() {
+ return true;
+ }
+
+ public interface Listener {
+
+ /** Notification that the contact was selected; no specific action is dictated. */
+ void onContactSelected(Uri contactLookupUri, Rect viewRect);
+
+ /** Notification that the specified number is to be called. */
+ void onCallNumberDirectly(String phoneNumber);
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactsSectionIndexer.java b/java/com/android/contacts/common/list/ContactsSectionIndexer.java
new file mode 100644
index 000000000..3f0f2b7ee
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactsSectionIndexer.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.text.TextUtils;
+import android.widget.SectionIndexer;
+import java.util.Arrays;
+
+/**
+ * A section indexer that is configured with precomputed section titles and their respective counts.
+ */
+public class ContactsSectionIndexer implements SectionIndexer {
+
+ private static final String BLANK_HEADER_STRING = " ";
+ private String[] mSections;
+ private int[] mPositions;
+ private int mCount;
+
+ /**
+ * Constructor.
+ *
+ * @param sections a non-null array
+ * @param counts a non-null array of the same size as <code>sections</code>
+ */
+ public ContactsSectionIndexer(String[] sections, int[] counts) {
+ if (sections == null || counts == null) {
+ throw new NullPointerException();
+ }
+
+ if (sections.length != counts.length) {
+ throw new IllegalArgumentException(
+ "The sections and counts arrays must have the same length");
+ }
+
+ // TODO process sections/counts based on current locale and/or specific section titles
+
+ this.mSections = sections;
+ mPositions = new int[counts.length];
+ int position = 0;
+ for (int i = 0; i < counts.length; i++) {
+ if (TextUtils.isEmpty(mSections[i])) {
+ mSections[i] = BLANK_HEADER_STRING;
+ } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
+ mSections[i] = mSections[i].trim();
+ }
+
+ mPositions[i] = position;
+ position += counts[i];
+ }
+ mCount = position;
+ }
+
+ public Object[] getSections() {
+ return mSections;
+ }
+
+ public int getPositionForSection(int section) {
+ if (section < 0 || section >= mSections.length) {
+ return -1;
+ }
+
+ return mPositions[section];
+ }
+
+ public int getSectionForPosition(int position) {
+ if (position < 0 || position >= mCount) {
+ return -1;
+ }
+
+ int index = Arrays.binarySearch(mPositions, position);
+
+ /*
+ * Consider this example: section positions are 0, 3, 5; the supplied
+ * position is 4. The section corresponding to position 4 starts at
+ * position 3, so the expected return value is 1. Binary search will not
+ * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+ * To get from that number to the expected value of 1 we need to negate
+ * and subtract 2.
+ */
+ return index >= 0 ? index : -index - 2;
+ }
+
+ public void setProfileAndFavoritesHeader(String header, int numberOfItemsToAdd) {
+ if (mSections != null) {
+ // Don't do anything if the header is already set properly.
+ if (mSections.length > 0 && header.equals(mSections[0])) {
+ return;
+ }
+
+ // Since the section indexer isn't aware of the profile at the top, we need to add a
+ // special section at the top for it and shift everything else down.
+ String[] tempSections = new String[mSections.length + 1];
+ int[] tempPositions = new int[mPositions.length + 1];
+ tempSections[0] = header;
+ tempPositions[0] = 0;
+ for (int i = 1; i <= mPositions.length; i++) {
+ tempSections[i] = mSections[i - 1];
+ tempPositions[i] = mPositions[i - 1] + numberOfItemsToAdd;
+ }
+ mSections = tempSections;
+ mPositions = tempPositions;
+ mCount = mCount + numberOfItemsToAdd;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/DefaultContactListAdapter.java b/java/com/android/contacts/common/list/DefaultContactListAdapter.java
new file mode 100644
index 000000000..7bcae0e0e
--- /dev/null
+++ b/java/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippets;
+import android.text.TextUtils;
+import android.view.View;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. */
+public class DefaultContactListAdapter extends ContactListAdapter {
+
+ public DefaultContactListAdapter(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ String sortOrder = null;
+ if (isSearchMode()) {
+ String query = getQueryString();
+ if (query == null) {
+ query = "";
+ }
+ query = query.trim();
+ if (TextUtils.isEmpty(query)) {
+ // Regardless of the directory, we don't want anything returned,
+ // so let's just send a "nothing" query to the local directory.
+ loader.setUri(Contacts.CONTENT_URI);
+ loader.setProjection(getProjection(false));
+ loader.setSelection("0");
+ } else {
+ final Builder builder = ContactsCompat.getContentUri().buildUpon();
+ appendSearchParameters(builder, query, directoryId);
+ loader.setUri(builder.build());
+ loader.setProjection(getProjection(true));
+ }
+ } else {
+ final ContactListFilter filter = getFilter();
+ configureUri(loader, directoryId, filter);
+ loader.setProjection(getProjection(false));
+ configureSelection(loader, directoryId, filter);
+ }
+
+ if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+ if (sortOrder == null) {
+ sortOrder = Contacts.SORT_KEY_PRIMARY;
+ } else {
+ sortOrder += ", " + Contacts.SORT_KEY_PRIMARY;
+ }
+ } else {
+ if (sortOrder == null) {
+ sortOrder = Contacts.SORT_KEY_ALTERNATIVE;
+ } else {
+ sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE;
+ }
+ }
+ loader.setSortOrder(sortOrder);
+ }
+
+ private void appendSearchParameters(Builder builder, String query, long directoryId) {
+ builder.appendPath(query); // Builder will encode the query
+ builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
+ builder.appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY,
+ String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+ }
+ builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1");
+ }
+
+ protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
+ Uri uri = Contacts.CONTENT_URI;
+
+ if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) {
+ uri = ContactListAdapter.buildSectionIndexerUri(uri);
+ }
+
+ // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT
+ if (filter != null
+ && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM
+ && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+ final Uri.Builder builder = uri.buildUpon();
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ filter.addAccountQueryParameterToUrl(builder);
+ }
+ uri = builder.build();
+ }
+
+ loader.setUri(uri);
+ }
+
+ private void configureSelection(CursorLoader loader, long directoryId, ContactListFilter filter) {
+ if (filter == null) {
+ return;
+ }
+
+ if (directoryId != Directory.DEFAULT) {
+ return;
+ }
+
+ StringBuilder selection = new StringBuilder();
+ List<String> selectionArgs = new ArrayList<String>();
+
+ switch (filter.filterType) {
+ case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
+ {
+ // We have already added directory=0 to the URI, which takes care of this
+ // filter
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT:
+ {
+ // We have already added the lookup key to the URI, which takes care of this
+ // filter
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_STARRED:
+ {
+ selection.append(Contacts.STARRED + "!=0");
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ {
+ selection.append(Contacts.HAS_PHONE_NUMBER + "=1");
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_CUSTOM:
+ {
+ selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+ if (isCustomFilterForPhoneNumbersOnly()) {
+ selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+ }
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ACCOUNT:
+ {
+ // We use query parameters for account filter, so no selection to add here.
+ break;
+ }
+ }
+ loader.setSelection(selection.toString());
+ loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+ final ContactListItemView view = (ContactListItemView) itemView;
+
+ view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+
+ bindSectionHeaderAndDivider(view, position, cursor);
+
+ if (isQuickContactEnabled()) {
+ bindQuickContact(
+ view,
+ partition,
+ cursor,
+ ContactQuery.CONTACT_PHOTO_ID,
+ ContactQuery.CONTACT_PHOTO_URI,
+ ContactQuery.CONTACT_ID,
+ ContactQuery.CONTACT_LOOKUP_KEY,
+ ContactQuery.CONTACT_DISPLAY_NAME);
+ } else {
+ if (getDisplayPhotos()) {
+ bindPhoto(view, partition, cursor);
+ }
+ }
+
+ bindNameAndViewId(view, cursor);
+ bindPresenceAndStatusMessage(view, cursor);
+
+ if (isSearchMode()) {
+ bindSearchSnippet(view, cursor);
+ } else {
+ view.setSnippet(null);
+ }
+ }
+
+ private boolean isCustomFilterForPhoneNumbersOnly() {
+ // TODO: this flag should not be stored in shared prefs. It needs to be in the db.
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+ return prefs.getBoolean(
+ ContactsPreferences.PREF_DISPLAY_ONLY_PHONES,
+ ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT);
+ }
+}
diff --git a/java/com/android/contacts/common/list/DirectoryListLoader.java b/java/com/android/contacts/common/list/DirectoryListLoader.java
new file mode 100644
index 000000000..48b098c07
--- /dev/null
+++ b/java/com/android/contacts/common/list/DirectoryListLoader.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.DirectoryCompat;
+
+/** A specialized loader for the list of directories, see {@link Directory}. */
+public class DirectoryListLoader extends AsyncTaskLoader<Cursor> {
+
+ public static final int SEARCH_MODE_NONE = 0;
+ public static final int SEARCH_MODE_DEFAULT = 1;
+ public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2;
+ public static final int SEARCH_MODE_DATA_SHORTCUT = 3;
+ // This is a virtual column created for a MatrixCursor.
+ public static final String DIRECTORY_TYPE = "directoryType";
+ private static final String TAG = "ContactEntryListAdapter";
+ private static final String[] RESULT_PROJECTION = {
+ Directory._ID, DIRECTORY_TYPE, Directory.DISPLAY_NAME, Directory.PHOTO_SUPPORT,
+ };
+ private final ContentObserver mObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ forceLoad();
+ }
+ };
+ private int mDirectorySearchMode;
+ private boolean mLocalInvisibleDirectoryEnabled;
+ private MatrixCursor mDefaultDirectoryList;
+
+ public DirectoryListLoader(Context context) {
+ super(context);
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ /**
+ * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be
+ * included in the results.
+ */
+ public void setLocalInvisibleDirectoryEnabled(boolean flag) {
+ this.mLocalInvisibleDirectoryEnabled = flag;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ getContext().getContentResolver().registerContentObserver(DirectoryQuery.URI, false, mObserver);
+ forceLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ if (mDirectorySearchMode == SEARCH_MODE_NONE) {
+ return getDefaultDirectories();
+ }
+
+ MatrixCursor result = new MatrixCursor(RESULT_PROJECTION);
+ Context context = getContext();
+ PackageManager pm = context.getPackageManager();
+ String selection;
+ switch (mDirectorySearchMode) {
+ case SEARCH_MODE_DEFAULT:
+ selection = null;
+ break;
+
+ case SEARCH_MODE_CONTACT_SHORTCUT:
+ selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL;
+ break;
+
+ case SEARCH_MODE_DATA_SHORTCUT:
+ selection =
+ Directory.SHORTCUT_SUPPORT
+ + " IN ("
+ + Directory.SHORTCUT_SUPPORT_FULL
+ + ", "
+ + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY
+ + ")";
+ break;
+
+ default:
+ throw new RuntimeException("Unsupported directory search mode: " + mDirectorySearchMode);
+ }
+ Cursor cursor = null;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(
+ DirectoryQuery.URI,
+ DirectoryQuery.PROJECTION,
+ selection,
+ null,
+ DirectoryQuery.ORDER_BY);
+
+ if (cursor == null) {
+ return result;
+ }
+
+ while (cursor.moveToNext()) {
+ long directoryId = cursor.getLong(DirectoryQuery.ID);
+ if (!mLocalInvisibleDirectoryEnabled && DirectoryCompat.isInvisibleDirectory(directoryId)) {
+ continue;
+ }
+ String directoryType = null;
+
+ String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) {
+ try {
+ directoryType = pm.getResourcesForApplication(packageName).getString(typeResourceId);
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot obtain directory type from package: " + packageName);
+ }
+ }
+ String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+ int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
+ result.addRow(new Object[] {directoryId, directoryType, displayName, photoSupport});
+ }
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Runtime Exception when querying directory");
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return result;
+ }
+
+ private Cursor getDefaultDirectories() {
+ if (mDefaultDirectoryList == null) {
+ mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION);
+ mDefaultDirectoryList.addRow(
+ new Object[] {Directory.DEFAULT, getContext().getString(R.string.contactsList), null});
+ mDefaultDirectoryList.addRow(
+ new Object[] {
+ Directory.LOCAL_INVISIBLE,
+ getContext().getString(R.string.local_invisible_directory),
+ null
+ });
+ }
+ return mDefaultDirectoryList;
+ }
+
+ @Override
+ protected void onReset() {
+ stopLoading();
+ }
+
+ private static final class DirectoryQuery {
+
+ public static final Uri URI = DirectoryCompat.getContentUri();
+ public static final String ORDER_BY = Directory._ID;
+
+ public static final String[] PROJECTION = {
+ Directory._ID,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.DISPLAY_NAME,
+ Directory.PHOTO_SUPPORT,
+ };
+
+ public static final int ID = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int DISPLAY_NAME = 3;
+ public static final int PHOTO_SUPPORT = 4;
+ }
+}
diff --git a/java/com/android/contacts/common/list/DirectoryPartition.java b/java/com/android/contacts/common/list/DirectoryPartition.java
new file mode 100644
index 000000000..26b851041
--- /dev/null
+++ b/java/com/android/contacts/common/list/DirectoryPartition.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.provider.ContactsContract.Directory;
+import com.android.common.widget.CompositeCursorAdapter;
+
+/** Model object for a {@link Directory} row. */
+public final class DirectoryPartition extends CompositeCursorAdapter.Partition {
+
+ public static final int STATUS_NOT_LOADED = 0;
+ public static final int STATUS_LOADING = 1;
+ public static final int STATUS_LOADED = 2;
+
+ public static final int RESULT_LIMIT_DEFAULT = -1;
+
+ private long mDirectoryId;
+ private String mContentUri;
+ private String mDirectoryType;
+ private String mDisplayName;
+ private int mStatus;
+ private boolean mPriorityDirectory;
+ private boolean mPhotoSupported;
+ private int mResultLimit = RESULT_LIMIT_DEFAULT;
+ private boolean mDisplayNumber = true;
+
+ private String mLabel;
+
+ public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) {
+ super(showIfEmpty, hasHeader);
+ }
+
+ /** Directory ID, see {@link Directory}. */
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public void setDirectoryId(long directoryId) {
+ this.mDirectoryId = directoryId;
+ }
+
+ /**
+ * Directory type resolved from {@link Directory#PACKAGE_NAME} and {@link
+ * Directory#TYPE_RESOURCE_ID};
+ */
+ public String getDirectoryType() {
+ return mDirectoryType;
+ }
+
+ public void setDirectoryType(String directoryType) {
+ this.mDirectoryType = directoryType;
+ }
+
+ /** See {@link Directory#DISPLAY_NAME}. */
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.mDisplayName = displayName;
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+
+ public void setStatus(int status) {
+ mStatus = status;
+ }
+
+ public boolean isLoading() {
+ return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING;
+ }
+
+ /** Returns true if this directory should be loaded before non-priority directories. */
+ public boolean isPriorityDirectory() {
+ return mPriorityDirectory;
+ }
+
+ public void setPriorityDirectory(boolean priorityDirectory) {
+ mPriorityDirectory = priorityDirectory;
+ }
+
+ /** Returns true if this directory supports photos. */
+ public boolean isPhotoSupported() {
+ return mPhotoSupported;
+ }
+
+ public void setPhotoSupported(boolean flag) {
+ this.mPhotoSupported = flag;
+ }
+
+ /**
+ * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which
+ * implies using the adapter's {@link
+ * com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()}
+ */
+ public int getResultLimit() {
+ return mResultLimit;
+ }
+
+ public void setResultLimit(int resultLimit) {
+ mResultLimit = resultLimit;
+ }
+
+ /**
+ * Used by extended directories to specify a custom content URI. Extended directories MUST have a
+ * content URI
+ */
+ public String getContentUri() {
+ return mContentUri;
+ }
+
+ public void setContentUri(String contentUri) {
+ mContentUri = contentUri;
+ }
+
+ /** A label to display in the header next to the display name. */
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ mLabel = label;
+ }
+
+ @Override
+ public String toString() {
+ return "DirectoryPartition{"
+ + "mDirectoryId="
+ + mDirectoryId
+ + ", mContentUri='"
+ + mContentUri
+ + '\''
+ + ", mDirectoryType='"
+ + mDirectoryType
+ + '\''
+ + ", mDisplayName='"
+ + mDisplayName
+ + '\''
+ + ", mStatus="
+ + mStatus
+ + ", mPriorityDirectory="
+ + mPriorityDirectory
+ + ", mPhotoSupported="
+ + mPhotoSupported
+ + ", mResultLimit="
+ + mResultLimit
+ + ", mLabel='"
+ + mLabel
+ + '\''
+ + '}';
+ }
+
+ /**
+ * Whether or not to display the phone number in app that have that option - Dialer. If false,
+ * Phone Label should be used instead of Phone Number.
+ */
+ public boolean isDisplayNumber() {
+ return mDisplayNumber;
+ }
+
+ public void setDisplayNumber(boolean displayNumber) {
+ mDisplayNumber = displayNumber;
+ }
+}
diff --git a/java/com/android/contacts/common/list/IndexerListAdapter.java b/java/com/android/contacts/common/list/IndexerListAdapter.java
new file mode 100644
index 000000000..2289f6e59
--- /dev/null
+++ b/java/com/android/contacts/common/list/IndexerListAdapter.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+
+/** A list adapter that supports section indexer and a pinned header. */
+public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
+
+ protected Context mContext;
+ private SectionIndexer mIndexer;
+ private int mIndexedPartition = 0;
+ private boolean mSectionHeaderDisplayEnabled;
+ private View mHeader;
+ private Placement mPlacementCache = new Placement();
+
+ /** Constructor. */
+ public IndexerListAdapter(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ /**
+ * Creates a section header view that will be pinned at the top of the list as the user scrolls.
+ */
+ protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent);
+
+ /** Sets the title in the pinned header as the user scrolls. */
+ protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title);
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ this.mSectionHeaderDisplayEnabled = flag;
+ }
+
+ public int getIndexedPartition() {
+ return mIndexedPartition;
+ }
+
+ public void setIndexedPartition(int partition) {
+ this.mIndexedPartition = partition;
+ }
+
+ public SectionIndexer getIndexer() {
+ return mIndexer;
+ }
+
+ public void setIndexer(SectionIndexer indexer) {
+ mIndexer = indexer;
+ mPlacementCache.invalidate();
+ }
+
+ public Object[] getSections() {
+ if (mIndexer == null) {
+ return new String[] {" "};
+ } else {
+ return mIndexer.getSections();
+ }
+ }
+
+ /** @return relative position of the section in the indexed partition */
+ public int getPositionForSection(int sectionIndex) {
+ if (mIndexer == null) {
+ return -1;
+ }
+
+ return mIndexer.getPositionForSection(sectionIndex);
+ }
+
+ /** @param position relative position in the indexed partition */
+ public int getSectionForPosition(int position) {
+ if (mIndexer == null) {
+ return -1;
+ }
+
+ return mIndexer.getSectionForPosition(position);
+ }
+
+ @Override
+ public int getPinnedHeaderCount() {
+ if (isSectionHeaderDisplayEnabled()) {
+ return super.getPinnedHeaderCount() + 1;
+ } else {
+ return super.getPinnedHeaderCount();
+ }
+ }
+
+ @Override
+ public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
+ if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
+ if (mHeader == null) {
+ mHeader = createPinnedSectionHeaderView(mContext, parent);
+ }
+ return mHeader;
+ } else {
+ return super.getPinnedHeaderView(viewIndex, convertView, parent);
+ }
+ }
+
+ @Override
+ public void configurePinnedHeaders(PinnedHeaderListView listView) {
+ super.configurePinnedHeaders(listView);
+
+ if (!isSectionHeaderDisplayEnabled()) {
+ return;
+ }
+
+ int index = getPinnedHeaderCount() - 1;
+ if (mIndexer == null || getCount() == 0) {
+ listView.setHeaderInvisible(index, false);
+ } else {
+ int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight());
+ int position = listPosition - listView.getHeaderViewsCount();
+
+ int section = -1;
+ int partition = getPartitionForPosition(position);
+ if (partition == mIndexedPartition) {
+ int offset = getOffsetInPartition(position);
+ if (offset != -1) {
+ section = getSectionForPosition(offset);
+ }
+ }
+
+ if (section == -1) {
+ listView.setHeaderInvisible(index, false);
+ } else {
+ View topChild = listView.getChildAt(listPosition);
+ if (topChild != null) {
+ // Match the pinned header's height to the height of the list item.
+ mHeader.setMinimumHeight(topChild.getMeasuredHeight());
+ }
+ setPinnedSectionTitle(mHeader, (String) mIndexer.getSections()[section]);
+
+ // Compute the item position where the current partition begins
+ int partitionStart = getPositionForPartition(mIndexedPartition);
+ if (hasHeader(mIndexedPartition)) {
+ partitionStart++;
+ }
+
+ // Compute the item position where the next section begins
+ int nextSectionPosition = partitionStart + getPositionForSection(section + 1);
+ boolean isLastInSection = position == nextSectionPosition - 1;
+ listView.setFadingHeader(index, listPosition, isLastInSection);
+ }
+ }
+ }
+
+ /**
+ * Computes the item's placement within its section and populates the {@code placement} object
+ * accordingly. Please note that the returned object is volatile and should be copied if the
+ * result needs to be used later.
+ */
+ public Placement getItemPlacementInSection(int position) {
+ if (mPlacementCache.position == position) {
+ return mPlacementCache;
+ }
+
+ mPlacementCache.position = position;
+ if (isSectionHeaderDisplayEnabled()) {
+ int section = getSectionForPosition(position);
+ if (section != -1 && getPositionForSection(section) == position) {
+ mPlacementCache.firstInSection = true;
+ mPlacementCache.sectionHeader = (String) getSections()[section];
+ } else {
+ mPlacementCache.firstInSection = false;
+ mPlacementCache.sectionHeader = null;
+ }
+
+ mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position);
+ } else {
+ mPlacementCache.firstInSection = false;
+ mPlacementCache.lastInSection = false;
+ mPlacementCache.sectionHeader = null;
+ }
+ return mPlacementCache;
+ }
+
+ /**
+ * An item view is displayed differently depending on whether it is placed at the beginning,
+ * middle or end of a section. It also needs to know the section header when it is at the
+ * beginning of a section. This object captures all this configuration.
+ */
+ public static final class Placement {
+
+ public boolean firstInSection;
+ public boolean lastInSection;
+ public String sectionHeader;
+ private int position = ListView.INVALID_POSITION;
+
+ public void invalidate() {
+ position = ListView.INVALID_POSITION;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
new file mode 100644
index 000000000..89bd889e6
--- /dev/null
+++ b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.app.ActionBar;
+import android.net.Uri;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+
+/** Action callbacks that can be sent by a phone number picker. */
+public interface OnPhoneNumberPickerActionListener {
+
+ int CALL_INITIATION_UNKNOWN = 0;
+
+ /** Returns the selected phone number uri to the requester. */
+ void onPickDataUri(Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData);
+
+ /**
+ * Returns the specified phone number to the requester. May call the specified phone number,
+ * either as an audio or video call.
+ */
+ void onPickPhoneNumber(
+ String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData);
+
+ /** Called when home menu in {@link ActionBar} is clicked by the user. */
+ void onHomeInActionBarSelected();
+}
diff --git a/java/com/android/contacts/common/list/PhoneNumberListAdapter.java b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java
new file mode 100644
index 000000000..c7b24229f
--- /dev/null
+++ b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CallableCompat;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.compat.PhoneCompat;
+import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.Constants;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.CallUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and {@link
+ * SipAddress#CONTENT_ITEM_TYPE}.
+ *
+ * <p>By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)}
+ * is called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
+ * API instead of {@link Phone}.
+ */
+public class PhoneNumberListAdapter extends ContactEntryListAdapter {
+
+ private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
+ private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000";
+ // A list of extended directories to add to the directories from the database
+ private final List<DirectoryPartition> mExtendedDirectories;
+ private final CharSequence mUnknownNameText;
+ // Extended directories will have ID's that are higher than any of the id's from the database,
+ // so that we can identify them and set them up properly. If no extended directories
+ // exist, this will be Long.MAX_VALUE
+ private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
+ private ContactListItemView.PhotoPosition mPhotoPosition;
+ private boolean mUseCallableUri;
+ private Listener mListener;
+ private boolean mIsVideoEnabled;
+ private boolean mIsPresenceEnabled;
+
+ public PhoneNumberListAdapter(Context context) {
+ super(context);
+ setDefaultFilterHeaderText(R.string.list_filter_phones);
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+
+ mExtendedDirectories =
+ PhoneDirectoryExtenderAccessor.get(mContext).getExtendedDirectories(mContext);
+
+ int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
+ mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
+ mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ String query = getQueryString();
+ if (query == null) {
+ query = "";
+ }
+ if (isExtendedDirectory(directoryId)) {
+ final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
+ final String contentUri = directory.getContentUri();
+ if (contentUri == null) {
+ throw new IllegalStateException("Extended directory must have a content URL: " + directory);
+ }
+ final Builder builder = Uri.parse(contentUri).buildUpon();
+ builder.appendPath(query);
+ builder.appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY, String.valueOf(getDirectoryResultLimit(directory)));
+ loader.setUri(builder.build());
+ loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+ } else {
+ final boolean isRemoteDirectoryQuery = DirectoryCompat.isRemoteDirectoryId(directoryId);
+ final Builder builder;
+ if (isSearchMode()) {
+ final Uri baseUri;
+ if (isRemoteDirectoryQuery) {
+ baseUri = PhoneCompat.getContentFilterUri();
+ } else if (mUseCallableUri) {
+ baseUri = CallableCompat.getContentFilterUri();
+ } else {
+ baseUri = PhoneCompat.getContentFilterUri();
+ }
+ builder = baseUri.buildUpon();
+ builder.appendPath(query); // Builder will encode the query
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ if (isRemoteDirectoryQuery) {
+ builder.appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY,
+ String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+ }
+ } else {
+ Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
+ builder =
+ baseUri
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+ if (isSectionHeaderDisplayEnabled()) {
+ builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
+ }
+ applyFilter(loader, builder, directoryId, getFilter());
+ }
+
+ // Ignore invalid phone numbers that are too long. These can potentially cause freezes
+ // in the UI and there is no reason to display them.
+ final String prevSelection = loader.getSelection();
+ final String newSelection;
+ if (!TextUtils.isEmpty(prevSelection)) {
+ newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE;
+ } else {
+ newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE;
+ }
+ loader.setSelection(newSelection);
+
+ // Remove duplicates when it is possible.
+ builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
+ loader.setUri(builder.build());
+
+ // TODO a projection that includes the search snippet
+ if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+ } else {
+ loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
+ }
+
+ if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+ loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+ } else {
+ loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+ }
+ }
+ }
+
+ protected boolean isExtendedDirectory(long directoryId) {
+ return directoryId >= mFirstExtendedDirectoryId;
+ }
+
+ private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
+ final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
+ return mExtendedDirectories.get(directoryIndex);
+ }
+
+ /**
+ * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
+ * filter}.
+ */
+ private void applyFilter(
+ CursorLoader loader, Uri.Builder uriBuilder, long directoryId, ContactListFilter filter) {
+ if (filter == null || directoryId != Directory.DEFAULT) {
+ return;
+ }
+
+ final StringBuilder selection = new StringBuilder();
+ final List<String> selectionArgs = new ArrayList<String>();
+
+ switch (filter.filterType) {
+ case ContactListFilter.FILTER_TYPE_CUSTOM:
+ {
+ selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+ selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ACCOUNT:
+ {
+ filter.addAccountQueryParameterToUrl(uriBuilder);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
+ case ContactListFilter.FILTER_TYPE_DEFAULT:
+ break; // No selection needed.
+ case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ break; // This adapter is always "phone only", so no selection needed either.
+ default:
+ Log.w(
+ TAG,
+ "Unsupported filter type came "
+ + "(type: "
+ + filter.filterType
+ + ", toString: "
+ + filter
+ + ")"
+ + " showing all contacts.");
+ // No selection.
+ break;
+ }
+ loader.setSelection(selection.toString());
+ loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+ }
+
+ public String getPhoneNumber(int position) {
+ final Cursor item = (Cursor) getItem(position);
+ return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
+ }
+
+ /**
+ * Retrieves the lookup key for the given cursor position.
+ *
+ * @param position The cursor position.
+ * @return The lookup key.
+ */
+ public String getLookupKey(int position) {
+ final Cursor item = (Cursor) getItem(position);
+ return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null;
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setQuickContactEnabled(isQuickContactEnabled());
+ view.setPhotoPosition(mPhotoPosition);
+ return view;
+ }
+
+ protected void setHighlight(ContactListItemView view, Cursor cursor) {
+ view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+ ContactListItemView view = (ContactListItemView) itemView;
+
+ setHighlight(view, cursor);
+
+ // Look at elements before and after this position, checking if contact IDs are same.
+ // If they have one same contact ID, it means they can be grouped.
+ //
+ // In one group, only the first entry will show its photo and its name, and the other
+ // entries in the group show just their data (e.g. phone number, email address).
+ cursor.moveToPosition(position);
+ boolean isFirstEntry = true;
+ final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+ if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
+ final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+ if (currentContactId == previousContactId) {
+ isFirstEntry = false;
+ }
+ }
+ cursor.moveToPosition(position);
+
+ bindViewId(view, cursor, PhoneQuery.PHONE_ID);
+
+ bindSectionHeaderAndDivider(view, position);
+ if (isFirstEntry) {
+ bindName(view, cursor);
+ if (isQuickContactEnabled()) {
+ bindQuickContact(
+ view,
+ partition,
+ cursor,
+ PhoneQuery.PHOTO_ID,
+ PhoneQuery.PHOTO_URI,
+ PhoneQuery.CONTACT_ID,
+ PhoneQuery.LOOKUP_KEY,
+ PhoneQuery.DISPLAY_NAME);
+ } else {
+ if (getDisplayPhotos()) {
+ bindPhoto(view, partition, cursor);
+ }
+ }
+ } else {
+ unbindName(view);
+
+ view.removePhotoView(true, false);
+ }
+
+ final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
+
+ // If the first partition does not have a header, then all subsequent partitions'
+ // getPositionForPartition returns an index off by 1.
+ int partitionOffset = 0;
+ if (partition > 0 && !getPartition(0).getHasHeader()) {
+ partitionOffset = 1;
+ }
+ position += getPositionForPartition(partition) + partitionOffset;
+
+ bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position);
+ }
+
+ protected void bindPhoneNumber(
+ ContactListItemView view, Cursor cursor, boolean displayNumber, int position) {
+ CharSequence label = null;
+ if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
+ final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
+ final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+
+ // TODO cache
+ label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+ }
+ view.setLabel(label);
+ final String text;
+ if (displayNumber) {
+ text = cursor.getString(PhoneQuery.PHONE_NUMBER);
+ } else {
+ // Display phone label. If that's null, display geocoded location for the number
+ final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+ if (phoneLabel != null) {
+ text = phoneLabel;
+ } else {
+ final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
+ text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
+ }
+ }
+ view.setPhoneNumber(text);
+
+ if (CompatUtils.isVideoCompatible()) {
+ // Determine if carrier presence indicates the number supports video calling.
+ int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE);
+ boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
+
+ boolean isVideoIconShown = mIsVideoEnabled && (!mIsPresenceEnabled || isPresent);
+ view.setShowVideoCallIcon(isVideoIconShown, mListener, position);
+ }
+ }
+
+ protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+ if (isSectionHeaderDisplayEnabled()) {
+ Placement placement = getItemPlacementInSection(position);
+ view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
+ } else {
+ view.setSectionHeader(null);
+ }
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME);
+ // Note: we don't show phonetic names any more (see issue 5265330)
+ }
+
+ protected void unbindName(final ContactListItemView view) {
+ view.hideDisplayName();
+ }
+
+ @Override
+ protected void bindWorkProfileIcon(final ContactListItemView view, int partition) {
+ final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
+ final long directoryId = directory.getDirectoryId();
+ final long userType = ContactsUtils.determineUserType(directoryId, null);
+ // Work directory must not be a extended directory. An extended directory is custom
+ // directory in the app, but not a directory provided by framework. So it can't be
+ // USER_TYPE_WORK.
+ view.setWorkProfileIconEnabled(
+ !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK);
+ }
+
+ protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+ if (!isPhotoSupported(partitionIndex)) {
+ view.removePhotoView();
+ return;
+ }
+
+ long photoId = 0;
+ if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
+ photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
+ }
+
+ if (photoId != 0) {
+ getPhotoLoader()
+ .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null);
+ } else {
+ final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
+ final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+
+ DefaultImageRequest request = null;
+ if (photoUri == null) {
+ final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
+ final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY);
+ request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos());
+ }
+ getPhotoLoader()
+ .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request);
+ }
+ }
+
+ public ContactListItemView.PhotoPosition getPhotoPosition() {
+ return mPhotoPosition;
+ }
+
+ public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+ }
+
+ public void setUseCallableUri(boolean useCallableUri) {
+ mUseCallableUri = useCallableUri;
+ }
+
+ /**
+ * Override base implementation to inject extended directories between local & remote directories.
+ * This is done in the following steps: 1. Call base implementation to add directories from the
+ * cursor. 2. Iterate all base directories and establish the following information: a. The highest
+ * directory id so that we can assign unused id's to the extended directories. b. The index of the
+ * last non-remote directory. This is where we will insert extended directories. 3. Iterate the
+ * extended directories and for each one, assign an ID and insert it in the proper location.
+ */
+ @Override
+ public void changeDirectories(Cursor cursor) {
+ super.changeDirectories(cursor);
+ if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
+ return;
+ }
+ final int numExtendedDirectories = mExtendedDirectories.size();
+ if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
+ // already added all directories;
+ return;
+ }
+ //
+ mFirstExtendedDirectoryId = Long.MAX_VALUE;
+ if (numExtendedDirectories > 0) {
+ // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
+ // "special" ID.
+ long maxId = Directory.LOCAL_INVISIBLE;
+ int insertIndex = 0;
+ for (int i = 0, n = getPartitionCount(); i < n; i++) {
+ final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+ final long id = partition.getDirectoryId();
+ if (id > maxId) {
+ maxId = id;
+ }
+ if (!DirectoryCompat.isRemoteDirectoryId(id)) {
+ // assuming remote directories come after local, we will end up with the index
+ // where we should insert extended directories. This also works if there are no
+ // remote directories at all.
+ insertIndex = i + 1;
+ }
+ }
+ // Extended directories ID's cannot collide with base directories
+ mFirstExtendedDirectoryId = maxId + 1;
+ for (int i = 0; i < numExtendedDirectories; i++) {
+ final long id = mFirstExtendedDirectoryId + i;
+ final DirectoryPartition directory = mExtendedDirectories.get(i);
+ if (getPartitionByDirectoryId(id) == -1) {
+ addPartition(insertIndex, directory);
+ directory.setDirectoryId(id);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Uri getContactUri(
+ int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) {
+ final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
+ final long directoryId = directory.getDirectoryId();
+ if (!isExtendedDirectory(directoryId)) {
+ return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
+ }
+ return Contacts.CONTENT_LOOKUP_URI
+ .buildUpon()
+ .appendPath(Constants.LOOKUP_URI_ENCODED)
+ .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
+ .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
+ .encodedFragment(cursor.getString(lookUpKeyColumn))
+ .build();
+ }
+
+ public Listener getListener() {
+ return mListener;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public interface Listener {
+
+ void onVideoCallIconClicked(int position);
+ }
+
+ public static class PhoneQuery {
+
+ /**
+ * Optional key used as part of a JSON lookup key to specify an analytics category associated
+ * with the row.
+ */
+ public static final String ANALYTICS_CATEGORY = "analytics_category";
+
+ /**
+ * Optional key used as part of a JSON lookup key to specify an analytics action associated with
+ * the row.
+ */
+ public static final String ANALYTICS_ACTION = "analytics_action";
+
+ /**
+ * Optional key used as part of a JSON lookup key to specify an analytics value associated with
+ * the row.
+ */
+ public static final String ANALYTICS_VALUE = "analytics_value";
+
+ public static final String[] PROJECTION_PRIMARY_INTERNAL =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.PHOTO_ID, // 6
+ Phone.DISPLAY_NAME_PRIMARY, // 7
+ Phone.PHOTO_THUMBNAIL_URI, // 8
+ };
+
+ public static final String[] PROJECTION_PRIMARY;
+ public static final String[] PROJECTION_ALTERNATIVE_INTERNAL =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.PHOTO_ID, // 6
+ Phone.DISPLAY_NAME_ALTERNATIVE, // 7
+ Phone.PHOTO_THUMBNAIL_URI, // 8
+ };
+ public static final String[] PROJECTION_ALTERNATIVE;
+ public static final int PHONE_ID = 0;
+ public static final int PHONE_TYPE = 1;
+ public static final int PHONE_LABEL = 2;
+ public static final int PHONE_NUMBER = 3;
+ public static final int CONTACT_ID = 4;
+ public static final int LOOKUP_KEY = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int DISPLAY_NAME = 7;
+ public static final int PHOTO_URI = 8;
+ public static final int CARRIER_PRESENCE = 9;
+
+ static {
+ final List<String> projectionList =
+ new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL));
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Phone.CARRIER_PRESENCE); // 9
+ }
+ PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ static {
+ final List<String> projectionList =
+ new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL));
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Phone.CARRIER_PRESENCE); // 9
+ }
+ PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java
new file mode 100644
index 000000000..4ae81529b
--- /dev/null
+++ b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.AccountFilterUtil;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import java.util.Set;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Fragment containing a phone number list for picking. */
+public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+ implements PhoneNumberListAdapter.Listener {
+
+ private static final String KEY_FILTER = "filter";
+ private OnPhoneNumberPickerActionListener mListener;
+ private ContactListFilter mFilter;
+ private View mAccountFilterHeader;
+ /**
+ * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE.
+ */
+ private View mPaddingView;
+ /** true if the loader has started at least once. */
+ private boolean mLoaderStarted;
+
+ private boolean mUseCallableUri;
+
+ private ContactListItemView.PhotoPosition mPhotoPosition =
+ ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */);
+
+ private final Set<OnLoadFinishedListener> mLoadFinishedListeners =
+ new ArraySet<OnLoadFinishedListener>();
+
+ private CursorReranker mCursorReranker;
+
+ public PhoneNumberPickerFragment() {
+ setQuickContactEnabled(false);
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
+
+ // Show nothing instead of letting caller Activity show something.
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * Handles a click on the video call icon for a row in the list.
+ *
+ * @param position The position in the list where the click ocurred.
+ */
+ @Override
+ public void onVideoCallIconClicked(int position) {
+ callNumber(position, true /* isVideoCall */);
+ }
+
+ public void setDirectorySearchEnabled(boolean flag) {
+ setDirectorySearchMode(
+ flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE);
+ }
+
+ public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
+ this.mListener = listener;
+ }
+
+ public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() {
+ return mListener;
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ super.onCreateView(inflater, container);
+
+ View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false);
+ mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding);
+ getListView().addHeaderView(paddingView);
+
+ mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container);
+ updateFilterHeaderView();
+
+ setVisibleScrollbarEnabled(getVisibleScrollbarEnabled());
+ }
+
+ protected boolean getVisibleScrollbarEnabled() {
+ return true;
+ }
+
+ @Override
+ protected void setSearchMode(boolean flag) {
+ super.setSearchMode(flag);
+ updateFilterHeaderView();
+ }
+
+ private void updateFilterHeaderView() {
+ final ContactListFilter filter = getFilter();
+ if (mAccountFilterHeader == null || filter == null) {
+ return;
+ }
+ final boolean shouldShowHeader =
+ !isSearchMode()
+ && AccountFilterUtil.updateAccountFilterTitleForPhone(
+ mAccountFilterHeader, filter, false);
+ if (shouldShowHeader) {
+ mPaddingView.setVisibility(View.GONE);
+ mAccountFilterHeader.setVisibility(View.VISIBLE);
+ } else {
+ mPaddingView.setVisibility(View.VISIBLE);
+ mAccountFilterHeader.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void restoreSavedState(Bundle savedState) {
+ super.restoreSavedState(savedState);
+
+ if (savedState == null) {
+ return;
+ }
+
+ mFilter = savedState.getParcelable(KEY_FILTER);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_FILTER, mFilter);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ if (mListener != null) {
+ mListener.onHomeInActionBarSelected();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ callNumber(position, false /* isVideoCall */);
+ }
+
+ /**
+ * Initiates a call to the number at the specified position.
+ *
+ * @param position The position.
+ * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false}
+ * otherwise.
+ */
+ private void callNumber(int position, boolean isVideoCall) {
+ final String number = getPhoneNumber(position);
+ if (!TextUtils.isEmpty(number)) {
+ cacheContactInfo(position);
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = getCallInitiationType(true /* isRemoteDirectory */);
+ callSpecificAppData.positionOfSelectedSearchResult = position;
+ callSpecificAppData.charactersInSearchString =
+ getQueryString() == null ? 0 : getQueryString().length();
+ mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData);
+ } else {
+ LogUtil.i(
+ "PhoneNumberPickerFragment.callNumber",
+ "item at %d was clicked before adapter is ready, ignoring",
+ position);
+ }
+
+ // Get the lookup key and track any analytics
+ final String lookupKey = getLookupKey(position);
+ if (!TextUtils.isEmpty(lookupKey)) {
+ maybeTrackAnalytics(lookupKey);
+ }
+ }
+
+ protected void cacheContactInfo(int position) {
+ // Not implemented. Hook for child classes
+ }
+
+ protected String getPhoneNumber(int position) {
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ return adapter.getPhoneNumber(position);
+ }
+
+ protected String getLookupKey(int position) {
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ return adapter.getLookupKey(position);
+ }
+
+ @Override
+ protected void startLoading() {
+ mLoaderStarted = true;
+ super.startLoading();
+ }
+
+ @Override
+ @MainThread
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ Assert.isMainThread();
+ // TODO: define and verify behavior for "Nearby places", corp directories,
+ // and dividers listed in UI between these categories
+ if (mCursorReranker != null
+ && data != null
+ && !data.isClosed()
+ && data.getCount() > 0
+ && loader.getId() != -1) { // skip invalid directory ID of -1
+ data = mCursorReranker.rerankCursor(data);
+ }
+ super.onLoadFinished(loader, data);
+
+ // disable scroll bar if there is no data
+ setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0);
+
+ if (data != null) {
+ notifyListeners();
+ }
+ }
+
+ /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */
+ public interface CursorReranker {
+ @MainThread
+ Cursor rerankCursor(Cursor data);
+ }
+
+ @MainThread
+ public void setReranker(@Nullable CursorReranker reranker) {
+ Assert.isMainThread();
+ mCursorReranker = reranker;
+ }
+
+ /** Listener that is notified when cursor has finished loading data. */
+ public interface OnLoadFinishedListener {
+ void onLoadFinished();
+ }
+
+ @MainThread
+ public void addOnLoadFinishedListener(OnLoadFinishedListener listener) {
+ Assert.isMainThread();
+ mLoadFinishedListeners.add(listener);
+ }
+
+ @MainThread
+ public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) {
+ Assert.isMainThread();
+ mLoadFinishedListeners.remove(listener);
+ }
+
+ @MainThread
+ protected void notifyListeners() {
+ Assert.isMainThread();
+ for (OnLoadFinishedListener listener : mLoadFinishedListeners) {
+ listener.onLoadFinished();
+ }
+ }
+
+ @MainThread
+ @Override
+ public void onDetach() {
+ Assert.isMainThread();
+ mLoadFinishedListeners.clear();
+ super.onDetach();
+ }
+
+ public void setUseCallableUri(boolean useCallableUri) {
+ mUseCallableUri = useCallableUri;
+ }
+
+ public boolean usesCallableUri() {
+ return mUseCallableUri;
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ adapter.setUseCallableUri(mUseCallableUri);
+ return adapter;
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ final ContactEntryListAdapter adapter = getAdapter();
+ if (adapter == null) {
+ return;
+ }
+
+ if (!isSearchMode() && mFilter != null) {
+ adapter.setFilter(mFilter);
+ }
+
+ setPhotoPosition(adapter);
+ }
+
+ protected void setPhotoPosition(ContactEntryListAdapter adapter) {
+ ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contact_list_content, null);
+ }
+
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
+ return;
+ }
+
+ mFilter = filter;
+ if (mLoaderStarted) {
+ reloadData();
+ }
+ updateFilterHeaderView();
+ }
+
+ public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ if (adapter != null) {
+ adapter.setPhotoPosition(photoPosition);
+ }
+ }
+
+ /**
+ * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number
+ * not in the local contacts database
+ */
+ protected int getCallInitiationType(boolean isRemoteDirectory) {
+ return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN;
+ }
+
+ /**
+ * Where a lookup key contains analytic event information, logs the associated analytics event.
+ *
+ * @param lookupKey The lookup key JSON object.
+ */
+ private void maybeTrackAnalytics(String lookupKey) {
+ try {
+ JSONObject json = new JSONObject(lookupKey);
+
+ String analyticsCategory =
+ json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY);
+ String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION);
+ String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE);
+
+ if (TextUtils.isEmpty(analyticsCategory)
+ || TextUtils.isEmpty(analyticsAction)
+ || TextUtils.isEmpty(analyticsValue)) {
+ return;
+ }
+
+ // Assume that the analytic value being tracked could be a float value, but just cast
+ // to a long so that the analytic server can handle it.
+ long value;
+ try {
+ float floatValue = Float.parseFloat(analyticsValue);
+ value = (long) floatValue;
+ } catch (NumberFormatException nfe) {
+ return;
+ }
+
+ Logger.get(getActivity())
+ .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value);
+ } catch (JSONException e) {
+ // Not an error; just a lookup key that doesn't have the right information.
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java
new file mode 100644
index 000000000..0bdcef084
--- /dev/null
+++ b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.common.widget.CompositeCursorAdapter;
+
+/** A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. */
+public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter
+ implements PinnedHeaderListView.PinnedHeaderAdapter {
+
+ public static final int PARTITION_HEADER_TYPE = 0;
+
+ private boolean mPinnedPartitionHeadersEnabled;
+ private boolean[] mHeaderVisibility;
+
+ public PinnedHeaderListAdapter(Context context) {
+ super(context);
+ }
+
+ public boolean getPinnedPartitionHeadersEnabled() {
+ return mPinnedPartitionHeadersEnabled;
+ }
+
+ public void setPinnedPartitionHeadersEnabled(boolean flag) {
+ this.mPinnedPartitionHeadersEnabled = flag;
+ }
+
+ @Override
+ public int getPinnedHeaderCount() {
+ if (mPinnedPartitionHeadersEnabled) {
+ return getPartitionCount();
+ } else {
+ return 0;
+ }
+ }
+
+ protected boolean isPinnedPartitionHeaderVisible(int partition) {
+ return getPinnedPartitionHeadersEnabled()
+ && hasHeader(partition)
+ && !isPartitionEmpty(partition);
+ }
+
+ /** The default implementation creates the same type of view as a normal partition header. */
+ @Override
+ public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) {
+ if (hasHeader(partition)) {
+ View view = null;
+ if (convertView != null) {
+ Integer headerType = (Integer) convertView.getTag();
+ if (headerType != null && headerType == PARTITION_HEADER_TYPE) {
+ view = convertView;
+ }
+ }
+ if (view == null) {
+ view = newHeaderView(getContext(), partition, null, parent);
+ view.setTag(PARTITION_HEADER_TYPE);
+ view.setFocusable(false);
+ view.setEnabled(false);
+ }
+ bindHeaderView(view, partition, getCursor(partition));
+ view.setLayoutDirection(parent.getLayoutDirection());
+ return view;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void configurePinnedHeaders(PinnedHeaderListView listView) {
+ if (!getPinnedPartitionHeadersEnabled()) {
+ return;
+ }
+
+ int size = getPartitionCount();
+
+ // Cache visibility bits, because we will need them several times later on
+ if (mHeaderVisibility == null || mHeaderVisibility.length != size) {
+ mHeaderVisibility = new boolean[size];
+ }
+ for (int i = 0; i < size; i++) {
+ boolean visible = isPinnedPartitionHeaderVisible(i);
+ mHeaderVisibility[i] = visible;
+ if (!visible) {
+ listView.setHeaderInvisible(i, true);
+ }
+ }
+
+ int headerViewsCount = listView.getHeaderViewsCount();
+
+ // Starting at the top, find and pin headers for partitions preceding the visible one(s)
+ int maxTopHeader = -1;
+ int topHeaderHeight = 0;
+ for (int i = 0; i < size; i++) {
+ if (mHeaderVisibility[i]) {
+ int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount;
+ int partition = getPartitionForPosition(position);
+ if (i > partition) {
+ break;
+ }
+
+ listView.setHeaderPinnedAtTop(i, topHeaderHeight, false);
+ topHeaderHeight += listView.getPinnedHeaderHeight(i);
+ maxTopHeader = i;
+ }
+ }
+
+ // Starting at the bottom, find and pin headers for partitions following the visible one(s)
+ int maxBottomHeader = size;
+ int bottomHeaderHeight = 0;
+ int listHeight = listView.getHeight();
+ for (int i = size; --i > maxTopHeader; ) {
+ if (mHeaderVisibility[i]) {
+ int position = listView.getPositionAt(listHeight - bottomHeaderHeight) - headerViewsCount;
+ if (position < 0) {
+ break;
+ }
+
+ int partition = getPartitionForPosition(position - 1);
+ if (partition == -1 || i <= partition) {
+ break;
+ }
+
+ int height = listView.getPinnedHeaderHeight(i);
+ bottomHeaderHeight += height;
+
+ listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, false);
+ maxBottomHeader = i;
+ }
+ }
+
+ // Headers in between the top-pinned and bottom-pinned should be hidden
+ for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) {
+ if (mHeaderVisibility[i]) {
+ listView.setHeaderInvisible(i, isPartitionEmpty(i));
+ }
+ }
+ }
+
+ @Override
+ public int getScrollPositionForHeader(int viewIndex) {
+ return getPositionForPartition(viewIndex);
+ }
+}
diff --git a/java/com/android/contacts/common/list/PinnedHeaderListView.java b/java/com/android/contacts/common/list/PinnedHeaderListView.java
new file mode 100644
index 000000000..33c68b68c
--- /dev/null
+++ b/java/com/android/contacts/common/list/PinnedHeaderListView.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ListAdapter;
+import com.android.dialer.util.ViewUtil;
+
+/**
+ * A ListView that maintains a header pinned at the top of the list. The pinned header can be pushed
+ * up and dissolved as needed.
+ */
+public class PinnedHeaderListView extends AutoScrollListView
+ implements OnScrollListener, OnItemSelectedListener {
+
+ private static final int MAX_ALPHA = 255;
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int FADING = 2;
+ private static final int DEFAULT_ANIMATION_DURATION = 20;
+ private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100;
+ private PinnedHeaderAdapter mAdapter;
+ private int mSize;
+ private PinnedHeader[] mHeaders;
+ private RectF mBounds = new RectF();
+ private OnScrollListener mOnScrollListener;
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private int mScrollState;
+ private boolean mScrollToSectionOnHeaderTouch = false;
+ private boolean mHeaderTouched = false;
+ private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
+ private boolean mAnimating;
+ private long mAnimationTargetTime;
+ private int mHeaderPaddingStart;
+ private int mHeaderWidth;
+
+ public PinnedHeaderListView(Context context) {
+ this(context, null, android.R.attr.listViewStyle);
+ }
+
+ public PinnedHeaderListView(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.listViewStyle);
+ }
+
+ public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ super.setOnScrollListener(this);
+ super.setOnItemSelectedListener(this);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mHeaderPaddingStart = getPaddingStart();
+ mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd();
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ mAdapter = (PinnedHeaderAdapter) adapter;
+ super.setAdapter(adapter);
+ }
+
+ @Override
+ public void setOnScrollListener(OnScrollListener onScrollListener) {
+ mOnScrollListener = onScrollListener;
+ super.setOnScrollListener(this);
+ }
+
+ @Override
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ super.setOnItemSelectedListener(this);
+ }
+
+ public void setScrollToSectionOnHeaderTouch(boolean value) {
+ mScrollToSectionOnHeaderTouch = value;
+ }
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (mAdapter != null) {
+ int count = mAdapter.getPinnedHeaderCount();
+ if (count != mSize) {
+ mSize = count;
+ if (mHeaders == null) {
+ mHeaders = new PinnedHeader[mSize];
+ } else if (mHeaders.length < mSize) {
+ PinnedHeader[] headers = mHeaders;
+ mHeaders = new PinnedHeader[mSize];
+ System.arraycopy(headers, 0, mHeaders, 0, headers.length);
+ }
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (mHeaders[i] == null) {
+ mHeaders[i] = new PinnedHeader();
+ }
+ mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
+ }
+
+ mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
+ mAdapter.configurePinnedHeaders(this);
+ invalidateIfAnimating();
+ }
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
+ }
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ // Disable vertical fading at the top when the pinned header is present
+ return mSize > 0 ? 0 : super.getTopFadingEdgeStrength();
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ mScrollState = scrollState;
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(this, scrollState);
+ }
+ }
+
+ /**
+ * Ensures that the selected item is positioned below the top-pinned headers and above the
+ * bottom-pinned ones.
+ */
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int height = getHeight();
+
+ int windowTop = 0;
+ int windowBottom = height;
+
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ if (header.state == TOP) {
+ windowTop = header.y + header.height;
+ } else if (header.state == BOTTOM) {
+ windowBottom = header.y;
+ break;
+ }
+ }
+ }
+
+ View selectedView = getSelectedView();
+ if (selectedView != null) {
+ if (selectedView.getTop() < windowTop) {
+ setSelectionFromTop(position, windowTop);
+ } else if (selectedView.getBottom() > windowBottom) {
+ setSelectionFromTop(position, windowBottom - selectedView.getHeight());
+ }
+ }
+
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(parent, view, position, id);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onNothingSelected(parent);
+ }
+ }
+
+ public int getPinnedHeaderHeight(int viewIndex) {
+ ensurePinnedHeaderLayout(viewIndex);
+ return mHeaders[viewIndex].view.getHeight();
+ }
+
+ /**
+ * Set header to be pinned at the top.
+ *
+ * @param viewIndex index of the header view
+ * @param y is position of the header in pixels.
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
+ ensurePinnedHeaderLayout(viewIndex);
+ PinnedHeader header = mHeaders[viewIndex];
+ header.visible = true;
+ header.y = y;
+ header.state = TOP;
+
+ // TODO perhaps we should animate at the top as well
+ header.animating = false;
+ }
+
+ /**
+ * Set header to be pinned at the bottom.
+ *
+ * @param viewIndex index of the header view
+ * @param y is position of the header in pixels.
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
+ ensurePinnedHeaderLayout(viewIndex);
+ PinnedHeader header = mHeaders[viewIndex];
+ header.state = BOTTOM;
+ if (header.animating) {
+ header.targetTime = mAnimationTargetTime;
+ header.sourceY = header.y;
+ header.targetY = y;
+ } else if (animate && (header.y != y || !header.visible)) {
+ if (header.visible) {
+ header.sourceY = header.y;
+ } else {
+ header.visible = true;
+ header.sourceY = y + header.height;
+ }
+ header.animating = true;
+ header.targetVisible = true;
+ header.targetTime = mAnimationTargetTime;
+ header.targetY = y;
+ } else {
+ header.visible = true;
+ header.y = y;
+ }
+ }
+
+ /**
+ * Set header to be pinned at the top of the first visible item.
+ *
+ * @param viewIndex index of the header view
+ * @param position is position of the header in pixels.
+ */
+ public void setFadingHeader(int viewIndex, int position, boolean fade) {
+ ensurePinnedHeaderLayout(viewIndex);
+
+ View child = getChildAt(position - getFirstVisiblePosition());
+ if (child == null) {
+ return;
+ }
+
+ PinnedHeader header = mHeaders[viewIndex];
+ header.visible = true;
+ header.state = FADING;
+ header.alpha = MAX_ALPHA;
+ header.animating = false;
+
+ int top = getTotalTopPinnedHeaderHeight();
+ header.y = top;
+ if (fade) {
+ int bottom = child.getBottom() - top;
+ int headerHeight = header.height;
+ if (bottom < headerHeight) {
+ int portion = bottom - headerHeight;
+ header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
+ header.y = top + portion;
+ }
+ }
+ }
+
+ /**
+ * Makes header invisible.
+ *
+ * @param viewIndex index of the header view
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderInvisible(int viewIndex, boolean animate) {
+ PinnedHeader header = mHeaders[viewIndex];
+ if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
+ header.sourceY = header.y;
+ if (!header.animating) {
+ header.visible = true;
+ header.targetY = getBottom() + header.height;
+ }
+ header.animating = true;
+ header.targetTime = mAnimationTargetTime;
+ header.targetVisible = false;
+ } else {
+ header.visible = false;
+ }
+ }
+
+ private void ensurePinnedHeaderLayout(int viewIndex) {
+ View view = mHeaders[viewIndex].view;
+ if (view.isLayoutRequested()) {
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ int widthSpec;
+ int heightSpec;
+
+ if (layoutParams != null && layoutParams.width > 0) {
+ widthSpec = View.MeasureSpec.makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY);
+ } else {
+ widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY);
+ }
+
+ if (layoutParams != null && layoutParams.height > 0) {
+ heightSpec =
+ View.MeasureSpec.makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY);
+ } else {
+ heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+ view.measure(widthSpec, heightSpec);
+ int height = view.getMeasuredHeight();
+ mHeaders[viewIndex].height = height;
+ view.layout(0, 0, view.getMeasuredWidth(), height);
+ }
+ }
+
+ /** Returns the sum of heights of headers pinned to the top. */
+ public int getTotalTopPinnedHeaderHeight() {
+ for (int i = mSize; --i >= 0; ) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.state == TOP) {
+ return header.y + header.height;
+ }
+ }
+ return 0;
+ }
+
+ /** Returns the list item position at the specified y coordinate. */
+ public int getPositionAt(int y) {
+ do {
+ int position = pointToPosition(getPaddingLeft() + 1, y);
+ if (position != -1) {
+ return position;
+ }
+ // If position == -1, we must have hit a separator. Let's examine
+ // a nearby pixel
+ y--;
+ } while (y > 0);
+ return 0;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mHeaderTouched = false;
+ if (super.onInterceptTouchEvent(ev)) {
+ return true;
+ }
+
+ if (mScrollState == SCROLL_STATE_IDLE) {
+ final int y = (int) ev.getY();
+ final int x = (int) ev.getX();
+ for (int i = mSize; --i >= 0; ) {
+ PinnedHeader header = mHeaders[i];
+ // For RTL layouts, this also takes into account that the scrollbar is on the left
+ // side.
+ final int padding = getPaddingLeft();
+ if (header.visible
+ && header.y <= y
+ && header.y + header.height > y
+ && x >= padding
+ && padding + header.view.getWidth() >= x) {
+ mHeaderTouched = true;
+ if (mScrollToSectionOnHeaderTouch && ev.getAction() == MotionEvent.ACTION_DOWN) {
+ return smoothScrollToPartition(i);
+ } else {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mHeaderTouched) {
+ if (ev.getAction() == MotionEvent.ACTION_UP) {
+ mHeaderTouched = false;
+ }
+ return true;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ private boolean smoothScrollToPartition(int partition) {
+ if (mAdapter == null) {
+ return false;
+ }
+ final int position = mAdapter.getScrollPositionForHeader(partition);
+ if (position == -1) {
+ return false;
+ }
+
+ int offset = 0;
+ for (int i = 0; i < partition; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ offset += header.height;
+ }
+ }
+ smoothScrollToPositionFromTop(
+ position + getHeaderViewsCount(), offset, DEFAULT_SMOOTH_SCROLL_DURATION);
+ return true;
+ }
+
+ private void invalidateIfAnimating() {
+ mAnimating = false;
+ for (int i = 0; i < mSize; i++) {
+ if (mHeaders[i].animating) {
+ mAnimating = true;
+ invalidate();
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ long currentTime = mAnimating ? System.currentTimeMillis() : 0;
+
+ int top = 0;
+ int bottom = getBottom();
+ boolean hasVisibleHeaders = false;
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ hasVisibleHeaders = true;
+ if (header.state == BOTTOM && header.y < bottom) {
+ bottom = header.y;
+ } else if (header.state == TOP || header.state == FADING) {
+ int newTop = header.y + header.height;
+ if (newTop > top) {
+ top = newTop;
+ }
+ }
+ }
+ }
+
+ if (hasVisibleHeaders) {
+ canvas.save();
+ }
+
+ super.dispatchDraw(canvas);
+
+ if (hasVisibleHeaders) {
+ canvas.restore();
+
+ // If the first item is visible and if it has a positive top that is greater than the
+ // first header's assigned y-value, use that for the first header's y value. This way,
+ // the header inherits any padding applied to the list view.
+ if (mSize > 0 && getFirstVisiblePosition() == 0) {
+ View firstChild = getChildAt(0);
+ PinnedHeader firstHeader = mHeaders[0];
+
+ if (firstHeader != null) {
+ int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0;
+ firstHeader.y = Math.max(firstHeader.y, firstHeaderTop);
+ }
+ }
+
+ // First draw top headers, then the bottom ones to handle the Z axis correctly
+ for (int i = mSize; --i >= 0; ) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && (header.state == TOP || header.state == FADING)) {
+ drawHeader(canvas, header, currentTime);
+ }
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.state == BOTTOM) {
+ drawHeader(canvas, header, currentTime);
+ }
+ }
+ }
+
+ invalidateIfAnimating();
+ }
+
+ private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
+ if (header.animating) {
+ int timeLeft = (int) (header.targetTime - currentTime);
+ if (timeLeft <= 0) {
+ header.y = header.targetY;
+ header.visible = header.targetVisible;
+ header.animating = false;
+ } else {
+ header.y =
+ header.targetY + (header.sourceY - header.targetY) * timeLeft / mAnimationDuration;
+ }
+ }
+ if (header.visible) {
+ View view = header.view;
+ int saveCount = canvas.save();
+ int translateX =
+ ViewUtil.isViewLayoutRtl(this)
+ ? getWidth() - mHeaderPaddingStart - view.getWidth()
+ : mHeaderPaddingStart;
+ canvas.translate(translateX, header.y);
+ if (header.state == FADING) {
+ mBounds.set(0, 0, view.getWidth(), view.getHeight());
+ canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
+ }
+ view.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ /** Adapter interface. The list adapter must implement this interface. */
+ public interface PinnedHeaderAdapter {
+
+ /** Returns the overall number of pinned headers, visible or not. */
+ int getPinnedHeaderCount();
+
+ /** Creates or updates the pinned header view. */
+ View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
+
+ /**
+ * Configures the pinned headers to match the visible list items. The adapter should call {@link
+ * PinnedHeaderListView#setHeaderPinnedAtTop}, {@link
+ * PinnedHeaderListView#setHeaderPinnedAtBottom}, {@link PinnedHeaderListView#setFadingHeader}
+ * or {@link PinnedHeaderListView#setHeaderInvisible}, for each header that needs to change its
+ * position or visibility.
+ */
+ void configurePinnedHeaders(PinnedHeaderListView listView);
+
+ /**
+ * Returns the list position to scroll to if the pinned header is touched. Return -1 if the list
+ * does not need to be scrolled.
+ */
+ int getScrollPositionForHeader(int viewIndex);
+ }
+
+ private static final class PinnedHeader {
+
+ View view;
+ boolean visible;
+ int y;
+ int height;
+ int alpha;
+ int state;
+
+ boolean animating;
+ boolean targetVisible;
+ int sourceY;
+ int targetY;
+ long targetTime;
+ }
+}
diff --git a/java/com/android/contacts/common/list/ViewPagerTabStrip.java b/java/com/android/contacts/common/list/ViewPagerTabStrip.java
new file mode 100644
index 000000000..969a6d342
--- /dev/null
+++ b/java/com/android/contacts/common/list/ViewPagerTabStrip.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 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
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.contacts.common.R;
+
+public class ViewPagerTabStrip extends LinearLayout {
+
+ private final Paint mSelectedUnderlinePaint;
+ private int mSelectedUnderlineThickness;
+ private int mIndexForSelection;
+ private float mSelectionOffset;
+
+ public ViewPagerTabStrip(Context context) {
+ this(context, null);
+ }
+
+ public ViewPagerTabStrip(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final Resources res = context.getResources();
+
+ mSelectedUnderlineThickness = res.getDimensionPixelSize(R.dimen.tab_selected_underline_height);
+ int underlineColor = res.getColor(R.color.tab_selected_underline_color);
+ int backgroundColor = res.getColor(R.color.contactscommon_actionbar_background_color);
+
+ mSelectedUnderlinePaint = new Paint();
+ mSelectedUnderlinePaint.setColor(underlineColor);
+
+ setBackgroundColor(backgroundColor);
+ setWillNotDraw(false);
+ }
+
+ /**
+ * Notifies this view that view pager has been scrolled. We save the tab index and selection
+ * offset for interpolating the position and width of selection underline.
+ */
+ void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ mIndexForSelection = position;
+ mSelectionOffset = positionOffset;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int childCount = getChildCount();
+
+ // Thick colored underline below the current selection
+ if (childCount > 0) {
+ View selectedTitle = getChildAt(mIndexForSelection);
+
+ if (selectedTitle == null) {
+ // The view pager's tab count changed but we weren't notified yet. Ignore this draw
+ // pass, when we get a new selection we will update and draw the selection strip in
+ // the correct place.
+ return;
+ }
+ int selectedLeft = selectedTitle.getLeft();
+ int selectedRight = selectedTitle.getRight();
+ final boolean isRtl = isRtl();
+ final boolean hasNextTab =
+ isRtl ? mIndexForSelection > 0 : (mIndexForSelection < (getChildCount() - 1));
+ if ((mSelectionOffset > 0.0f) && hasNextTab) {
+ // Draw the selection partway between the tabs
+ View nextTitle = getChildAt(mIndexForSelection + (isRtl ? -1 : 1));
+ int nextLeft = nextTitle.getLeft();
+ int nextRight = nextTitle.getRight();
+
+ selectedLeft =
+ (int) (mSelectionOffset * nextLeft + (1.0f - mSelectionOffset) * selectedLeft);
+ selectedRight =
+ (int) (mSelectionOffset * nextRight + (1.0f - mSelectionOffset) * selectedRight);
+ }
+
+ int height = getHeight();
+ canvas.drawRect(
+ selectedLeft,
+ height - mSelectedUnderlineThickness,
+ selectedRight,
+ height,
+ mSelectedUnderlinePaint);
+ }
+ }
+
+ private boolean isRtl() {
+ return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+}
diff --git a/java/com/android/contacts/common/list/ViewPagerTabs.java b/java/com/android/contacts/common/list/ViewPagerTabs.java
new file mode 100644
index 000000000..34f623ef4
--- /dev/null
+++ b/java/com/android/contacts/common/list/ViewPagerTabs.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Outline;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.contacts.common.R;
+import com.android.dialer.compat.CompatUtils;
+
+/**
+ * Lightweight implementation of ViewPager tabs. This looks similar to traditional actionBar tabs,
+ * but allows for the view containing the tabs to be placed anywhere on screen. Text-related
+ * attributes can also be assigned in XML - these will get propogated to the child TextViews
+ * automatically.
+ */
+public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnPageChangeListener {
+
+ private static final ViewOutlineProvider VIEW_BOUNDS_OUTLINE_PROVIDER;
+ private static final int TAB_SIDE_PADDING_IN_DPS = 10;
+ // TODO: This should use <declare-styleable> in the future
+ private static final int[] ATTRS =
+ new int[] {
+ android.R.attr.textSize,
+ android.R.attr.textStyle,
+ android.R.attr.textColor,
+ android.R.attr.textAllCaps
+ };
+
+ static {
+ if (CompatUtils.isLollipopCompatible()) {
+ VIEW_BOUNDS_OUTLINE_PROVIDER =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRect(0, 0, view.getWidth(), view.getHeight());
+ }
+ };
+ } else {
+ VIEW_BOUNDS_OUTLINE_PROVIDER = null;
+ }
+ }
+
+ /**
+ * Linearlayout that will contain the TextViews serving as tabs. This is the only child of the
+ * parent HorizontalScrollView.
+ */
+ final int mTextStyle;
+
+ final ColorStateList mTextColor;
+ final int mTextSize;
+ final boolean mTextAllCaps;
+ ViewPager mPager;
+ int mPrevSelected = -1;
+ int mSidePadding;
+ private ViewPagerTabStrip mTabStrip;
+ private int[] mTabIcons;
+ // For displaying the unread count next to the tab icon.
+ private int[] mUnreadCounts;
+
+ public ViewPagerTabs(Context context) {
+ this(context, null);
+ }
+
+ public ViewPagerTabs(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ViewPagerTabs(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setFillViewport(true);
+
+ mSidePadding = (int) (getResources().getDisplayMetrics().density * TAB_SIDE_PADDING_IN_DPS);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+ mTextSize = a.getDimensionPixelSize(0, 0);
+ mTextStyle = a.getInt(1, 0);
+ mTextColor = a.getColorStateList(2);
+ mTextAllCaps = a.getBoolean(3, false);
+
+ mTabStrip = new ViewPagerTabStrip(context);
+ addView(
+ mTabStrip,
+ new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+ a.recycle();
+
+ if (CompatUtils.isLollipopCompatible()) {
+ // enable shadow casting from view bounds
+ setOutlineProvider(VIEW_BOUNDS_OUTLINE_PROVIDER);
+ }
+ }
+
+ public void setViewPager(ViewPager viewPager) {
+ mPager = viewPager;
+ addTabs(mPager.getAdapter());
+ }
+
+ /**
+ * Set the tab icons and initialize an array for unread counts the same length as the icon array.
+ *
+ * @param tabIcons An array representing the tab icons in order.
+ */
+ public void configureTabIcons(int[] tabIcons) {
+ mTabIcons = tabIcons;
+ mUnreadCounts = new int[tabIcons.length];
+ }
+
+ public void setUnreadCount(int count, int position) {
+ if (mUnreadCounts == null || position >= mUnreadCounts.length) {
+ return;
+ }
+ mUnreadCounts[position] = count;
+ }
+
+ private void addTabs(PagerAdapter adapter) {
+ mTabStrip.removeAllViews();
+
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ addTab(adapter.getPageTitle(i), i);
+ }
+ }
+
+ private void addTab(CharSequence tabTitle, final int position) {
+ View tabView;
+ if (mTabIcons != null && position < mTabIcons.length) {
+ View layout = LayoutInflater.from(getContext()).inflate(R.layout.unread_count_tab, null);
+ View iconView = layout.findViewById(R.id.icon);
+ iconView.setBackgroundResource(mTabIcons[position]);
+ iconView.setContentDescription(tabTitle);
+ TextView textView = (TextView) layout.findViewById(R.id.count);
+ if (mUnreadCounts != null && mUnreadCounts[position] > 0) {
+ textView.setText(Integer.toString(mUnreadCounts[position]));
+ textView.setVisibility(View.VISIBLE);
+ iconView.setContentDescription(
+ getResources()
+ .getQuantityString(
+ R.plurals.tab_title_with_unread_items,
+ mUnreadCounts[position],
+ tabTitle.toString(),
+ mUnreadCounts[position]));
+ } else {
+ textView.setVisibility(View.INVISIBLE);
+ iconView.setContentDescription(getResources().getString(R.string.tab_title, tabTitle));
+ }
+ tabView = layout;
+ } else {
+ final TextView textView = new TextView(getContext());
+ textView.setText(tabTitle);
+ textView.setBackgroundResource(R.drawable.view_pager_tab_background);
+
+ // Assign various text appearance related attributes to child views.
+ if (mTextStyle > 0) {
+ textView.setTypeface(textView.getTypeface(), mTextStyle);
+ }
+ if (mTextSize > 0) {
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ }
+ if (mTextColor != null) {
+ textView.setTextColor(mTextColor);
+ }
+ textView.setAllCaps(mTextAllCaps);
+ textView.setGravity(Gravity.CENTER);
+
+ tabView = textView;
+ }
+
+ tabView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mPager.setCurrentItem(getRtlPosition(position));
+ }
+ });
+
+ tabView.setOnLongClickListener(new OnTabLongClickListener(position));
+
+ tabView.setPadding(mSidePadding, 0, mSidePadding, 0);
+
+ mTabStrip.addView(
+ tabView,
+ position,
+ new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1));
+
+ // Default to the first child being selected
+ if (position == 0) {
+ mPrevSelected = 0;
+ tabView.setSelected(true);
+ }
+ }
+
+ /**
+ * Remove a tab at a certain index.
+ *
+ * @param index The index of the tab view we wish to remove.
+ */
+ public void removeTab(int index) {
+ View view = mTabStrip.getChildAt(index);
+ if (view != null) {
+ mTabStrip.removeView(view);
+ }
+ }
+
+ /**
+ * Refresh a tab at a certain index by removing it and reconstructing it.
+ *
+ * @param index The index of the tab view we wish to update.
+ */
+ public void updateTab(int index) {
+ removeTab(index);
+
+ if (index < mPager.getAdapter().getCount()) {
+ addTab(mPager.getAdapter().getPageTitle(index), index);
+ }
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ position = getRtlPosition(position);
+ int tabStripChildCount = mTabStrip.getChildCount();
+ if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+ return;
+ }
+
+ mTabStrip.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ position = getRtlPosition(position);
+ int tabStripChildCount = mTabStrip.getChildCount();
+ if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+ return;
+ }
+
+ if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) {
+ mTabStrip.getChildAt(mPrevSelected).setSelected(false);
+ }
+ final View selectedChild = mTabStrip.getChildAt(position);
+ selectedChild.setSelected(true);
+
+ // Update scroll position
+ final int scrollPos = selectedChild.getLeft() - (getWidth() - selectedChild.getWidth()) / 2;
+ smoothScrollTo(scrollPos, 0);
+ mPrevSelected = position;
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ private int getRtlPosition(int position) {
+ if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ return mTabStrip.getChildCount() - 1 - position;
+ }
+ return position;
+ }
+
+ /** Simulates actionbar tab behavior by showing a toast with the tab title when long clicked. */
+ private class OnTabLongClickListener implements OnLongClickListener {
+
+ final int mPosition;
+
+ public OnTabLongClickListener(int position) {
+ mPosition = position;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ final int[] screenPos = new int[2];
+ getLocationOnScreen(screenPos);
+
+ final Context context = getContext();
+ final int width = getWidth();
+ final int height = getHeight();
+ final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+
+ Toast toast =
+ Toast.makeText(context, mPager.getAdapter().getPageTitle(mPosition), Toast.LENGTH_SHORT);
+
+ // Show the toast under the tab
+ toast.setGravity(
+ Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+ (screenPos[0] + width / 2) - screenWidth / 2,
+ screenPos[1] + height);
+
+ toast.show();
+ return true;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/location/CountryDetector.java b/java/com/android/contacts/common/location/CountryDetector.java
new file mode 100644
index 000000000..7d9e42b38
--- /dev/null
+++ b/java/com/android/contacts/common/location/CountryDetector.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.location;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.preference.PreferenceManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.Locale;
+
+/**
+ * This class is used to detect the country where the user is. It is a simplified version of the
+ * country detector service in the framework. The sources of country location are queried in the
+ * following order of reliability:
+ *
+ * <ul>
+ * <li>Mobile network
+ * <li>Location manager
+ * <li>SIM's country
+ * <li>User's default locale
+ * </ul>
+ *
+ * As far as possible this class tries to replicate the behavior of the system's country detector
+ * service: 1) Order in priority of sources of country location 2) Mobile network information
+ * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of
+ * 24 hours in the system) 4) Location updates only uses the {@link
+ * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully
+ * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the
+ * fallback never happens without a reboot) 6) Location is not used if the device does not implement
+ * a {@link android.location.Geocoder}
+ */
+public class CountryDetector {
+
+ public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated";
+ public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country";
+ private static final String TAG = "CountryDetector";
+ // Wait 12 hours between updates
+ private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12;
+ // Minimum distance before an update is triggered, in meters. We don't need this to be too
+ // exact because all we care about is what country the user is in.
+ private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000;
+ private static CountryDetector sInstance;
+ private final TelephonyManager mTelephonyManager;
+ private final LocationManager mLocationManager;
+ private final LocaleProvider mLocaleProvider;
+ // Used as a default country code when all the sources of country data have failed in the
+ // exceedingly rare event that the device does not have a default locale set for some reason.
+ private static final String DEFAULT_COUNTRY_ISO = "US";
+ private final Context mContext;
+
+ private CountryDetector(Context context) {
+ this(
+ context,
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
+ (LocationManager) context.getSystemService(Context.LOCATION_SERVICE),
+ new LocaleProvider());
+ }
+
+ private CountryDetector(
+ Context context,
+ TelephonyManager telephonyManager,
+ LocationManager locationManager,
+ LocaleProvider localeProvider) {
+ mTelephonyManager = telephonyManager;
+ mLocationManager = locationManager;
+ mLocaleProvider = localeProvider;
+ mContext = context;
+
+ registerForLocationUpdates(context, mLocationManager);
+ }
+
+ public static void registerForLocationUpdates(Context context, LocationManager locationManager) {
+ if (!PermissionsUtil.hasLocationPermissions(context)) {
+ Log.w(TAG, "No location permissions, not registering for location updates.");
+ return;
+ }
+
+ if (!Geocoder.isPresent()) {
+ // Certain devices do not have an implementation of a geocoder - in that case there is
+ // no point trying to get location updates because we cannot retrieve the country based
+ // on the location anyway.
+ return;
+ }
+ final Intent activeIntent = new Intent(context, LocationChangedReceiver.class);
+ final PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ locationManager.requestLocationUpdates(
+ LocationManager.PASSIVE_PROVIDER,
+ TIME_BETWEEN_UPDATES_MS,
+ DISTANCE_BETWEEN_UPDATES_METERS,
+ pendingIntent);
+ }
+
+ /**
+ * Returns the instance of the country detector. {@link #initialize(Context)} must have been
+ * called previously.
+ *
+ * @return the initialized country detector.
+ */
+ public static synchronized CountryDetector getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new CountryDetector(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */
+ public CountryDetector getInstanceForTest(
+ Context context,
+ TelephonyManager telephonyManager,
+ LocationManager locationManager,
+ LocaleProvider localeProvider,
+ Geocoder geocoder) {
+ return new CountryDetector(context, telephonyManager, locationManager, localeProvider);
+ }
+
+ public String getCurrentCountryIso() {
+ String result = null;
+ if (isNetworkCountryCodeAvailable()) {
+ result = getNetworkBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = getLocationBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = getSimBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = getLocaleBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = DEFAULT_COUNTRY_ISO;
+ }
+ return result.toUpperCase(Locale.US);
+ }
+
+ /** @return the country code of the current telephony network the user is connected to. */
+ private String getNetworkBasedCountryIso() {
+ return mTelephonyManager.getNetworkCountryIso();
+ }
+
+ /** @return the geocoded country code detected by the {@link LocationManager}. */
+ private String getLocationBasedCountryIso() {
+ if (!Geocoder.isPresent() || !PermissionsUtil.hasLocationPermissions(mContext)) {
+ return null;
+ }
+ final SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(mContext);
+ return sharedPreferences.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null);
+ }
+
+ /** @return the country code of the SIM card currently inserted in the device. */
+ private String getSimBasedCountryIso() {
+ return mTelephonyManager.getSimCountryIso();
+ }
+
+ /** @return the country code of the user's currently selected locale. */
+ private String getLocaleBasedCountryIso() {
+ Locale defaultLocale = mLocaleProvider.getDefaultLocale();
+ if (defaultLocale != null) {
+ return defaultLocale.getCountry();
+ }
+ return null;
+ }
+
+ private boolean isNetworkCountryCodeAvailable() {
+ // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code.
+ // In this case, we want to ignore the value returned and fallback to location instead.
+ return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
+ }
+
+ /**
+ * Class that can be used to return the user's default locale. This is in its own class so that it
+ * can be mocked out.
+ */
+ public static class LocaleProvider {
+
+ public Locale getDefaultLocale() {
+ return Locale.getDefault();
+ }
+ }
+
+ public static class LocationChangedReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) {
+ return;
+ }
+
+ final Location location =
+ (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED);
+
+ UpdateCountryService.updateCountry(context, location);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/location/UpdateCountryService.java b/java/com/android/contacts/common/location/UpdateCountryService.java
new file mode 100644
index 000000000..f23e09e20
--- /dev/null
+++ b/java/com/android/contacts/common/location/UpdateCountryService.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.location;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Service used to perform asynchronous geocoding from within a broadcast receiver. Given a {@link
+ * Location}, convert it into a country code, and save it in shared preferences.
+ */
+public class UpdateCountryService extends IntentService {
+
+ private static final String TAG = UpdateCountryService.class.getSimpleName();
+
+ private static final String ACTION_UPDATE_COUNTRY = "saveCountry";
+
+ private static final String KEY_INTENT_LOCATION = "location";
+
+ public UpdateCountryService() {
+ super(TAG);
+ }
+
+ public static void updateCountry(Context context, Location location) {
+ final Intent serviceIntent = new Intent(context, UpdateCountryService.class);
+ serviceIntent.setAction(ACTION_UPDATE_COUNTRY);
+ serviceIntent.putExtra(UpdateCountryService.KEY_INTENT_LOCATION, location);
+ context.startService(serviceIntent);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent == null) {
+ Log.d(TAG, "onHandleIntent: could not handle null intent");
+ return;
+ }
+ if (ACTION_UPDATE_COUNTRY.equals(intent.getAction())) {
+ final Location location = intent.getParcelableExtra(KEY_INTENT_LOCATION);
+ final String country = getCountryFromLocation(getApplicationContext(), location);
+
+ if (country == null) {
+ return;
+ }
+
+ final SharedPreferences prefs =
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+
+ final Editor editor = prefs.edit();
+ editor.putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis());
+ editor.putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country);
+ editor.commit();
+ }
+ }
+
+ /**
+ * Given a {@link Location}, return a country code.
+ *
+ * @return the ISO 3166-1 two letter country code
+ */
+ private String getCountryFromLocation(Context context, Location location) {
+ final Geocoder geocoder = new Geocoder(context);
+ String country = null;
+ try {
+ double latitude = location.getLatitude();
+ // Latitude has to be between 90 and -90 (latitude of north and south poles wrt equator)
+ if (latitude <= 90 && latitude >= -90) {
+ final List<Address> addresses =
+ geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
+ if (addresses != null && addresses.size() > 0) {
+ country = addresses.get(0).getCountryCode();
+ }
+ } else {
+ Log.w(TAG, "Invalid latitude");
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Exception occurred when getting geocoded country from location");
+ }
+ return country;
+ }
+}
diff --git a/java/com/android/contacts/common/model/AccountTypeManager.java b/java/com/android/contacts/common/model/AccountTypeManager.java
new file mode 100644
index 000000000..f225ff6ac
--- /dev/null
+++ b/java/com/android/contacts/common/model/AccountTypeManager.java
@@ -0,0 +1,813 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.OnAccountsUpdateListener;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncAdapterType;
+import android.content.SyncStatusObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.TimingLogger;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.list.ContactListFilterController;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.ExchangeAccountType;
+import com.android.contacts.common.model.account.ExternalAccountType;
+import com.android.contacts.common.model.account.FallbackAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.account.SamsungAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.Constants;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Singleton holder for all parsed {@link AccountType} available on the system, typically filled
+ * through {@link PackageManager} queries.
+ */
+public abstract class AccountTypeManager {
+
+ static final String TAG = "AccountTypeManager";
+
+ private static final Object mInitializationLock = new Object();
+ private static AccountTypeManager mAccountTypeManager;
+
+ /**
+ * Requests the singleton instance of {@link AccountTypeManager} with data bound from the
+ * available authenticators. This method can safely be called from the UI thread.
+ */
+ public static AccountTypeManager getInstance(Context context) {
+ synchronized (mInitializationLock) {
+ if (mAccountTypeManager == null) {
+ context = context.getApplicationContext();
+ mAccountTypeManager = new AccountTypeManagerImpl(context);
+ }
+ }
+ return mAccountTypeManager;
+ }
+
+ /**
+ * Set the instance of account type manager. This is only for and should only be used by unit
+ * tests. While having this method is not ideal, it's simpler than the alternative of holding this
+ * as a service in the ContactsApplication context class.
+ *
+ * @param mockManager The mock AccountTypeManager.
+ */
+ public static void setInstanceForTest(AccountTypeManager mockManager) {
+ synchronized (mInitializationLock) {
+ mAccountTypeManager = mockManager;
+ }
+ }
+
+ /**
+ * Returns the list of all accounts (if contactWritableOnly is false) or just the list of contact
+ * writable accounts (if contactWritableOnly is true).
+ */
+ // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
+ public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
+
+ /** Returns the list of accounts that are group writable. */
+ public abstract List<AccountWithDataSet> getGroupWritableAccounts();
+
+ public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
+
+ public final AccountType getAccountType(String accountType, String dataSet) {
+ return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
+ }
+
+ public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
+ if (account != null) {
+ return getAccountType(account.getAccountTypeWithDataSet());
+ }
+ return getAccountType(null, null);
+ }
+
+ /**
+ * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
+ * support the "invite" feature and have one or more account.
+ * <p>This is a filtered down and more "usable" list compared to {@link
+ * #getAllInvitableAccountTypes}, where usable is defined as: (1) making sure that the app
+ * that contributed the account type is not disabled (in order to avoid presenting the user
+ * with an option that does nothing), and (2) that there is at least one raw contact with that
+ * account type in the database (assuming that the user probably doesn't use that account
+ * type).
+ * <p>Warning: Don't use on the UI thread because this can scan the database.
+ */
+ public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
+
+ /**
+ * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
+ * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
+ * {@link FallbackAccountType}.
+ */
+ public DataKind getKindOrFallback(AccountType type, String mimeType) {
+ return type == null ? null : type.getKindForMimetype(mimeType);
+ }
+
+ /**
+ * Returns all registered {@link AccountType}s, including extension ones.
+ *
+ * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+ */
+ public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
+
+ /**
+ * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+ * @return true when this instance contains the given account.
+ */
+ public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
+ for (AccountWithDataSet account_2 : getAccounts(false)) {
+ if (account.equals(account_2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+class AccountTypeManagerImpl extends AccountTypeManager
+ implements OnAccountsUpdateListener, SyncStatusObserver {
+
+ private static final Map<AccountTypeWithDataSet, AccountType>
+ EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
+ Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
+
+ /**
+ * A sample contact URI used to test whether any activities will respond to an invitable intent
+ * with the given URI as the intent data. This doesn't need to be specific to a real contact
+ * because an app that intercepts the intent should probably do so for all types of contact URIs.
+ */
+ private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(1, "xxx");
+
+ private static final int MESSAGE_LOAD_DATA = 0;
+ private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
+ private static final Comparator<AccountWithDataSet> ACCOUNT_COMPARATOR =
+ new Comparator<AccountWithDataSet>() {
+ @Override
+ public int compare(AccountWithDataSet a, AccountWithDataSet b) {
+ if (Objects.equals(a.name, b.name)
+ && Objects.equals(a.type, b.type)
+ && Objects.equals(a.dataSet, b.dataSet)) {
+ return 0;
+ } else if (b.name == null || b.type == null) {
+ return -1;
+ } else if (a.name == null || a.type == null) {
+ return 1;
+ } else {
+ int diff = a.name.compareTo(b.name);
+ if (diff != 0) {
+ return diff;
+ }
+ diff = a.type.compareTo(b.type);
+ if (diff != 0) {
+ return diff;
+ }
+
+ // Accounts without data sets get sorted before those that have them.
+ if (a.dataSet != null) {
+ return b.dataSet == null ? 1 : a.dataSet.compareTo(b.dataSet);
+ } else {
+ return -1;
+ }
+ }
+ }
+ };
+ private final InvitableAccountTypeCache mInvitableAccountTypeCache;
+ /**
+ * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
+ * initialized. False otherwise.
+ */
+ private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
+ /**
+ * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. False
+ * otherwise.
+ */
+ private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private Context mContext;
+ private final Runnable mCheckFilterValidityRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
+ }
+ };
+ private AccountManager mAccountManager;
+ private AccountType mFallbackAccountType;
+ private List<AccountWithDataSet> mAccounts = new ArrayList<>();
+ private List<AccountWithDataSet> mContactWritableAccounts = new ArrayList<>();
+ private List<AccountWithDataSet> mGroupWritableAccounts = new ArrayList<>();
+ private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = new ArrayMap<>();
+ private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
+ EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+ private HandlerThread mListenerThread;
+ private Handler mListenerHandler;
+ private BroadcastReceiver mBroadcastReceiver =
+ new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
+ mListenerHandler.sendMessage(msg);
+ }
+ };
+ /* A latch that ensures that asynchronous initialization completes before data is used */
+ private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
+
+ /** Internal constructor that only performs initial parsing. */
+ public AccountTypeManagerImpl(Context context) {
+ mContext = context;
+ mFallbackAccountType = new FallbackAccountType(context);
+
+ mAccountManager = AccountManager.get(mContext);
+
+ mListenerThread = new HandlerThread("AccountChangeListener");
+ mListenerThread.start();
+ mListenerHandler =
+ new Handler(mListenerThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_LOAD_DATA:
+ loadAccountsInBackground();
+ break;
+ case MESSAGE_PROCESS_BROADCAST_INTENT:
+ processBroadcastIntent((Intent) msg.obj);
+ break;
+ }
+ }
+ };
+
+ mInvitableAccountTypeCache = new InvitableAccountTypeCache();
+
+ // Request updates when packages or accounts change
+ IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ IntentFilter sdFilter = new IntentFilter();
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+ mContext.registerReceiver(mBroadcastReceiver, sdFilter);
+
+ // Request updates when locale is changed so that the order of each field will
+ // be able to be changed on the locale change.
+ filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+
+ mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
+
+ ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
+
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ /**
+ * Find a specific {@link AuthenticatorDescription} in the provided list that matches the given
+ * account type.
+ */
+ protected static AuthenticatorDescription findAuthenticator(
+ AuthenticatorDescription[] auths, String accountType) {
+ for (AuthenticatorDescription auth : auths) {
+ if (accountType.equals(auth.type)) {
+ return auth;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return all {@link AccountType}s with at least one account which supports "invite", i.e. its
+ * {@link AccountType#getInviteContactActivityClassName()} is not empty.
+ */
+ @VisibleForTesting
+ static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(
+ Context context,
+ Collection<AccountWithDataSet> accounts,
+ Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
+ Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
+ for (AccountWithDataSet account : accounts) {
+ AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
+ AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
+ if (type == null) {
+ continue; // just in case
+ }
+ if (result.containsKey(accountTypeWithDataSet)) {
+ continue;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(
+ TAG,
+ "Type "
+ + accountTypeWithDataSet
+ + " inviteClass="
+ + type.getInviteContactActivityClassName());
+ }
+ if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
+ result.put(accountTypeWithDataSet, type);
+ }
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public void onStatusChanged(int which) {
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ public void processBroadcastIntent(Intent intent) {
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ /* This notification will arrive on the background thread */
+ public void onAccountsUpdated(Account[] accounts) {
+ // Refresh to catch any changed accounts
+ loadAccountsInBackground();
+ }
+
+ /**
+ * Returns instantly if accounts and account types have already been loaded. Otherwise waits for
+ * the background thread to complete the loading.
+ */
+ void ensureAccountsLoaded() {
+ CountDownLatch latch = mInitializationLatch;
+ if (latch == null) {
+ return;
+ }
+ while (true) {
+ try {
+ latch.await();
+ return;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * Loads account list and corresponding account types (potentially with data sets). Always called
+ * on a background thread.
+ */
+ protected void loadAccountsInBackground() {
+ if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+ Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
+ }
+ TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
+ final long startTime = SystemClock.currentThreadTimeMillis();
+ final long startTimeWall = SystemClock.elapsedRealtime();
+
+ // Account types, keyed off the account type and data set concatenation.
+ final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = new ArrayMap<>();
+
+ // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can
+ // be multiple account types (with different data sets) for the same type of account, each
+ // type string may have multiple AccountType entries.
+ final Map<String, List<AccountType>> accountTypesByType = new ArrayMap<>();
+
+ final List<AccountWithDataSet> allAccounts = new ArrayList<>();
+ final List<AccountWithDataSet> contactWritableAccounts = new ArrayList<>();
+ final List<AccountWithDataSet> groupWritableAccounts = new ArrayList<>();
+ final Set<String> extensionPackages = new HashSet<>();
+
+ final AccountManager am = mAccountManager;
+
+ final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
+ final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
+
+ // First process sync adapters to find any that provide contact data.
+ for (SyncAdapterType sync : syncs) {
+ if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
+ // Skip sync adapters that don't provide contact data.
+ continue;
+ }
+
+ // Look for the formatting details provided by each sync
+ // adapter, using the authenticator to find general resources.
+ final String type = sync.accountType;
+ final AuthenticatorDescription auth = findAuthenticator(auths, type);
+ if (auth == null) {
+ Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
+ continue;
+ }
+
+ AccountType accountType;
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
+ accountType = new GoogleAccountType(mContext, auth.packageName);
+ } else if (ExchangeAccountType.isExchangeType(type)) {
+ accountType = new ExchangeAccountType(mContext, auth.packageName, type);
+ } else if (SamsungAccountType.isSamsungAccountType(mContext, type, auth.packageName)) {
+ accountType = new SamsungAccountType(mContext, auth.packageName, type);
+ } else {
+ Log.d(
+ TAG, "Registering external account type=" + type + ", packageName=" + auth.packageName);
+ accountType = new ExternalAccountType(mContext, auth.packageName, false);
+ }
+ if (!accountType.isInitialized()) {
+ if (accountType.isEmbedded()) {
+ throw new IllegalStateException(
+ "Problem initializing embedded type " + accountType.getClass().getCanonicalName());
+ } else {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ }
+
+ accountType.accountType = auth.type;
+ accountType.titleRes = auth.labelId;
+ accountType.iconRes = auth.iconId;
+
+ addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+
+ // Check to see if the account type knows of any other non-sync-adapter packages
+ // that may provide other data sets of contact data.
+ extensionPackages.addAll(accountType.getExtensionPackageNames());
+ }
+
+ // If any extension packages were specified, process them as well.
+ if (!extensionPackages.isEmpty()) {
+ Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
+ for (String extensionPackage : extensionPackages) {
+ ExternalAccountType accountType = new ExternalAccountType(mContext, extensionPackage, true);
+ if (!accountType.isInitialized()) {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ if (!accountType.hasContactsMetadata()) {
+ Log.w(
+ TAG,
+ "Skipping extension package "
+ + extensionPackage
+ + " because"
+ + " it doesn't have the CONTACTS_STRUCTURE metadata");
+ continue;
+ }
+ if (TextUtils.isEmpty(accountType.accountType)) {
+ Log.w(
+ TAG,
+ "Skipping extension package "
+ + extensionPackage
+ + " because"
+ + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
+ + " attribute");
+ continue;
+ }
+ Log.d(
+ TAG,
+ "Registering extension package account type="
+ + accountType.accountType
+ + ", dataSet="
+ + accountType.dataSet
+ + ", packageName="
+ + extensionPackage);
+
+ addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+ }
+ }
+ timings.addSplit("Loaded account types");
+
+ // Map in accounts to associate the account names with each account type entry.
+ Account[] accounts = mAccountManager.getAccounts();
+ for (Account account : accounts) {
+ boolean syncable = ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
+
+ if (syncable) {
+ List<AccountType> accountTypes = accountTypesByType.get(account.type);
+ if (accountTypes != null) {
+ // Add an account-with-data-set entry for each account type that is
+ // authenticated by this account.
+ for (AccountType accountType : accountTypes) {
+ AccountWithDataSet accountWithDataSet =
+ new AccountWithDataSet(account.name, account.type, accountType.dataSet);
+ allAccounts.add(accountWithDataSet);
+ if (accountType.areContactsWritable()) {
+ contactWritableAccounts.add(accountWithDataSet);
+ }
+ if (accountType.isGroupMembershipEditable()) {
+ groupWritableAccounts.add(accountWithDataSet);
+ }
+ }
+ }
+ }
+ }
+
+ Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
+ Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR);
+ Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR);
+
+ timings.addSplit("Loaded accounts");
+
+ synchronized (this) {
+ mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
+ mAccounts = allAccounts;
+ mContactWritableAccounts = contactWritableAccounts;
+ mGroupWritableAccounts = groupWritableAccounts;
+ mInvitableAccountTypes =
+ findAllInvitableAccountTypes(mContext, allAccounts, accountTypesByTypeAndDataSet);
+ }
+
+ timings.dumpToLog();
+ final long endTimeWall = SystemClock.elapsedRealtime();
+ final long endTime = SystemClock.currentThreadTimeMillis();
+
+ Log.i(
+ TAG,
+ "Loaded meta-data for "
+ + mAccountTypesWithDataSets.size()
+ + " account types, "
+ + mAccounts.size()
+ + " accounts in "
+ + (endTimeWall - startTimeWall)
+ + "ms(wall) "
+ + (endTime - startTime)
+ + "ms(cpu)");
+
+ if (mInitializationLatch != null) {
+ mInitializationLatch.countDown();
+ mInitializationLatch = null;
+ }
+ if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+ Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
+ }
+
+ // Check filter validity since filter may become obsolete after account update. It must be
+ // done from UI thread.
+ mMainThreadHandler.post(mCheckFilterValidityRunnable);
+ }
+
+ // Bookkeeping method for tracking the known account types in the given maps.
+ private void addAccountType(
+ AccountType accountType,
+ Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
+ Map<String, List<AccountType>> accountTypesByType) {
+ accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
+ List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
+ if (accountsForType == null) {
+ accountsForType = new ArrayList<>();
+ }
+ accountsForType.add(accountType);
+ accountTypesByType.put(accountType.accountType, accountsForType);
+ }
+
+ /** Return list of all known, contact writable {@link AccountWithDataSet}'s. */
+ @Override
+ public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
+ ensureAccountsLoaded();
+ return contactWritableOnly ? mContactWritableAccounts : mAccounts;
+ }
+
+ /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */
+ public List<AccountWithDataSet> getGroupWritableAccounts() {
+ ensureAccountsLoaded();
+ return mGroupWritableAccounts;
+ }
+
+ /**
+ * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
+ * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
+ * {@link FallbackAccountType}.
+ */
+ @Override
+ public DataKind getKindOrFallback(AccountType type, String mimeType) {
+ ensureAccountsLoaded();
+ DataKind kind = null;
+
+ // Try finding account type and kind matching request
+ if (type != null) {
+ kind = type.getKindForMimetype(mimeType);
+ }
+
+ if (kind == null) {
+ // Nothing found, so try fallback as last resort
+ kind = mFallbackAccountType.getKindForMimetype(mimeType);
+ }
+
+ if (kind == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
+ }
+ }
+
+ return kind;
+ }
+
+ /** Return {@link AccountType} for the given account type and data set. */
+ @Override
+ public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+ ensureAccountsLoaded();
+ synchronized (this) {
+ AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
+ return type != null ? type : mFallbackAccountType;
+ }
+ }
+
+ /**
+ * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
+ * support the "invite" feature and have one or more account. This is an unfiltered list. See
+ * {@link #getUsableInvitableAccountTypes()}.
+ */
+ private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
+ ensureAccountsLoaded();
+ return mInvitableAccountTypes;
+ }
+
+ @Override
+ public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+ ensureAccountsLoaded();
+ // Since this method is not thread-safe, it's possible for multiple threads to encounter
+ // the situation where (1) the cache has not been initialized yet or
+ // (2) an async task to refresh the account type list in the cache has already been
+ // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
+ // while we compute the actual result in the background. We use this approach instead of
+ // using "synchronized" because computing the account type list involves a DB read, and
+ // can potentially cause a deadlock situation if this method is called from code which
+ // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
+ // account types for a short period of time seems more manageable than enforcing the
+ // context in which this method is called.
+
+ // Computing the list of usable invitable account types is done on the fly as requested.
+ // If this method has never been called before, then block until the list has been computed.
+ if (!mInvitablesCacheIsInitialized.get()) {
+ mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
+ mInvitablesCacheIsInitialized.set(true);
+ } else {
+ // Otherwise, there is a value in the cache. If the value has expired and
+ // an async task has not already been started by another thread, then kick off a new
+ // async task to compute the list.
+ if (mInvitableAccountTypeCache.isExpired()
+ && mInvitablesTaskIsRunning.compareAndSet(false, true)) {
+ new FindInvitablesTask().execute();
+ }
+ }
+
+ return mInvitableAccountTypeCache.getCachedValue();
+ }
+
+ /**
+ * Return all usable {@link AccountType}s that support the "invite" feature from the list of all
+ * potential invitable account types (retrieved from {@link #getAllInvitableAccountTypes}). A
+ * usable invitable account type means: (1) there is at least 1 raw contact in the database with
+ * that account type, and (2) the app contributing the account type is not disabled.
+ *
+ * <p>Warning: Don't use on the UI thread because this can scan the database.
+ */
+ private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
+ Context context) {
+ Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
+ if (allInvitables.isEmpty()) {
+ return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+ }
+
+ final Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
+ result.putAll(allInvitables);
+
+ final PackageManager packageManager = context.getPackageManager();
+ for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
+ AccountType accountType = allInvitables.get(accountTypeWithDataSet);
+
+ // Make sure that account types don't come from apps that are disabled.
+ Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, SAMPLE_CONTACT_URI);
+ if (invitableIntent == null) {
+ result.remove(accountTypeWithDataSet);
+ continue;
+ }
+ ResolveInfo resolveInfo =
+ packageManager.resolveActivity(invitableIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo == null) {
+ // If we can't find an activity to start for this intent, then there's no point in
+ // showing this option to the user.
+ result.remove(accountTypeWithDataSet);
+ continue;
+ }
+
+ // Make sure that there is at least 1 raw contact with this account type. This check
+ // is non-trivial and should not be done on the UI thread.
+ if (!accountTypeWithDataSet.hasData(context)) {
+ result.remove(accountTypeWithDataSet);
+ }
+ }
+
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+ ensureAccountsLoaded();
+ final List<AccountType> accountTypes = new ArrayList<>();
+ synchronized (this) {
+ for (AccountType type : mAccountTypesWithDataSets.values()) {
+ if (!contactWritableOnly || type.areContactsWritable()) {
+ accountTypes.add(type);
+ }
+ }
+ }
+ return accountTypes;
+ }
+
+ /**
+ * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a {@link
+ * Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only for {@link
+ * #TIME_TO_LIVE} milliseconds.
+ */
+ private static final class InvitableAccountTypeCache {
+
+ /**
+ * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds has
+ * elapsed.
+ */
+ private static final long TIME_TO_LIVE = 60000;
+
+ private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
+
+ private long mTimeLastSet;
+
+ /**
+ * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
+ * otherwise.
+ */
+ public boolean isExpired() {
+ return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
+ }
+
+ /**
+ * Returns the cached value. Note that the caller is responsible for checking {@link
+ * #isExpired()} to ensure that the value is not stale.
+ */
+ public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
+ return mInvitableAccountTypes;
+ }
+
+ public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
+ mInvitableAccountTypes = map;
+ mTimeLastSet = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Background task to find all usable {@link AccountType}s that support the "invite" feature from
+ * the list of all potential invitable account types. Once the work is completed, the list of
+ * account types is stored in the {@link AccountTypeManager}'s {@link InvitableAccountTypeCache}.
+ */
+ private class FindInvitablesTask
+ extends AsyncTask<Void, Void, Map<AccountTypeWithDataSet, AccountType>> {
+
+ @Override
+ protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
+ return findUsableInvitableAccountTypes(mContext);
+ }
+
+ @Override
+ protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
+ mInvitableAccountTypeCache.setCachedValue(accountTypes);
+ mInvitablesTaskIsRunning.set(false);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/BuilderWrapper.java b/java/com/android/contacts/common/model/BuilderWrapper.java
new file mode 100644
index 000000000..9c666e59c
--- /dev/null
+++ b/java/com/android/contacts/common/model/BuilderWrapper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation.Builder;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs. Since ContentProviderOperation is usually
+ * created by Builder and we don’t have access to the type via Builder, so we need to create a
+ * wrapper class for Builder first and include type. Then we could use the builder and the type in
+ * this class to create a wrapper of ContentProviderOperation.
+ */
+public class BuilderWrapper {
+
+ private Builder mBuilder;
+ private int mType;
+
+ public BuilderWrapper(Builder builder, int type) {
+ mBuilder = builder;
+ mType = type;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int mType) {
+ this.mType = mType;
+ }
+
+ public Builder getBuilder() {
+ return mBuilder;
+ }
+
+ public void setBuilder(Builder mBuilder) {
+ this.mBuilder = mBuilder;
+ }
+}
diff --git a/java/com/android/contacts/common/model/CPOWrapper.java b/java/com/android/contacts/common/model/CPOWrapper.java
new file mode 100644
index 000000000..4a67e6700
--- /dev/null
+++ b/java/com/android/contacts/common/model/CPOWrapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs.
+ */
+public class CPOWrapper {
+
+ private ContentProviderOperation mOperation;
+ private int mType;
+
+ public CPOWrapper(ContentProviderOperation builder, int type) {
+ mOperation = builder;
+ mType = type;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int type) {
+ this.mType = type;
+ }
+
+ public ContentProviderOperation getOperation() {
+ return mOperation;
+ }
+
+ public void setOperation(ContentProviderOperation operation) {
+ this.mOperation = operation;
+ }
+}
diff --git a/java/com/android/contacts/common/model/Contact.java b/java/com/android/contacts/common/model/Contact.java
new file mode 100644
index 000000000..ad0b66efe
--- /dev/null
+++ b/java/com/android/contacts/common/model/Contact.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.support.annotation.VisibleForTesting;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+
+/**
+ * A Contact represents a single person or logical entity as perceived by the user. The information
+ * about a contact can come from multiple data sources, which are each represented by a RawContact
+ * object. Thus, a Contact is associated with a collection of RawContact objects.
+ *
+ * <p>The aggregation of raw contacts into a single contact is performed automatically, and it is
+ * also possible for users to manually split and join raw contacts into various contacts.
+ *
+ * <p>Only the {@link ContactLoader} class can create a Contact object with various flags to allow
+ * partial loading of contact data. Thus, an instance of this class should be treated as a read-only
+ * object.
+ */
+public class Contact {
+
+ private final Uri mRequestedUri;
+ private final Uri mLookupUri;
+ private final Uri mUri;
+ private final long mDirectoryId;
+ private final String mLookupKey;
+ private final long mId;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+ private final long mPhotoId;
+ private final String mPhotoUri;
+ private final String mDisplayName;
+ private final String mAltDisplayName;
+ private final String mPhoneticName;
+ private final boolean mStarred;
+ private final Integer mPresence;
+ private final boolean mSendToVoicemail;
+ private final String mCustomRingtone;
+ private final boolean mIsUserProfile;
+ private final Contact.Status mStatus;
+ private final Exception mException;
+ private ImmutableList<RawContact> mRawContacts;
+ private ImmutableList<AccountType> mInvitableAccountTypes;
+ private String mDirectoryDisplayName;
+ private String mDirectoryType;
+ private String mDirectoryAccountType;
+ private String mDirectoryAccountName;
+ private int mDirectoryExportSupport;
+ private ImmutableList<GroupMetaData> mGroups;
+ private byte[] mPhotoBinaryData;
+ /**
+ * Small version of the contact photo loaded from a blob instead of from a file. If a large
+ * contact photo is not available yet, then this has the same value as mPhotoBinaryData.
+ */
+ private byte[] mThumbnailPhotoBinaryData;
+
+ /** Constructor for special results, namely "no contact found" and "error". */
+ private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
+ if (status == Status.ERROR && exception == null) {
+ throw new IllegalArgumentException("ERROR result must have exception");
+ }
+ mStatus = status;
+ mException = exception;
+ mRequestedUri = requestedUri;
+ mLookupUri = null;
+ mUri = null;
+ mDirectoryId = -1;
+ mLookupKey = null;
+ mId = -1;
+ mRawContacts = null;
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
+ mPhotoId = -1;
+ mPhotoUri = null;
+ mDisplayName = null;
+ mAltDisplayName = null;
+ mPhoneticName = null;
+ mStarred = false;
+ mPresence = null;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = false;
+ mCustomRingtone = null;
+ mIsUserProfile = false;
+ }
+
+ /** Constructor to call when contact was found */
+ public Contact(
+ Uri requestedUri,
+ Uri uri,
+ Uri lookupUri,
+ long directoryId,
+ String lookupKey,
+ long id,
+ long nameRawContactId,
+ int displayNameSource,
+ long photoId,
+ String photoUri,
+ String displayName,
+ String altDisplayName,
+ String phoneticName,
+ boolean starred,
+ Integer presence,
+ boolean sendToVoicemail,
+ String customRingtone,
+ boolean isUserProfile) {
+ mStatus = Status.LOADED;
+ mException = null;
+ mRequestedUri = requestedUri;
+ mLookupUri = lookupUri;
+ mUri = uri;
+ mDirectoryId = directoryId;
+ mLookupKey = lookupKey;
+ mId = id;
+ mRawContacts = null;
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ mPhotoId = photoId;
+ mPhotoUri = photoUri;
+ mDisplayName = displayName;
+ mAltDisplayName = altDisplayName;
+ mPhoneticName = phoneticName;
+ mStarred = starred;
+ mPresence = presence;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = sendToVoicemail;
+ mCustomRingtone = customRingtone;
+ mIsUserProfile = isUserProfile;
+ }
+
+ public Contact(Uri requestedUri, Contact from) {
+ mRequestedUri = requestedUri;
+
+ mStatus = from.mStatus;
+ mException = from.mException;
+ mLookupUri = from.mLookupUri;
+ mUri = from.mUri;
+ mDirectoryId = from.mDirectoryId;
+ mLookupKey = from.mLookupKey;
+ mId = from.mId;
+ mNameRawContactId = from.mNameRawContactId;
+ mDisplayNameSource = from.mDisplayNameSource;
+ mPhotoId = from.mPhotoId;
+ mPhotoUri = from.mPhotoUri;
+ mDisplayName = from.mDisplayName;
+ mAltDisplayName = from.mAltDisplayName;
+ mPhoneticName = from.mPhoneticName;
+ mStarred = from.mStarred;
+ mPresence = from.mPresence;
+ mRawContacts = from.mRawContacts;
+ mInvitableAccountTypes = from.mInvitableAccountTypes;
+
+ mDirectoryDisplayName = from.mDirectoryDisplayName;
+ mDirectoryType = from.mDirectoryType;
+ mDirectoryAccountType = from.mDirectoryAccountType;
+ mDirectoryAccountName = from.mDirectoryAccountName;
+ mDirectoryExportSupport = from.mDirectoryExportSupport;
+
+ mGroups = from.mGroups;
+
+ mPhotoBinaryData = from.mPhotoBinaryData;
+ mSendToVoicemail = from.mSendToVoicemail;
+ mCustomRingtone = from.mCustomRingtone;
+ mIsUserProfile = from.mIsUserProfile;
+ }
+
+ public static Contact forError(Uri requestedUri, Exception exception) {
+ return new Contact(requestedUri, Status.ERROR, exception);
+ }
+
+ public static Contact forNotFound(Uri requestedUri) {
+ return new Contact(requestedUri, Status.NOT_FOUND, null);
+ }
+
+ /** @param exportSupport See {@link Directory#EXPORT_SUPPORT}. */
+ public void setDirectoryMetaData(
+ String displayName,
+ String directoryType,
+ String accountType,
+ String accountName,
+ int exportSupport) {
+ mDirectoryDisplayName = displayName;
+ mDirectoryType = directoryType;
+ mDirectoryAccountType = accountType;
+ mDirectoryAccountName = accountName;
+ mDirectoryExportSupport = exportSupport;
+ }
+
+ /**
+ * Returns the URI for the contact that contains both the lookup key and the ID. This is the best
+ * URI to reference a contact. For directory contacts, this is the same a the URI as returned by
+ * {@link #getUri()}
+ */
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+
+ /**
+ * Returns the contact Uri that was passed to the provider to make the query. This is the same as
+ * the requested Uri, unless the requested Uri doesn't specify a Contact: If it either references
+ * a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will always reference the full
+ * aggregate contact.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /** Returns the contact ID. */
+ @VisibleForTesting
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * @return true when an exception happened during loading, in which case {@link #getException}
+ * returns the actual exception object.
+ */
+ public boolean isError() {
+ return mStatus == Status.ERROR;
+ }
+
+ public Exception getException() {
+ return mException;
+ }
+
+ /** @return true if the specified contact is successfully loaded. */
+ public boolean isLoaded() {
+ return mStatus == Status.LOADED;
+ }
+
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+
+ public long getPhotoId() {
+ return mPhotoId;
+ }
+
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public boolean getStarred() {
+ return mStarred;
+ }
+
+ public Integer getPresence() {
+ return mPresence;
+ }
+
+ /**
+ * This can return non-null invitable account types only if the {@link ContactLoader} was
+ * configured to load invitable account types in its constructor.
+ */
+ public ImmutableList<AccountType> getInvitableAccountTypes() {
+ return mInvitableAccountTypes;
+ }
+
+ /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
+ mInvitableAccountTypes = accountTypes;
+ }
+
+ public ImmutableList<RawContact> getRawContacts() {
+ return mRawContacts;
+ }
+
+ /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
+ mRawContacts = rawContacts;
+ }
+
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public boolean isDirectoryEntry() {
+ return mDirectoryId != -1
+ && mDirectoryId != Directory.DEFAULT
+ && mDirectoryId != Directory.LOCAL_INVISIBLE;
+ }
+
+ /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
+ mPhotoBinaryData = photoBinaryData;
+ }
+
+ public byte[] getThumbnailPhotoBinaryData() {
+ return mThumbnailPhotoBinaryData;
+ }
+
+ /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) {
+ mThumbnailPhotoBinaryData = photoBinaryData;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ if (mRawContacts.size() != 1) {
+ throw new IllegalStateException("Cannot extract content values from an aggregated contact");
+ }
+
+ RawContact rawContact = mRawContacts.get(0);
+ ArrayList<ContentValues> result = rawContact.getContentValues();
+
+ // If the photo was loaded using the URI, create an entry for the photo
+ // binary data.
+ if (mPhotoId == 0 && mPhotoBinaryData != null) {
+ ContentValues photo = new ContentValues();
+ photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ photo.put(Photo.PHOTO, mPhotoBinaryData);
+ result.add(photo);
+ }
+
+ return result;
+ }
+
+ /**
+ * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
+ * load group metadata in its constructor.
+ */
+ public ImmutableList<GroupMetaData> getGroupMetaData() {
+ return mGroups;
+ }
+
+ /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
+ mGroups = groups;
+ }
+
+ public boolean isUserProfile() {
+ return mIsUserProfile;
+ }
+
+ @Override
+ public String toString() {
+ return "{requested="
+ + mRequestedUri
+ + ",lookupkey="
+ + mLookupKey
+ + ",uri="
+ + mUri
+ + ",status="
+ + mStatus
+ + "}";
+ }
+
+ private enum Status {
+ /** Contact is successfully loaded */
+ LOADED,
+ /** There was an error loading the contact */
+ ERROR,
+ /** Contact is not found */
+ NOT_FOUND,
+ }
+}
diff --git a/java/com/android/contacts/common/model/ContactLoader.java b/java/com/android/contacts/common/model/ContactLoader.java
new file mode 100644
index 000000000..eb16bffcd
--- /dev/null
+++ b/java/com/android/contacts/common/model/ContactLoader.java
@@ -0,0 +1,998 @@
+/*
+ * Copyright (C) 2010 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
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.PhotoDataItem;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.compat.CompatUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Loads a single Contact and all it constituent RawContacts. */
+public class ContactLoader extends AsyncTaskLoader<Contact> {
+
+ private static final String TAG = ContactLoader.class.getSimpleName();
+
+ /** A short-lived cache that can be set by {@link #cacheResult()} */
+ private static Contact sCachedResult = null;
+
+ private final Uri mRequestedUri;
+ private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
+ private Uri mLookupUri;
+ private boolean mLoadGroupMetaData;
+ private boolean mLoadInvitableAccountTypes;
+ private boolean mPostViewNotification;
+ private boolean mComputeFormattedPhoneNumber;
+ private Contact mContact;
+ private ForceLoadContentObserver mObserver;
+
+ public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
+ this(context, lookupUri, false, false, postViewNotification, false);
+ }
+
+ public ContactLoader(
+ Context context,
+ Uri lookupUri,
+ boolean loadGroupMetaData,
+ boolean loadInvitableAccountTypes,
+ boolean postViewNotification,
+ boolean computeFormattedPhoneNumber) {
+ super(context);
+ mLookupUri = lookupUri;
+ mRequestedUri = lookupUri;
+ mLoadGroupMetaData = loadGroupMetaData;
+ mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+ mPostViewNotification = postViewNotification;
+ mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
+ }
+
+ /**
+ * Parses a {@link Contact} stored as a JSON string in a lookup URI.
+ *
+ * @param lookupUri The contact information to parse .
+ * @return The parsed {@code Contact} information.
+ */
+ public static Contact parseEncodedContactEntity(Uri lookupUri) {
+ try {
+ return loadEncodedContactEntity(lookupUri, lookupUri);
+ } catch (JSONException je) {
+ return null;
+ }
+ }
+
+ private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException {
+ final String jsonString = uri.getEncodedFragment();
+ final JSONObject json = new JSONObject(jsonString);
+
+ final long directoryId =
+ Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
+
+ final String displayName = json.optString(Contacts.DISPLAY_NAME);
+ final String altDisplayName = json.optString(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+ final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
+ final String photoUri = json.optString(Contacts.PHOTO_URI, null);
+ final Contact contact =
+ new Contact(
+ uri,
+ uri,
+ lookupUri,
+ directoryId,
+ null /* lookupKey */,
+ -1 /* id */,
+ -1 /* nameRawContactId */,
+ displayNameSource,
+ 0 /* photoId */,
+ photoUri,
+ displayName,
+ altDisplayName,
+ null /* phoneticName */,
+ false /* starred */,
+ null /* presence */,
+ false /* sendToVoicemail */,
+ null /* customRingtone */,
+ false /* isUserProfile */);
+
+ final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
+ final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
+ if (accountName != null) {
+ final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
+ contact.setDirectoryMetaData(
+ directoryName,
+ null,
+ accountName,
+ accountType,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
+ } else {
+ contact.setDirectoryMetaData(
+ directoryName,
+ null,
+ null,
+ null,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
+ }
+
+ final ContentValues values = new ContentValues();
+ values.put(Data._ID, -1);
+ values.put(Data.CONTACT_ID, -1);
+ final RawContact rawContact = new RawContact(values);
+
+ final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+ final Iterator keys = items.keys();
+ while (keys.hasNext()) {
+ final String mimetype = (String) keys.next();
+
+ // Could be single object or array.
+ final JSONObject obj = items.optJSONObject(mimetype);
+ if (obj == null) {
+ final JSONArray array = items.getJSONArray(mimetype);
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject item = array.getJSONObject(i);
+ processOneRecord(rawContact, item, mimetype);
+ }
+ } else {
+ processOneRecord(rawContact, obj, mimetype);
+ }
+ }
+
+ contact.setRawContacts(new ImmutableList.Builder<RawContact>().add(rawContact).build());
+ return contact;
+ }
+
+ private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
+ throws JSONException {
+ final ContentValues itemValues = new ContentValues();
+ itemValues.put(Data.MIMETYPE, mimetype);
+ itemValues.put(Data._ID, -1);
+
+ final Iterator iterator = item.keys();
+ while (iterator.hasNext()) {
+ String name = (String) iterator.next();
+ final Object o = item.get(name);
+ if (o instanceof String) {
+ itemValues.put(name, (String) o);
+ } else if (o instanceof Integer) {
+ itemValues.put(name, (Integer) o);
+ }
+ }
+ rawContact.addDataItemValues(itemValues);
+ }
+
+ @Override
+ public Contact loadInBackground() {
+ Log.e(TAG, "loadInBackground=" + mLookupUri);
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mLookupUri);
+ final Contact cachedResult = sCachedResult;
+ sCachedResult = null;
+ // Is this the same Uri as what we had before already? In that case, reuse that result
+ final Contact result;
+ final boolean resultIsCached;
+ if (cachedResult != null && UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
+ // We are using a cached result from earlier. Below, we should make sure
+ // we are not doing any more network or disc accesses
+ result = new Contact(mRequestedUri, cachedResult);
+ resultIsCached = true;
+ } else {
+ if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
+ result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri);
+ } else {
+ result = loadContactEntity(resolver, uriCurrentFormat);
+ }
+ resultIsCached = false;
+ }
+ if (result.isLoaded()) {
+ if (result.isDirectoryEntry()) {
+ if (!resultIsCached) {
+ loadDirectoryMetaData(result);
+ }
+ } else if (mLoadGroupMetaData) {
+ if (result.getGroupMetaData() == null) {
+ loadGroupMetaData(result);
+ }
+ }
+ if (mComputeFormattedPhoneNumber) {
+ computeFormattedPhoneNumbers(result);
+ }
+ if (!resultIsCached) {
+ loadPhotoBinaryData(result);
+ }
+
+ // Note ME profile should never have "Add connection"
+ if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
+ loadInvitableAccountTypes(result);
+ }
+ }
+ return result;
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+ return Contact.forError(mRequestedUri, e);
+ }
+ }
+
+ private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
+ Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+ Cursor cursor =
+ resolver.query(entityUri, ContactQuery.COLUMNS, null, null, Contacts.Entity.RAW_CONTACT_ID);
+ if (cursor == null) {
+ Log.e(TAG, "No cursor returned in loadContactEntity");
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ // Create the loaded contact starting with the header data.
+ Contact contact = loadContactHeaderData(cursor, contactUri);
+
+ // Fill in the raw contacts, which is wrapped in an Entity and any
+ // status data. Initially, result has empty entities and statuses.
+ long currentRawContactId = -1;
+ RawContact rawContact = null;
+ ImmutableList.Builder<RawContact> rawContactsBuilder =
+ new ImmutableList.Builder<RawContact>();
+ do {
+ long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
+ if (rawContactId != currentRawContactId) {
+ // First time to see this raw contact id, so create a new entity, and
+ // add it to the result's entities.
+ currentRawContactId = rawContactId;
+ rawContact = new RawContact(loadRawContactValues(cursor));
+ rawContactsBuilder.add(rawContact);
+ }
+ if (!cursor.isNull(ContactQuery.DATA_ID)) {
+ ContentValues data = loadDataValues(cursor);
+ rawContact.addDataItemValues(data);
+ }
+ } while (cursor.moveToNext());
+
+ contact.setRawContacts(rawContactsBuilder.build());
+
+ return contact;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger photo
+ * will also be stored if available.
+ */
+ private void loadPhotoBinaryData(Contact contactData) {
+ loadThumbnailBinaryData(contactData);
+
+ // Try to load the large photo from a file using the photo URI.
+ String photoUri = contactData.getPhotoUri();
+ if (photoUri != null) {
+ try {
+ final InputStream inputStream;
+ final AssetFileDescriptor fd;
+ final Uri uri = Uri.parse(photoUri);
+ final String scheme = uri.getScheme();
+ if ("http".equals(scheme) || "https".equals(scheme)) {
+ // Support HTTP urls that might come from extended directories
+ inputStream = new URL(photoUri).openStream();
+ fd = null;
+ } else {
+ fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
+ inputStream = fd.createInputStream();
+ }
+ byte[] buffer = new byte[16 * 1024];
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ int size;
+ while ((size = inputStream.read(buffer)) != -1) {
+ baos.write(buffer, 0, size);
+ }
+ contactData.setPhotoBinaryData(baos.toByteArray());
+ } finally {
+ inputStream.close();
+ if (fd != null) {
+ fd.close();
+ }
+ }
+ return;
+ } catch (IOException ioe) {
+ // Just fall back to the case below.
+ }
+ }
+
+ // If we couldn't load from a file, fall back to the data blob.
+ contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData());
+ }
+
+ private void loadThumbnailBinaryData(Contact contactData) {
+ final long photoId = contactData.getPhotoId();
+ if (photoId <= 0) {
+ // No photo ID
+ return;
+ }
+
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ for (DataItem dataItem : rawContact.getDataItems()) {
+ if (dataItem.getId() == photoId) {
+ if (!(dataItem instanceof PhotoDataItem)) {
+ break;
+ }
+
+ final PhotoDataItem photo = (PhotoDataItem) dataItem;
+ contactData.setThumbnailPhotoBinaryData(photo.getPhoto());
+ break;
+ }
+ }
+ }
+ }
+
+ /** Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. */
+ private void loadInvitableAccountTypes(Contact contactData) {
+ final ImmutableList.Builder<AccountType> resultListBuilder =
+ new ImmutableList.Builder<AccountType>();
+ if (!contactData.isUserProfile()) {
+ Map<AccountTypeWithDataSet, AccountType> invitables =
+ AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+ if (!invitables.isEmpty()) {
+ final Map<AccountTypeWithDataSet, AccountType> resultMap = Maps.newHashMap(invitables);
+
+ // Remove the ones that already have a raw contact in the current contact
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ final AccountTypeWithDataSet type =
+ AccountTypeWithDataSet.get(
+ rawContact.getAccountTypeString(), rawContact.getDataSet());
+ resultMap.remove(type);
+ }
+
+ resultListBuilder.addAll(resultMap.values());
+ }
+ }
+
+ // Set to mInvitableAccountTypes
+ contactData.setInvitableAccountTypes(resultListBuilder.build());
+ }
+
+ /** Extracts Contact level columns from the cursor. */
+ private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+ final String directoryParameter =
+ contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ final long directoryId =
+ directoryParameter == null ? Directory.DEFAULT : Long.parseLong(directoryParameter);
+ final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+ final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+ final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+ final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+ final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+ final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+ final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+ final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+ final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+ final Integer presence =
+ cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+ ? null
+ : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+ final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+ final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+ final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+ Uri lookupUri;
+ if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+ lookupUri =
+ ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+ } else {
+ lookupUri = contactUri;
+ }
+
+ return new Contact(
+ mRequestedUri,
+ contactUri,
+ lookupUri,
+ directoryId,
+ lookupKey,
+ contactId,
+ nameRawContactId,
+ displayNameSource,
+ photoId,
+ photoUri,
+ displayName,
+ altDisplayName,
+ phoneticName,
+ starred,
+ presence,
+ sendToVoicemail,
+ customRingtone,
+ isUserProfile);
+ }
+
+ /** Extracts RawContact level columns from the cursor. */
+ private ContentValues loadRawContactValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+
+ return cv;
+ }
+
+ /** Extracts Data level columns from the cursor. */
+ private ContentValues loadDataValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED);
+ if (CompatUtils.isMarshmallowCompatible()) {
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE);
+ }
+
+ return cv;
+ }
+
+ private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) {
+ switch (cursor.getType(index)) {
+ case Cursor.FIELD_TYPE_NULL:
+ // don't put anything in the content values
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+ break;
+ default:
+ throw new IllegalStateException("Invalid or unhandled data type");
+ }
+ }
+
+ private void loadDirectoryMetaData(Contact result) {
+ long directoryId = result.getDirectoryId();
+
+ Cursor cursor =
+ getContext()
+ .getContentResolver()
+ .query(
+ ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+ DirectoryQuery.COLUMNS,
+ null,
+ null,
+ null);
+ if (cursor == null) {
+ return;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+ final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+ final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+ final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+ String directoryType = null;
+ if (!TextUtils.isEmpty(packageName)) {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ Resources resources = pm.getResourcesForApplication(packageName);
+ directoryType = resources.getString(typeResourceId);
+ } catch (NameNotFoundException e) {
+ Log.w(
+ TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId);
+ }
+ }
+
+ result.setDirectoryMetaData(
+ displayName, directoryType, accountType, accountName, exportSupport);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads groups meta-data for all groups associated with all constituent raw contacts' accounts.
+ */
+ private void loadGroupMetaData(Contact result) {
+ StringBuilder selection = new StringBuilder();
+ ArrayList<String> selectionArgs = new ArrayList<String>();
+ final HashSet<AccountKey> accountsSeen = new HashSet<>();
+ for (RawContact rawContact : result.getRawContacts()) {
+ final String accountName = rawContact.getAccountName();
+ final String accountType = rawContact.getAccountTypeString();
+ final String dataSet = rawContact.getDataSet();
+ final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet);
+ if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) {
+ accountsSeen.add(accountKey);
+ if (selection.length() != 0) {
+ selection.append(" OR ");
+ }
+ selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+ selectionArgs.add(accountName);
+ selectionArgs.add(accountType);
+
+ if (dataSet != null) {
+ selection.append(" AND " + Groups.DATA_SET + "=?");
+ selectionArgs.add(dataSet);
+ } else {
+ selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+ }
+ selection.append(")");
+ }
+ }
+ final ImmutableList.Builder<GroupMetaData> groupListBuilder =
+ new ImmutableList.Builder<GroupMetaData>();
+ final Cursor cursor =
+ getContext()
+ .getContentResolver()
+ .query(
+ Groups.CONTENT_URI,
+ GroupQuery.COLUMNS,
+ selection.toString(),
+ selectionArgs.toArray(new String[0]),
+ null);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+ final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+ final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+ final long groupId = cursor.getLong(GroupQuery.ID);
+ final String title = cursor.getString(GroupQuery.TITLE);
+ final boolean defaultGroup =
+ !cursor.isNull(GroupQuery.AUTO_ADD) && cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+ final boolean favorites =
+ !cursor.isNull(GroupQuery.FAVORITES) && cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+ groupListBuilder.add(
+ new GroupMetaData(
+ accountName, accountType, dataSet, groupId, title, defaultGroup, favorites));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ result.setGroupMetaData(groupListBuilder.build());
+ }
+
+ /**
+ * Iterates over all data items that represent phone numbers are tries to calculate a formatted
+ * number. This function can safely be called several times as no unformatted data is overwritten
+ */
+ private void computeFormattedPhoneNumbers(Contact contactData) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+ final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
+ final int rawContactCount = rawContacts.size();
+ for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
+ final RawContact rawContact = rawContacts.get(rawContactIndex);
+ final List<DataItem> dataItems = rawContact.getDataItems();
+ final int dataCount = dataItems.size();
+ for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
+ final DataItem dataItem = dataItems.get(dataIndex);
+ if (dataItem instanceof PhoneDataItem) {
+ final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
+ phoneDataItem.computeFormattedPhoneNumber(countryIso);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void deliverResult(Contact result) {
+ unregisterObserver();
+
+ // The creator isn't interested in any further updates
+ if (isReset() || result == null) {
+ return;
+ }
+
+ mContact = result;
+
+ if (result.isLoaded()) {
+ mLookupUri = result.getLookupUri();
+
+ if (!result.isDirectoryEntry()) {
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+ }
+
+ if (mPostViewNotification) {
+ // inform the source of the data that this contact is being looked at
+ postViewNotificationToSyncAdapter();
+ }
+ }
+
+ super.deliverResult(mContact);
+ }
+
+ /**
+ * Posts a message to the contributing sync adapters that have opted-in, notifying them that the
+ * contact has just been loaded
+ */
+ private void postViewNotificationToSyncAdapter() {
+ Context context = getContext();
+ for (RawContact rawContact : mContact.getRawContacts()) {
+ final long rawContactId = rawContact.getId();
+ if (mNotifiedRawContactIds.contains(rawContactId)) {
+ continue; // Already notified for this raw contact.
+ }
+ mNotifiedRawContactIds.add(rawContactId);
+ final AccountType accountType = rawContact.getAccountType(context);
+ final String serviceName = accountType.getViewContactNotifyServiceClassName();
+ final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
+ if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
+ final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Intent intent = new Intent();
+ intent.setClassName(servicePackageName, serviceName);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
+ try {
+ context.startService(intent);
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending message to source-app", e);
+ }
+ }
+ }
+ }
+
+ private void unregisterObserver() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public void setLookupUri(Uri lookupUri) {
+ mLookupUri = lookupUri;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mContact != null) {
+ deliverResult(mContact);
+ }
+
+ if (takeContentChanged() || mContact == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ cancelLoad();
+ unregisterObserver();
+ mContact = null;
+ }
+
+ /**
+ * Projection used for the query that loads all data for the entire contact (except for social
+ * stream items).
+ */
+ private static class ContactQuery {
+
+ public static final int NAME_RAW_CONTACT_ID = 0;
+ public static final int DISPLAY_NAME_SOURCE = 1;
+ public static final int LOOKUP_KEY = 2;
+ public static final int DISPLAY_NAME = 3;
+ public static final int ALT_DISPLAY_NAME = 4;
+ public static final int PHONETIC_NAME = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int STARRED = 7;
+ public static final int CONTACT_PRESENCE = 8;
+ public static final int CONTACT_STATUS = 9;
+ public static final int CONTACT_STATUS_TIMESTAMP = 10;
+ public static final int CONTACT_STATUS_RES_PACKAGE = 11;
+ public static final int CONTACT_STATUS_LABEL = 12;
+ public static final int CONTACT_ID = 13;
+ public static final int RAW_CONTACT_ID = 14;
+ public static final int ACCOUNT_NAME = 15;
+ public static final int ACCOUNT_TYPE = 16;
+ public static final int DATA_SET = 17;
+ public static final int DIRTY = 18;
+ public static final int VERSION = 19;
+ public static final int SOURCE_ID = 20;
+ public static final int SYNC1 = 21;
+ public static final int SYNC2 = 22;
+ public static final int SYNC3 = 23;
+ public static final int SYNC4 = 24;
+ public static final int DELETED = 25;
+ public static final int DATA_ID = 26;
+ public static final int DATA1 = 27;
+ public static final int DATA2 = 28;
+ public static final int DATA3 = 29;
+ public static final int DATA4 = 30;
+ public static final int DATA5 = 31;
+ public static final int DATA6 = 32;
+ public static final int DATA7 = 33;
+ public static final int DATA8 = 34;
+ public static final int DATA9 = 35;
+ public static final int DATA10 = 36;
+ public static final int DATA11 = 37;
+ public static final int DATA12 = 38;
+ public static final int DATA13 = 39;
+ public static final int DATA14 = 40;
+ public static final int DATA15 = 41;
+ public static final int DATA_SYNC1 = 42;
+ public static final int DATA_SYNC2 = 43;
+ public static final int DATA_SYNC3 = 44;
+ public static final int DATA_SYNC4 = 45;
+ public static final int DATA_VERSION = 46;
+ public static final int IS_PRIMARY = 47;
+ public static final int IS_SUPERPRIMARY = 48;
+ public static final int MIMETYPE = 49;
+ public static final int GROUP_SOURCE_ID = 50;
+ public static final int PRESENCE = 51;
+ public static final int CHAT_CAPABILITY = 52;
+ public static final int STATUS = 53;
+ public static final int STATUS_RES_PACKAGE = 54;
+ public static final int STATUS_ICON = 55;
+ public static final int STATUS_LABEL = 56;
+ public static final int STATUS_TIMESTAMP = 57;
+ public static final int PHOTO_URI = 58;
+ public static final int SEND_TO_VOICEMAIL = 59;
+ public static final int CUSTOM_RINGTONE = 60;
+ public static final int IS_USER_PROFILE = 61;
+ public static final int TIMES_USED = 62;
+ public static final int LAST_TIME_USED = 63;
+ public static final int CARRIER_PRESENCE = 64;
+ static final String[] COLUMNS_INTERNAL =
+ new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL,
+ Contacts.Entity.CONTACT_ID,
+ Contacts.Entity.RAW_CONTACT_ID,
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ RawContacts.DIRTY,
+ RawContacts.VERSION,
+ RawContacts.SOURCE_ID,
+ RawContacts.SYNC1,
+ RawContacts.SYNC2,
+ RawContacts.SYNC3,
+ RawContacts.SYNC4,
+ RawContacts.DELETED,
+ Contacts.Entity.DATA_ID,
+ Data.DATA1,
+ Data.DATA2,
+ Data.DATA3,
+ Data.DATA4,
+ Data.DATA5,
+ Data.DATA6,
+ Data.DATA7,
+ Data.DATA8,
+ Data.DATA9,
+ Data.DATA10,
+ Data.DATA11,
+ Data.DATA12,
+ Data.DATA13,
+ Data.DATA14,
+ Data.DATA15,
+ Data.SYNC1,
+ Data.SYNC2,
+ Data.SYNC3,
+ Data.SYNC4,
+ Data.DATA_VERSION,
+ Data.IS_PRIMARY,
+ Data.IS_SUPER_PRIMARY,
+ Data.MIMETYPE,
+ GroupMembership.GROUP_SOURCE_ID,
+ Data.PRESENCE,
+ Data.CHAT_CAPABILITY,
+ Data.STATUS,
+ Data.STATUS_RES_PACKAGE,
+ Data.STATUS_ICON,
+ Data.STATUS_LABEL,
+ Data.STATUS_TIMESTAMP,
+ Contacts.PHOTO_URI,
+ Contacts.SEND_TO_VOICEMAIL,
+ Contacts.CUSTOM_RINGTONE,
+ Contacts.IS_USER_PROFILE,
+ Data.TIMES_USED,
+ Data.LAST_TIME_USED
+ };
+ static final String[] COLUMNS;
+
+ static {
+ List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Data.CARRIER_PRESENCE);
+ }
+ COLUMNS = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+
+ /** Projection used for the query that loads all data for the entire contact. */
+ private static class DirectoryQuery {
+
+ public static final int DISPLAY_NAME = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int ACCOUNT_TYPE = 3;
+ public static final int ACCOUNT_NAME = 4;
+ public static final int EXPORT_SUPPORT = 5;
+ static final String[] COLUMNS =
+ new String[] {
+ Directory.DISPLAY_NAME,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.ACCOUNT_TYPE,
+ Directory.ACCOUNT_NAME,
+ Directory.EXPORT_SUPPORT,
+ };
+ }
+
+ private static class GroupQuery {
+
+ public static final int ACCOUNT_NAME = 0;
+ public static final int ACCOUNT_TYPE = 1;
+ public static final int DATA_SET = 2;
+ public static final int ID = 3;
+ public static final int TITLE = 4;
+ public static final int AUTO_ADD = 5;
+ public static final int FAVORITES = 6;
+ static final String[] COLUMNS =
+ new String[] {
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ Groups.DATA_SET,
+ Groups._ID,
+ Groups.TITLE,
+ Groups.AUTO_ADD,
+ Groups.FAVORITES,
+ };
+ }
+
+ private static class AccountKey {
+
+ private final String mAccountName;
+ private final String mAccountType;
+ private final String mDataSet;
+
+ public AccountKey(String accountName, String accountType, String dataSet) {
+ mAccountName = accountName;
+ mAccountType = accountType;
+ mDataSet = dataSet;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAccountName, mAccountType, mDataSet);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AccountKey)) {
+ return false;
+ }
+ final AccountKey other = (AccountKey) obj;
+ return Objects.equals(mAccountName, other.mAccountName)
+ && Objects.equals(mAccountType, other.mAccountType)
+ && Objects.equals(mDataSet, other.mDataSet);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/RawContact.java b/java/com/android/contacts/common/model/RawContact.java
new file mode 100644
index 000000000..9efc8a878
--- /dev/null
+++ b/java/com/android/contacts/common/model/RawContact.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * RawContact represents a single raw contact in the raw contacts database. It has specialized
+ * getters/setters for raw contact items, and also contains a collection of DataItem objects. A
+ * RawContact contains the information from a single account.
+ *
+ * <p>This allows RawContact objects to be thought of as a class with raw contact fields (like
+ * account type, name, data set, sync state, etc.) and a list of DataItem objects that represent
+ * contact information elements (like phone numbers, email, address, etc.).
+ */
+public final class RawContact implements Parcelable {
+
+ /** Create for building the parcelable. */
+ public static final Parcelable.Creator<RawContact> CREATOR =
+ new Parcelable.Creator<RawContact>() {
+
+ @Override
+ public RawContact createFromParcel(Parcel parcel) {
+ return new RawContact(parcel);
+ }
+
+ @Override
+ public RawContact[] newArray(int i) {
+ return new RawContact[i];
+ }
+ };
+
+ private final ContentValues mValues;
+ private final ArrayList<NamedDataItem> mDataItems;
+ private AccountTypeManager mAccountTypeManager;
+
+ /** A RawContact object can be created with or without a context. */
+ public RawContact() {
+ this(new ContentValues());
+ }
+
+ public RawContact(ContentValues values) {
+ mValues = values;
+ mDataItems = new ArrayList<NamedDataItem>();
+ }
+
+ /**
+ * Constructor for the parcelable.
+ *
+ * @param parcel The parcel to de-serialize from.
+ */
+ private RawContact(Parcel parcel) {
+ mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ mDataItems = new ArrayList<>();
+ parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
+ }
+
+ public static RawContact createFrom(Entity entity) {
+ final ContentValues values = entity.getEntityValues();
+ final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+ RawContact rawContact = new RawContact(values);
+ for (Entity.NamedContentValues subValue : subValues) {
+ rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
+ }
+ return rawContact;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mValues, i);
+ parcel.writeTypedList(mDataItems);
+ }
+
+ public AccountTypeManager getAccountTypeManager(Context context) {
+ if (mAccountTypeManager == null) {
+ mAccountTypeManager = AccountTypeManager.getInstance(context);
+ }
+ return mAccountTypeManager;
+ }
+
+ public ContentValues getValues() {
+ return mValues;
+ }
+
+ /** Returns the id of the raw contact. */
+ public Long getId() {
+ return getValues().getAsLong(RawContacts._ID);
+ }
+
+ /** Returns the account name of the raw contact. */
+ public String getAccountName() {
+ return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+ }
+
+ /** Returns the account type of the raw contact. */
+ public String getAccountTypeString() {
+ return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ }
+
+ /** Returns the data set of the raw contact. */
+ public String getDataSet() {
+ return getValues().getAsString(RawContacts.DATA_SET);
+ }
+
+ public boolean isDirty() {
+ return getValues().getAsBoolean(RawContacts.DIRTY);
+ }
+
+ public String getSourceId() {
+ return getValues().getAsString(RawContacts.SOURCE_ID);
+ }
+
+ public String getSync1() {
+ return getValues().getAsString(RawContacts.SYNC1);
+ }
+
+ public String getSync2() {
+ return getValues().getAsString(RawContacts.SYNC2);
+ }
+
+ public String getSync3() {
+ return getValues().getAsString(RawContacts.SYNC3);
+ }
+
+ public String getSync4() {
+ return getValues().getAsString(RawContacts.SYNC4);
+ }
+
+ public boolean isDeleted() {
+ return getValues().getAsBoolean(RawContacts.DELETED);
+ }
+
+ public long getContactId() {
+ return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
+ }
+
+ public boolean isStarred() {
+ return getValues().getAsBoolean(Contacts.STARRED);
+ }
+
+ public AccountType getAccountType(Context context) {
+ return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
+ }
+
+ /**
+ * Sets the account name, account type, and data set strings. Valid combinations for account-name,
+ * account-type, data-set 1) null, null, null (local account) 2) non-null, non-null, null (valid
+ * account without data-set) 3) non-null, non-null, non-null (valid account with data-set)
+ */
+ private void setAccount(String accountName, String accountType, String dataSet) {
+ final ContentValues values = getValues();
+ if (accountName == null) {
+ if (accountType == null && dataSet == null) {
+ // This is a local account
+ values.putNull(RawContacts.ACCOUNT_NAME);
+ values.putNull(RawContacts.ACCOUNT_TYPE);
+ values.putNull(RawContacts.DATA_SET);
+ return;
+ }
+ } else {
+ if (accountType != null) {
+ // This is a valid account, either with or without a dataSet.
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ if (dataSet == null) {
+ values.putNull(RawContacts.DATA_SET);
+ } else {
+ values.put(RawContacts.DATA_SET, dataSet);
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Not a valid combination of account name, type, and data set.");
+ }
+
+ public void setAccount(AccountWithDataSet accountWithDataSet) {
+ if (accountWithDataSet != null) {
+ setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet);
+ } else {
+ setAccount(null, null, null);
+ }
+ }
+
+ public void setAccountToLocal() {
+ setAccount(null, null, null);
+ }
+
+ /** Creates and inserts a DataItem object that wraps the content values, and returns it. */
+ public void addDataItemValues(ContentValues values) {
+ addNamedDataItemValues(Data.CONTENT_URI, values);
+ }
+
+ public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
+ final NamedDataItem namedItem = new NamedDataItem(uri, values);
+ mDataItems.add(namedItem);
+ return namedItem;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ final ArrayList<ContentValues> list = new ArrayList<>(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(dataItem.mContentValues);
+ }
+ }
+ return list;
+ }
+
+ public List<DataItem> getDataItems() {
+ final ArrayList<DataItem> list = new ArrayList<>(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(DataItem.createFrom(dataItem.mContentValues));
+ }
+ }
+ return list;
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RawContact: ").append(mValues);
+ for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+ sb.append("\n ").append(namedDataItem.mUri);
+ sb.append("\n -> ").append(namedDataItem.mContentValues);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mValues, mDataItems);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ RawContact other = (RawContact) obj;
+ return Objects.equals(mValues, other.mValues) && Objects.equals(mDataItems, other.mDataItems);
+ }
+
+ public static final class NamedDataItem implements Parcelable {
+
+ public static final Parcelable.Creator<NamedDataItem> CREATOR =
+ new Parcelable.Creator<NamedDataItem>() {
+
+ @Override
+ public NamedDataItem createFromParcel(Parcel parcel) {
+ return new NamedDataItem(parcel);
+ }
+
+ @Override
+ public NamedDataItem[] newArray(int i) {
+ return new NamedDataItem[i];
+ }
+ };
+ public final Uri mUri;
+ // This use to be a DataItem. DataItem creation is now delayed until the point of request
+ // since there is no benefit to storing them here due to the multiple inheritance.
+ // Eventually instanceof still has to be used anyways to determine which sub-class of
+ // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
+ // parcelable.
+ //
+ // Instead of having a common DataItem super class, we should refactor this to be a generic
+ // Object where the object is a concrete class that no longer relies on ContentValues.
+ // (this will also make the classes easier to use).
+ // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
+ // worse than having a DataItem.
+ public final ContentValues mContentValues;
+
+ public NamedDataItem(Uri uri, ContentValues values) {
+ this.mUri = uri;
+ this.mContentValues = values;
+ }
+
+ public NamedDataItem(Parcel parcel) {
+ this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
+ this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mUri, i);
+ parcel.writeParcelable(mContentValues, i);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUri, mContentValues);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final NamedDataItem other = (NamedDataItem) obj;
+ return Objects.equals(mUri, other.mUri)
+ && Objects.equals(mContentValues, other.mContentValues);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountType.java b/java/com/android/contacts/common/model/account/AccountType.java
new file mode 100644
index 000000000..1ae485a5f
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountType.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Internal structure that represents constraints and styles for a specific data source, such as the
+ * various data types they support, including details on how those types should be rendered and
+ * edited.
+ *
+ * <p>In the future this may be inflated from XML defined by a data source.
+ */
+public abstract class AccountType {
+
+ private static final String TAG = "AccountType";
+ /** {@link Comparator} to sort by {@link DataKind#weight}. */
+ private static Comparator<DataKind> sWeightComparator =
+ new Comparator<DataKind>() {
+ @Override
+ public int compare(DataKind object1, DataKind object2) {
+ return object1.weight - object2.weight;
+ }
+ };
+ /** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */
+ public String accountType = null;
+ /** The {@link RawContacts#DATA_SET} these constraints apply to. */
+ public String dataSet = null;
+ /**
+ * Package that resources should be loaded from. Will be null for embedded types, in which case
+ * resources are stored in this package itself.
+ *
+ * <p>TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link
+ * #getViewContactNotifyServicePackageName()}.
+ *
+ * <p>There's the following invariants: - {@link #syncAdapterPackageName} is always set to the
+ * actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value,
+ * unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of
+ * {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to
+ * {@link #resourcePackageName} for unit tests.
+ */
+ public String resourcePackageName;
+ /**
+ * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or
+ * the sync adapter (for external type, including extensions).
+ */
+ public String syncAdapterPackageName;
+
+ public int titleRes;
+ public int iconRes;
+ protected boolean mIsInitialized;
+ /** Set of {@link DataKind} supported by this source. */
+ private ArrayList<DataKind> mKinds = new ArrayList<>();
+ /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */
+ private Map<String, DataKind> mMimeKinds = new ArrayMap<>();
+
+ /**
+ * Return a string resource loaded from the given package (or the current package if {@code
+ * packageName} is null), unless {@code resId} is -1, in which case it returns {@code
+ * defaultValue}.
+ *
+ * <p>(The behavior is undefined if the resource or package doesn't exist.)
+ */
+ @VisibleForTesting
+ static CharSequence getResourceText(
+ Context context, String packageName, int resId, String defaultValue) {
+ if (resId != -1 && packageName != null) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getText(packageName, resId, null);
+ } else if (resId != -1) {
+ return context.getText(resId);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public static Drawable getDisplayIcon(
+ Context context, int titleRes, int iconRes, String syncAdapterPackageName) {
+ if (titleRes != -1 && syncAdapterPackageName != null) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getDrawable(syncAdapterPackageName, iconRes, null);
+ } else if (titleRes != -1) {
+ return context.getResources().getDrawable(iconRes);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Whether this account type was able to be fully initialized. This may be false if (for example)
+ * the package name associated with the account type could not be found.
+ */
+ public final boolean isInitialized() {
+ return mIsInitialized;
+ }
+
+ /**
+ * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType},
+ * {@link GoogleAccountType} or {@link ExternalAccountType}.
+ * <p>If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
+ * {@code false}) it's considered critical, and the application will crash. On the other hand
+ * if it's not an embedded type, we just skip loading the type.
+ */
+ public boolean isEmbedded() {
+ return true;
+ }
+
+ public boolean isExtension() {
+ return false;
+ }
+
+ /**
+ * @return True if contacts can be created and edited using this app. If false, there could still
+ * be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link
+ * #getCreateContactActivityClassName()}
+ */
+ public abstract boolean areContactsWritable();
+
+ /**
+ * Returns an optional custom edit activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getEditContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional custom new contact activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getCreateContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional custom invite contact activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getInviteContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional service that can be launched whenever a contact is being looked at. This
+ * allows the sync adapter to provide more up-to-date information.
+ *
+ * <p>The service class should reside in the sync adapter package as determined by {@link
+ * #getViewContactNotifyServicePackageName()}.
+ */
+ public String getViewContactNotifyServiceClassName() {
+ return null;
+ }
+
+ /**
+ * TODO This is way too hacky should be removed.
+ *
+ * <p>This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is
+ * the authenticator package name but the notification service is in the sync adapter package. See
+ * {@link #resourcePackageName} -- we should clean up those.
+ */
+ public String getViewContactNotifyServicePackageName() {
+ return syncAdapterPackageName;
+ }
+
+ /** Returns an optional Activity string that can be used to view the group. */
+ public String getViewGroupActivity() {
+ return null;
+ }
+
+ public CharSequence getDisplayLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
+ }
+
+ /** @return resource ID for the "invite contact" action label, or -1 if not defined. */
+ protected int getInviteContactActionResId() {
+ return -1;
+ }
+
+ /** @return resource ID for the "view group" label, or -1 if not defined. */
+ protected int getViewGroupLabelResId() {
+ return -1;
+ }
+
+ /** Returns {@link AccountTypeWithDataSet} for this type. */
+ public AccountTypeWithDataSet getAccountTypeAndDataSet() {
+ return AccountTypeWithDataSet.get(accountType, dataSet);
+ }
+
+ /**
+ * Returns a list of additional package names that should be inspected as additional external
+ * account types. This allows for a primary account type to indicate other packages that may not
+ * be sync adapters but which still provide contact data, perhaps under a separate data set within
+ * the account.
+ */
+ public List<String> getExtensionPackageNames() {
+ return new ArrayList<String>();
+ }
+
+ /**
+ * Returns an optional custom label for the "invite contact" action, which will be shown on the
+ * contact card. (If not defined, returns null.)
+ */
+ public CharSequence getInviteContactActionLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
+ }
+
+ /**
+ * Returns a label for the "view group" action. If not defined, this falls back to our own "View
+ * Updates" string
+ */
+ public CharSequence getViewGroupLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ final CharSequence customTitle =
+ getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
+
+ return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle;
+ }
+
+ public Drawable getDisplayIcon(Context context) {
+ return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName);
+ }
+
+ /** Whether or not groups created under this account type have editable membership lists. */
+ public abstract boolean isGroupMembershipEditable();
+
+ /** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */
+ public ArrayList<DataKind> getSortedDataKinds() {
+ // TODO: optimize by marking if already sorted
+ Collections.sort(mKinds, sWeightComparator);
+ return mKinds;
+ }
+
+ /** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */
+ public DataKind getKindForMimetype(String mimeType) {
+ return this.mMimeKinds.get(mimeType);
+ }
+
+ /** Add given {@link DataKind} to list of those provided by this source. */
+ public DataKind addKind(DataKind kind) throws DefinitionException {
+ if (kind.mimeType == null) {
+ throw new DefinitionException("null is not a valid mime type");
+ }
+ if (mMimeKinds.get(kind.mimeType) != null) {
+ throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered");
+ }
+
+ kind.resourcePackageName = this.resourcePackageName;
+ this.mKinds.add(kind);
+ this.mMimeKinds.put(kind.mimeType, kind);
+ return kind;
+ }
+
+ /**
+ * Generic method of inflating a given {@link ContentValues} into a user-readable {@link
+ * CharSequence}. For example, an inflater could combine the multiple columns of {@link
+ * StructuredPostal} together using a string resource before presenting to the user.
+ */
+ public interface StringInflater {
+
+ CharSequence inflateUsing(Context context, ContentValues values);
+ }
+
+ protected static class DefinitionException extends Exception {
+
+ public DefinitionException(String message) {
+ super(message);
+ }
+
+ public DefinitionException(String message, Exception inner) {
+ super(message, inner);
+ }
+ }
+
+ /**
+ * Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link
+ * Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of
+ * this type, and details on how user-defined labels are stored.
+ */
+ public static class EditType {
+
+ public int rawValue;
+ public int labelRes;
+ public boolean secondary;
+ /**
+ * The number of entries allowed for the type. -1 if not specified.
+ *
+ * @see DataKind#typeOverallMax
+ */
+ public int specificMax;
+
+ public String customColumn;
+
+ public EditType(int rawValue, int labelRes) {
+ this.rawValue = rawValue;
+ this.labelRes = labelRes;
+ this.specificMax = -1;
+ }
+
+ public EditType setSecondary(boolean secondary) {
+ this.secondary = secondary;
+ return this;
+ }
+
+ public EditType setSpecificMax(int specificMax) {
+ this.specificMax = specificMax;
+ return this;
+ }
+
+ public EditType setCustomColumn(String customColumn) {
+ this.customColumn = customColumn;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof EditType) {
+ final EditType other = (EditType) object;
+ return other.rawValue == rawValue;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return rawValue;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + " rawValue="
+ + rawValue
+ + " labelRes="
+ + labelRes
+ + " secondary="
+ + secondary
+ + " specificMax="
+ + specificMax
+ + " customColumn="
+ + customColumn;
+ }
+ }
+
+ public static class EventEditType extends EditType {
+
+ private boolean mYearOptional;
+
+ public EventEditType(int rawValue, int labelRes) {
+ super(rawValue, labelRes);
+ }
+
+ public boolean isYearOptional() {
+ return mYearOptional;
+ }
+
+ public EventEditType setYearOptional(boolean yearOptional) {
+ mYearOptional = yearOptional;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " mYearOptional=" + mYearOptional;
+ }
+ }
+
+ /**
+ * Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}.
+ * Includes flags to apply to an {@link EditText}, and the column where this field is stored.
+ */
+ public static final class EditField {
+
+ public String column;
+ public int titleRes;
+ public int inputType;
+ public int minLines;
+ public boolean optional;
+ public boolean shortForm;
+ public boolean longForm;
+
+ public EditField(String column, int titleRes) {
+ this.column = column;
+ this.titleRes = titleRes;
+ }
+
+ public EditField(String column, int titleRes, int inputType) {
+ this(column, titleRes);
+ this.inputType = inputType;
+ }
+
+ public EditField setOptional(boolean optional) {
+ this.optional = optional;
+ return this;
+ }
+
+ public EditField setShortForm(boolean shortForm) {
+ this.shortForm = shortForm;
+ return this;
+ }
+
+ public EditField setLongForm(boolean longForm) {
+ this.longForm = longForm;
+ return this;
+ }
+
+ public EditField setMinLines(int minLines) {
+ this.minLines = minLines;
+ return this;
+ }
+
+ public boolean isMultiLine() {
+ return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + ":"
+ + " column="
+ + column
+ + " titleRes="
+ + titleRes
+ + " inputType="
+ + inputType
+ + " minLines="
+ + minLines
+ + " optional="
+ + optional
+ + " shortForm="
+ + shortForm
+ + " longForm="
+ + longForm;
+ }
+ }
+
+ /**
+ * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current
+ * locale.
+ */
+ public static class DisplayLabelComparator implements Comparator<AccountType> {
+
+ private final Context mContext;
+ /** {@link Comparator} for the current locale. */
+ private final Collator mCollator = Collator.getInstance();
+
+ public DisplayLabelComparator(Context context) {
+ mContext = context;
+ }
+
+ private String getDisplayLabel(AccountType type) {
+ CharSequence label = type.getDisplayLabel(mContext);
+ return (label == null) ? "" : label.toString();
+ }
+
+ @Override
+ public int compare(AccountType lhs, AccountType rhs) {
+ return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
new file mode 100644
index 000000000..a32ebe139
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import java.util.Objects;
+
+/** Encapsulates an "account type" string and a "data set" string. */
+public class AccountTypeWithDataSet {
+
+ private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+ private static final Uri RAW_CONTACTS_URI_LIMIT_1 =
+ RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1")
+ .build();
+
+ /** account type. Can be null for fallback type. */
+ public final String accountType;
+
+ /** dataSet may be null, but never be "". */
+ public final String dataSet;
+
+ private AccountTypeWithDataSet(String accountType, String dataSet) {
+ this.accountType = TextUtils.isEmpty(accountType) ? null : accountType;
+ this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet;
+ }
+
+ public static AccountTypeWithDataSet get(String accountType, String dataSet) {
+ return new AccountTypeWithDataSet(accountType, dataSet);
+ }
+
+ /**
+ * Return true if there are any contacts in the database with this account type and data set.
+ * Touches DB. Don't use in the UI thread.
+ */
+ public boolean hasData(Context context) {
+ final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?";
+ final String selection;
+ final String[] args;
+ if (TextUtils.isEmpty(dataSet)) {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+ args = new String[] {accountType};
+ } else {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+ args = new String[] {accountType, dataSet};
+ }
+
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null);
+ if (c == null) {
+ return false;
+ }
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AccountTypeWithDataSet)) {
+ return false;
+ }
+
+ AccountTypeWithDataSet other = (AccountTypeWithDataSet) o;
+ return Objects.equals(accountType, other.accountType) && Objects.equals(dataSet, other.dataSet);
+ }
+
+ @Override
+ public int hashCode() {
+ return (accountType == null ? 0 : accountType.hashCode())
+ ^ (dataSet == null ? 0 : dataSet.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ return "[" + accountType + "/" + dataSet + "]";
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountWithDataSet.java b/java/com/android/contacts/common/model/account/AccountWithDataSet.java
new file mode 100644
index 000000000..71faf509c
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/** Wrapper for an account that includes a data set (which may be null). */
+public class AccountWithDataSet implements Parcelable {
+
+ // For Parcelable
+ public static final Creator<AccountWithDataSet> CREATOR =
+ new Creator<AccountWithDataSet>() {
+ public AccountWithDataSet createFromParcel(Parcel source) {
+ return new AccountWithDataSet(source);
+ }
+
+ public AccountWithDataSet[] newArray(int size) {
+ return new AccountWithDataSet[size];
+ }
+ };
+ private static final String STRINGIFY_SEPARATOR = "\u0001";
+ private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002";
+ private static final Pattern STRINGIFY_SEPARATOR_PAT =
+ Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR));
+ private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT =
+ Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR));
+ private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+ private static final Uri RAW_CONTACTS_URI_LIMIT_1 =
+ RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1")
+ .build();
+ public final String name;
+ public final String type;
+ public final String dataSet;
+ private final AccountTypeWithDataSet mAccountTypeWithDataSet;
+
+ public AccountWithDataSet(String name, String type, String dataSet) {
+ this.name = emptyToNull(name);
+ this.type = emptyToNull(type);
+ this.dataSet = emptyToNull(dataSet);
+ mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+ }
+
+ public AccountWithDataSet(Parcel in) {
+ this.name = in.readString();
+ this.type = in.readString();
+ this.dataSet = in.readString();
+ mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+ }
+
+ private static String emptyToNull(String text) {
+ return TextUtils.isEmpty(text) ? null : text;
+ }
+
+ private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+ if (!TextUtils.isEmpty(account.name)) {
+ sb.append(account.name);
+ }
+ sb.append(STRINGIFY_SEPARATOR);
+ if (!TextUtils.isEmpty(account.type)) {
+ sb.append(account.type);
+ }
+ sb.append(STRINGIFY_SEPARATOR);
+ if (!TextUtils.isEmpty(account.dataSet)) {
+ sb.append(account.dataSet);
+ }
+
+ return sb;
+ }
+
+ /**
+ * Unpack a string created by {@link #stringify}.
+ *
+ * @throws IllegalArgumentException if it's an invalid string.
+ */
+ public static AccountWithDataSet unstringify(String s) {
+ final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3);
+ if (array.length < 3) {
+ throw new IllegalArgumentException("Invalid string " + s);
+ }
+ return new AccountWithDataSet(
+ array[0], array[1], TextUtils.isEmpty(array[2]) ? null : array[2]);
+ }
+
+ /** Pack a list of {@link AccountWithDataSet} into a string. */
+ public static String stringifyList(List<AccountWithDataSet> accounts) {
+ final StringBuilder sb = new StringBuilder();
+
+ for (AccountWithDataSet account : accounts) {
+ if (sb.length() > 0) {
+ sb.append(ARRAY_STRINGIFY_SEPARATOR);
+ }
+ addStringified(sb, account);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Unpack a list of {@link AccountWithDataSet} into a string.
+ *
+ * @throws IllegalArgumentException if it's an invalid string.
+ */
+ public static List<AccountWithDataSet> unstringifyList(String s) {
+ final ArrayList<AccountWithDataSet> ret = new ArrayList<>();
+ if (TextUtils.isEmpty(s)) {
+ return ret;
+ }
+
+ final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s);
+
+ for (int i = 0; i < array.length; i++) {
+ ret.add(unstringify(array[i]));
+ }
+
+ return ret;
+ }
+
+ public boolean isLocalAccount() {
+ return name == null && type == null;
+ }
+
+ public Account getAccountOrNull() {
+ if (name != null && type != null) {
+ return new Account(name, type);
+ }
+ return null;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ dest.writeString(type);
+ dest.writeString(dataSet);
+ }
+
+ public AccountTypeWithDataSet getAccountTypeWithDataSet() {
+ return mAccountTypeWithDataSet;
+ }
+
+ /**
+ * Return {@code true} if this account has any contacts in the database. Touches DB. Don't use in
+ * the UI thread.
+ */
+ public boolean hasData(Context context) {
+ final String BASE_SELECTION =
+ RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?";
+ final String selection;
+ final String[] args;
+ if (TextUtils.isEmpty(dataSet)) {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+ args = new String[] {type, name};
+ } else {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+ args = new String[] {type, name, dataSet};
+ }
+
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null);
+ if (c == null) {
+ return false;
+ }
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+
+ public boolean equals(Object obj) {
+ if (obj instanceof AccountWithDataSet) {
+ AccountWithDataSet other = (AccountWithDataSet) obj;
+ return Objects.equals(name, other.name)
+ && Objects.equals(type, other.type)
+ && Objects.equals(dataSet, other.dataSet);
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (dataSet != null ? dataSet.hashCode() : 0);
+ return result;
+ }
+
+ public String toString() {
+ return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
+ }
+
+ /** Pack the instance into a string. */
+ public String stringify() {
+ return addStringified(new StringBuilder(), this).toString();
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/BaseAccountType.java b/java/com/android/contacts/common/model/account/BaseAccountType.java
new file mode 100644
index 000000000..21b555917
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/BaseAccountType.java
@@ -0,0 +1,1890 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public abstract class BaseAccountType extends AccountType {
+
+ public static final StringInflater ORGANIZATION_BODY_INFLATER =
+ new StringInflater() {
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final CharSequence companyValue =
+ values.containsKey(Organization.COMPANY)
+ ? values.getAsString(Organization.COMPANY)
+ : null;
+ final CharSequence titleValue =
+ values.containsKey(Organization.TITLE)
+ ? values.getAsString(Organization.TITLE)
+ : null;
+
+ if (companyValue != null && titleValue != null) {
+ return companyValue + ": " + titleValue;
+ } else if (companyValue == null) {
+ return titleValue;
+ } else {
+ return companyValue;
+ }
+ }
+ };
+ protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
+ protected static final int FLAGS_EMAIL =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ protected static final int FLAGS_PERSON_NAME =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+ protected static final int FLAGS_PHONETIC =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
+ protected static final int FLAGS_GENERIC_NAME =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+ protected static final int FLAGS_NOTE =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT;
+ protected static final int FLAGS_WEBSITE =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI;
+ protected static final int FLAGS_POSTAL =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ protected static final int FLAGS_SIP_ADDRESS =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same
+ // basic format as email addresses
+ protected static final int FLAGS_RELATION =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+
+ // Specify the maximum number of lines that can be used to display various field types. If no
+ // value is specified for a particular type, we use the default value from {@link DataKind}.
+ protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10;
+ protected static final int MAX_LINES_FOR_GROUP = 10;
+ protected static final int MAX_LINES_FOR_NOTE = 100;
+ private static final String TAG = "BaseAccountType";
+
+ public BaseAccountType() {
+ this.accountType = null;
+ this.dataSet = null;
+ this.titleRes = R.string.account_phone;
+ this.iconRes = R.mipmap.ic_contacts_launcher;
+ }
+
+ protected static EditType buildPhoneType(int type) {
+ return new EditType(type, Phone.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildEmailType(int type) {
+ return new EditType(type, Email.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildPostalType(int type) {
+ return new EditType(type, StructuredPostal.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildImType(int type) {
+ return new EditType(type, Im.getProtocolLabelResource(type));
+ }
+
+ protected static EditType buildEventType(int type, boolean yearOptional) {
+ return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
+ }
+
+ protected static EditType buildRelationType(int type) {
+ return new EditType(type, Relation.getTypeLabelResource(type));
+ }
+
+ // Utility methods to keep code shorter.
+ private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
+ return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
+ }
+
+ private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
+ return attrs.getAttributeIntValue(null, attribute, defaultValue);
+ }
+
+ private static String getAttr(AttributeSet attrs, String attribute) {
+ return attrs.getAttributeValue(null, attribute);
+ }
+
+ protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
+ .setShortForm(true));
+
+ boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ if (!displayOrderPrimary) {
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ }
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ R.string.name_phonetic,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
+ .setShortForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
+ .setLongForm(true));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Nickname.CONTENT_ITEM_TYPE, R.string.nicknameLabelsGroup, Weight.NICKNAME, true));
+ kind.typeOverallMax = 1;
+ kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, Weight.PHONE, true));
+ kind.iconAltRes = R.drawable.ic_message_24dp;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionHeader = new PhoneActionInflater();
+ kind.actionAltHeader = new PhoneActionAltInflater();
+ kind.actionBody = new SimpleInflater(Phone.NUMBER);
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, Weight.EMAIL, true));
+ kind.actionHeader = new EmailActionInflater();
+ kind.actionBody = new SimpleInflater(Email.DATA);
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ R.string.postalLabelsGroup,
+ Weight.STRUCTURED_POSTAL,
+ true));
+ kind.actionHeader = new PostalActionInflater();
+ kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
+ kind.typeList.add(
+ buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(StructuredPostal.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindIm(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, Weight.IM, true));
+ kind.actionHeader = new ImActionInflater();
+ kind.actionBody = new SimpleInflater(Im.DATA);
+
+ // NOTE: even though a traditional "type" exists, for editing
+ // purposes we're using the protocol to pick labels
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.typeColumn = Im.PROTOCOL;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
+ kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
+ kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
+ kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
+ kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
+ kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
+ kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
+ kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
+ kind.typeList.add(
+ buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Organization.CONTENT_ITEM_TYPE,
+ R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
+ kind.actionBody = ORGANIZATION_BODY_INFLATER;
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+ DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true));
+ kind.typeOverallMax = 1;
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+ return kind;
+ }
+
+ protected DataKind addDataKindNote(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, Weight.NOTE, true));
+ kind.typeOverallMax = 1;
+ kind.actionHeader = new SimpleInflater(R.string.label_notes);
+ kind.actionBody = new SimpleInflater(Note.NOTE);
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Website.CONTENT_ITEM_TYPE, R.string.websiteLabelsGroup, Weight.WEBSITE, true));
+ kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
+ kind.actionBody = new SimpleInflater(Website.URL);
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindSipAddress(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ SipAddress.CONTENT_ITEM_TYPE,
+ R.string.label_sip_address,
+ Weight.SIP_ADDRESS,
+ true));
+
+ kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
+ kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+ kind.typeOverallMax = 1;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ GroupMembership.CONTENT_ITEM_TYPE,
+ R.string.groupsLabel,
+ Weight.GROUP_MEMBERSHIP,
+ true));
+
+ kind.typeOverallMax = 1;
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return false;
+ }
+
+ /** Parses the content of the EditSchema tag in contacts.xml. */
+ protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException, DefinitionException {
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+
+ if (Tag.DATA_KIND.equals(tag)) {
+ for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
+ addKind(kind);
+ }
+ } else {
+ Log.w(TAG, "Skipping unknown tag " + tag);
+ }
+ }
+ }
+
+ private interface Tag {
+
+ String DATA_KIND = "DataKind";
+ String TYPE = "Type";
+ }
+
+ private interface Attr {
+
+ String MAX_OCCURRENCE = "maxOccurs";
+ String DATE_WITH_TIME = "dateWithTime";
+ String YEAR_OPTIONAL = "yearOptional";
+ String KIND = "kind";
+ String TYPE = "type";
+ }
+
+ protected interface Weight {
+
+ int NONE = -1;
+ int PHONE = 10;
+ int EMAIL = 15;
+ int STRUCTURED_POSTAL = 25;
+ int NICKNAME = 111;
+ int EVENT = 120;
+ int ORGANIZATION = 125;
+ int NOTE = 130;
+ int IM = 140;
+ int SIP_ADDRESS = 145;
+ int GROUP_MEMBERSHIP = 150;
+ int WEBSITE = 160;
+ int RELATIONSHIP = 999;
+ }
+
+ /**
+ * Simple inflater that assumes a string resource has a "%s" that will be filled from the given
+ * column.
+ */
+ public static class SimpleInflater implements StringInflater {
+
+ private final int mStringRes;
+ private final String mColumnName;
+
+ public SimpleInflater(int stringRes) {
+ this(stringRes, null);
+ }
+
+ public SimpleInflater(String columnName) {
+ this(-1, columnName);
+ }
+
+ public SimpleInflater(int stringRes, String columnName) {
+ mStringRes = stringRes;
+ mColumnName = columnName;
+ }
+
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final boolean validColumn = values.containsKey(mColumnName);
+ final boolean validString = mStringRes > 0;
+
+ final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
+ final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;
+
+ if (validString && validColumn) {
+ return String.format(stringValue.toString(), columnValue);
+ } else if (validString) {
+ return stringValue;
+ } else if (validColumn) {
+ return columnValue;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + " mStringRes="
+ + mStringRes
+ + " mColumnName"
+ + mColumnName;
+ }
+
+ public String getColumnNameForTest() {
+ return mColumnName;
+ }
+ }
+
+ public abstract static class CommonInflater implements StringInflater {
+
+ protected abstract int getTypeLabelResource(Integer type);
+
+ protected boolean isCustom(Integer type) {
+ return type == BaseTypes.TYPE_CUSTOM;
+ }
+
+ protected String getTypeColumn() {
+ return Phone.TYPE;
+ }
+
+ protected String getLabelColumn() {
+ return Phone.LABEL;
+ }
+
+ protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
+ final int labelRes = getTypeLabelResource(type);
+ if (type == null) {
+ return res.getText(labelRes);
+ } else if (isCustom(type)) {
+ return res.getString(labelRes, label == null ? "" : label);
+ } else {
+ return res.getText(labelRes);
+ }
+ }
+
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final Integer type = values.getAsInteger(getTypeColumn());
+ final String label = values.getAsString(getLabelColumn());
+ return getTypeLabel(context.getResources(), type, label);
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName();
+ }
+ }
+
+ public static class PhoneActionInflater extends CommonInflater {
+
+ @Override
+ protected boolean isCustom(Integer type) {
+ return ContactDisplayUtils.isCustomPhoneType(type);
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return ContactDisplayUtils.getPhoneLabelResourceId(type);
+ }
+ }
+
+ public static class PhoneActionAltInflater extends CommonInflater {
+
+ @Override
+ protected boolean isCustom(Integer type) {
+ return ContactDisplayUtils.isCustomPhoneType(type);
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return ContactDisplayUtils.getSmsLabelResourceId(type);
+ }
+ }
+
+ public static class EmailActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.email;
+ }
+ switch (type) {
+ case Email.TYPE_HOME:
+ return R.string.email_home;
+ case Email.TYPE_WORK:
+ return R.string.email_work;
+ case Email.TYPE_OTHER:
+ return R.string.email_other;
+ case Email.TYPE_MOBILE:
+ return R.string.email_mobile;
+ default:
+ return R.string.email_custom;
+ }
+ }
+ }
+
+ public static class EventActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return Event.getTypeResource(type);
+ }
+ }
+
+ public static class RelationActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type);
+ }
+ }
+
+ public static class PostalActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.map_other;
+ }
+ switch (type) {
+ case StructuredPostal.TYPE_HOME:
+ return R.string.map_home;
+ case StructuredPostal.TYPE_WORK:
+ return R.string.map_work;
+ case StructuredPostal.TYPE_OTHER:
+ return R.string.map_other;
+ default:
+ return R.string.map_custom;
+ }
+ }
+ }
+
+ public static class ImActionInflater extends CommonInflater {
+
+ @Override
+ protected String getTypeColumn() {
+ return Im.PROTOCOL;
+ }
+
+ @Override
+ protected String getLabelColumn() {
+ return Im.CUSTOM_PROTOCOL;
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.chat;
+ }
+ switch (type) {
+ case Im.PROTOCOL_AIM:
+ return R.string.chat_aim;
+ case Im.PROTOCOL_MSN:
+ return R.string.chat_msn;
+ case Im.PROTOCOL_YAHOO:
+ return R.string.chat_yahoo;
+ case Im.PROTOCOL_SKYPE:
+ return R.string.chat_skype;
+ case Im.PROTOCOL_QQ:
+ return R.string.chat_qq;
+ case Im.PROTOCOL_GOOGLE_TALK:
+ return R.string.chat_gtalk;
+ case Im.PROTOCOL_ICQ:
+ return R.string.chat_icq;
+ case Im.PROTOCOL_JABBER:
+ return R.string.chat_jabber;
+ case Im.PROTOCOL_NETMEETING:
+ return R.string.chat;
+ default:
+ return R.string.chat;
+ }
+ }
+ }
+
+ // TODO Extract it to its own class, and move all KindBuilders to it as well.
+ private static class KindParser {
+
+ public static final KindParser INSTANCE = new KindParser();
+
+ private final Map<String, KindBuilder> mBuilders = new ArrayMap<>();
+
+ private KindParser() {
+ addBuilder(new NameKindBuilder());
+ addBuilder(new NicknameKindBuilder());
+ addBuilder(new PhoneKindBuilder());
+ addBuilder(new EmailKindBuilder());
+ addBuilder(new StructuredPostalKindBuilder());
+ addBuilder(new ImKindBuilder());
+ addBuilder(new OrganizationKindBuilder());
+ addBuilder(new PhotoKindBuilder());
+ addBuilder(new NoteKindBuilder());
+ addBuilder(new WebsiteKindBuilder());
+ addBuilder(new SipAddressKindBuilder());
+ addBuilder(new GroupMembershipKindBuilder());
+ addBuilder(new EventKindBuilder());
+ addBuilder(new RelationshipKindBuilder());
+ }
+
+ private void addBuilder(KindBuilder builder) {
+ mBuilders.put(builder.getTagName(), builder);
+ }
+
+ /**
+ * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns {@link
+ * DataKind}s. (Usually just one, but there are three for the "name" kind.)
+ *
+ * <p>This method returns a list, because we need to add 3 kinds for the name data kind.
+ * (structured, display and phonetic)
+ */
+ public List<DataKind> parseDataKindTag(
+ Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final String kind = getAttr(attrs, Attr.KIND);
+ final KindBuilder builder = mBuilders.get(kind);
+ if (builder != null) {
+ return builder.parseDataKind(context, parser, attrs);
+ } else {
+ throw new DefinitionException("Undefined data kind '" + kind + "'");
+ }
+ }
+ }
+
+ private abstract static class KindBuilder {
+
+ public abstract String getTagName();
+
+ /** DataKind tag parser specific to each kind. Subclasses must implement it. */
+ public abstract List<DataKind> parseDataKind(
+ Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException;
+
+ /** Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind tag. */
+ protected final DataKind newDataKind(
+ Context context,
+ XmlPullParser parser,
+ AttributeSet attrs,
+ boolean isPseudo,
+ String mimeType,
+ String typeColumn,
+ int titleRes,
+ int weight,
+ StringInflater actionHeader,
+ StringInflater actionBody)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Adding DataKind: " + mimeType);
+ }
+
+ final DataKind kind = new DataKind(mimeType, titleRes, weight, true);
+ kind.typeColumn = typeColumn;
+ kind.actionHeader = actionHeader;
+ kind.actionBody = actionBody;
+ kind.fieldList = new ArrayList<>();
+
+ // Get more information from the tag...
+ // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
+ if (!isPseudo) {
+ kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ // Process "Type" tags.
+ // If a kind has the type column, contacts.xml must have at least one type
+ // definition. Otherwise, it mustn't have a type definition.
+ if (kind.typeColumn != null) {
+ // Parse and add types.
+ kind.typeList = new ArrayList<>();
+ parseTypes(context, parser, attrs, kind, true);
+ if (kind.typeList.size() == 0) {
+ throw new DefinitionException("Kind " + kind.mimeType + " must have at least one type");
+ }
+ } else {
+ // Make sure it has no types.
+ parseTypes(context, parser, attrs, kind, false /* can't have types */);
+ }
+ }
+
+ return kind;
+ }
+
+ /**
+ * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds them to
+ * the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, so throws
+ * {@link DefinitionException}.
+ */
+ private void parseTypes(
+ Context context,
+ XmlPullParser parser,
+ AttributeSet attrs,
+ DataKind kind,
+ boolean canHaveTypes)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+ if (Tag.TYPE.equals(tag)) {
+ if (canHaveTypes) {
+ kind.typeList.add(parseTypeTag(parser, attrs, kind));
+ } else {
+ throw new DefinitionException("Kind " + kind.mimeType + " can't have types");
+ }
+ } else {
+ throw new DefinitionException("Unknown tag: " + tag);
+ }
+ }
+ }
+
+ /**
+ * Parses a single Type element and returns an {@link EditType} built from it. Uses {@link
+ * #buildEditTypeForTypeTag} defined in subclasses to actually build an {@link EditType}.
+ */
+ private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
+ throws DefinitionException {
+
+ final String typeName = getAttr(attrs, Attr.TYPE);
+
+ final EditType et = buildEditTypeForTypeTag(attrs, typeName);
+ if (et == null) {
+ throw new DefinitionException(
+ "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
+ }
+ et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ return et;
+ }
+
+ /**
+ * Returns an {@link EditType} for the given "type". Subclasses may optionally use the
+ * attributes in the tag to set optional values. (e.g. "yearOptional" for the event kind)
+ */
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ return null;
+ }
+
+ protected final void throwIfList(DataKind kind) throws DefinitionException {
+ if (kind.typeOverallMax != 1) {
+ throw new DefinitionException("Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
+ }
+ }
+ }
+
+ /** DataKind parser for Name. (structured, display, phonetic) */
+ private static class NameKindBuilder extends KindBuilder {
+
+ private static void checkAttributeTrue(boolean value, String attrName)
+ throws DefinitionException {
+ if (!value) {
+ throw new DefinitionException(attrName + " must be true");
+ }
+ }
+
+ @Override
+ public String getTagName() {
+ return "name";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ // Build 3 data kinds:
+ // - StructuredName.CONTENT_ITEM_TYPE
+ // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
+ // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
+
+ final boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
+ final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
+ final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
+ final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
+ final boolean supportsPhoneticFamilyName =
+ getAttr(attrs, "supportsPhoneticFamilyName", false);
+ final boolean supportsPhoneticMiddleName =
+ getAttr(attrs, "supportsPhoneticMiddleName", false);
+ final boolean supportsPhoneticGivenName = getAttr(attrs, "supportsPhoneticGivenName", false);
+
+ // For now, every things must be supported.
+ checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
+ checkAttributeTrue(supportsPrefix, "supportsPrefix");
+ checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
+ checkAttributeTrue(supportsSuffix, "supportsSuffix");
+ checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
+ checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
+ checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
+
+ final List<DataKind> kinds = new ArrayList<>();
+
+ // Structured name
+ final DataKind ks =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ StructuredName.CONTENT_ITEM_TYPE,
+ null,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ throwIfList(ks);
+ kinds.add(ks);
+
+ // Note about setLongForm/setShortForm below.
+ // We need to set this only when the type supports display name. (=supportsDisplayName)
+ // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
+ // "optional".
+
+ ks.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
+ ks.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ // Display name
+ final DataKind kd =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ true,
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ null,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kd.typeOverallMax = 1;
+ kinds.add(kd);
+
+ kd.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
+ .setShortForm(true));
+
+ if (!displayOrderPrimary) {
+ kd.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ } else {
+ kd.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ }
+
+ // Phonetic name
+ final DataKind kp =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ true,
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ null,
+ R.string.name_phonetic,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kp.typeOverallMax = 1;
+ kinds.add(kp);
+
+ // We may want to change the order depending on displayOrderPrimary too.
+ kp.fieldList.add(
+ new EditField(
+ DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
+ .setShortForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME,
+ R.string.name_phonetic_family,
+ FLAGS_PHONETIC)
+ .setLongForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME,
+ R.string.name_phonetic_middle,
+ FLAGS_PHONETIC)
+ .setLongForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
+ .setLongForm(true));
+ return kinds;
+ }
+ }
+
+ private static class NicknameKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "nickname";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Nickname.CONTENT_ITEM_TYPE,
+ null,
+ R.string.nicknameLabelsGroup,
+ Weight.NICKNAME,
+ new SimpleInflater(R.string.nicknameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ throwIfList(kind);
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class PhoneKindBuilder extends KindBuilder {
+
+ /** Just to avoid line-wrapping... */
+ protected static EditType build(int type, boolean secondary) {
+ return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
+ }
+
+ @Override
+ public String getTagName() {
+ return "phone";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Phone.CONTENT_ITEM_TYPE,
+ Phone.TYPE,
+ R.string.phoneLabelsGroup,
+ Weight.PHONE,
+ new PhoneActionInflater(),
+ new SimpleInflater(Phone.NUMBER));
+
+ kind.iconAltRes = R.drawable.ic_message_24dp;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionAltHeader = new PhoneActionAltInflater();
+
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("home".equals(type)) {
+ return build(Phone.TYPE_HOME, false);
+ }
+ if ("mobile".equals(type)) {
+ return build(Phone.TYPE_MOBILE, false);
+ }
+ if ("work".equals(type)) {
+ return build(Phone.TYPE_WORK, false);
+ }
+ if ("fax_work".equals(type)) {
+ return build(Phone.TYPE_FAX_WORK, true);
+ }
+ if ("fax_home".equals(type)) {
+ return build(Phone.TYPE_FAX_HOME, true);
+ }
+ if ("pager".equals(type)) {
+ return build(Phone.TYPE_PAGER, true);
+ }
+ if ("other".equals(type)) {
+ return build(Phone.TYPE_OTHER, false);
+ }
+ if ("callback".equals(type)) {
+ return build(Phone.TYPE_CALLBACK, true);
+ }
+ if ("car".equals(type)) {
+ return build(Phone.TYPE_CAR, true);
+ }
+ if ("company_main".equals(type)) {
+ return build(Phone.TYPE_COMPANY_MAIN, true);
+ }
+ if ("isdn".equals(type)) {
+ return build(Phone.TYPE_ISDN, true);
+ }
+ if ("main".equals(type)) {
+ return build(Phone.TYPE_MAIN, true);
+ }
+ if ("other_fax".equals(type)) {
+ return build(Phone.TYPE_OTHER_FAX, true);
+ }
+ if ("radio".equals(type)) {
+ return build(Phone.TYPE_RADIO, true);
+ }
+ if ("telex".equals(type)) {
+ return build(Phone.TYPE_TELEX, true);
+ }
+ if ("tty_tdd".equals(type)) {
+ return build(Phone.TYPE_TTY_TDD, true);
+ }
+ if ("work_mobile".equals(type)) {
+ return build(Phone.TYPE_WORK_MOBILE, true);
+ }
+ if ("work_pager".equals(type)) {
+ return build(Phone.TYPE_WORK_PAGER, true);
+ }
+
+ // Note "assistant" used to be a custom column for the fallback type, but not anymore.
+ if ("assistant".equals(type)) {
+ return build(Phone.TYPE_ASSISTANT, true);
+ }
+ if ("mms".equals(type)) {
+ return build(Phone.TYPE_MMS, true);
+ }
+ if ("custom".equals(type)) {
+ return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class EmailKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "email";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Email.CONTENT_ITEM_TYPE,
+ Email.TYPE,
+ R.string.emailLabelsGroup,
+ Weight.EMAIL,
+ new EmailActionInflater(),
+ new SimpleInflater(Email.DATA));
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) {
+ return buildEmailType(Email.TYPE_HOME);
+ }
+ if ("work".equals(type)) {
+ return buildEmailType(Email.TYPE_WORK);
+ }
+ if ("other".equals(type)) {
+ return buildEmailType(Email.TYPE_OTHER);
+ }
+ if ("mobile".equals(type)) {
+ return buildEmailType(Email.TYPE_MOBILE);
+ }
+ if ("custom".equals(type)) {
+ return buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class StructuredPostalKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "postal";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ StructuredPostal.TYPE,
+ R.string.postalLabelsGroup,
+ Weight.STRUCTURED_POSTAL,
+ new PostalActionInflater(),
+ new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
+
+ if (getAttr(attrs, "needsStructured", false)) {
+ if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
+ // Japanese order
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ // Generic order
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+ } else {
+ kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+ kind.fieldList.add(
+ new EditField(
+ StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
+ }
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_HOME);
+ }
+ if ("work".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_WORK);
+ }
+ if ("other".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_OTHER);
+ }
+ if ("custom".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class ImKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "im";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ // IM is special:
+ // - It uses "protocol" as the custom label field
+ // - Its TYPE is fixed to TYPE_OTHER
+
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Im.CONTENT_ITEM_TYPE,
+ Im.PROTOCOL,
+ R.string.imLabelsGroup,
+ Weight.IM,
+ new ImActionInflater(),
+ new SimpleInflater(Im.DATA) // header / action
+ );
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("aim".equals(type)) {
+ return buildImType(Im.PROTOCOL_AIM);
+ }
+ if ("msn".equals(type)) {
+ return buildImType(Im.PROTOCOL_MSN);
+ }
+ if ("yahoo".equals(type)) {
+ return buildImType(Im.PROTOCOL_YAHOO);
+ }
+ if ("skype".equals(type)) {
+ return buildImType(Im.PROTOCOL_SKYPE);
+ }
+ if ("qq".equals(type)) {
+ return buildImType(Im.PROTOCOL_QQ);
+ }
+ if ("google_talk".equals(type)) {
+ return buildImType(Im.PROTOCOL_GOOGLE_TALK);
+ }
+ if ("icq".equals(type)) {
+ return buildImType(Im.PROTOCOL_ICQ);
+ }
+ if ("jabber".equals(type)) {
+ return buildImType(Im.PROTOCOL_JABBER);
+ }
+ if ("custom".equals(type)) {
+ return buildImType(Im.PROTOCOL_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Im.CUSTOM_PROTOCOL);
+ }
+ return null;
+ }
+ }
+
+ private static class OrganizationKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "organization";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Organization.CONTENT_ITEM_TYPE,
+ null,
+ R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION,
+ new SimpleInflater(R.string.organizationLabelsGroup),
+ ORGANIZATION_BODY_INFLATER);
+
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class PhotoKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "photo";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Photo.CONTENT_ITEM_TYPE,
+ null /* no type */,
+ Weight.NONE,
+ -1,
+ null,
+ null // no header, no body
+ );
+
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class NoteKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "note";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Note.CONTENT_ITEM_TYPE,
+ null,
+ R.string.label_notes,
+ Weight.NOTE,
+ new SimpleInflater(R.string.label_notes),
+ new SimpleInflater(Note.NOTE));
+
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+ kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class WebsiteKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "website";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Website.CONTENT_ITEM_TYPE,
+ null,
+ R.string.websiteLabelsGroup,
+ Weight.WEBSITE,
+ new SimpleInflater(R.string.websiteLabelsGroup),
+ new SimpleInflater(Website.URL));
+
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class SipAddressKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "sip_address";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ SipAddress.CONTENT_ITEM_TYPE,
+ null,
+ R.string.label_sip_address,
+ Weight.SIP_ADDRESS,
+ new SimpleInflater(R.string.label_sip_address),
+ new SimpleInflater(SipAddress.SIP_ADDRESS));
+
+ kind.fieldList.add(
+ new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class GroupMembershipKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "group_membership";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ GroupMembership.CONTENT_ITEM_TYPE,
+ null,
+ R.string.groupsLabel,
+ Weight.GROUP_MEMBERSHIP,
+ null,
+ null);
+
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+ kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ /**
+ * Event DataKind parser.
+ *
+ * <p>Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
+ */
+ private static class EventKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "event";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Event.CONTENT_ITEM_TYPE,
+ Event.TYPE,
+ R.string.eventLabelsGroup,
+ Weight.EVENT,
+ new EventActionInflater(),
+ new SimpleInflater(Event.START_DATE));
+
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+ } else {
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ }
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
+
+ if ("birthday".equals(type)) {
+ return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
+ }
+ if ("anniversary".equals(type)) {
+ return buildEventType(Event.TYPE_ANNIVERSARY, yo);
+ }
+ if ("other".equals(type)) {
+ return buildEventType(Event.TYPE_OTHER, yo);
+ }
+ if ("custom".equals(type)) {
+ return buildEventType(Event.TYPE_CUSTOM, yo)
+ .setSecondary(true)
+ .setCustomColumn(Event.LABEL);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Relationship DataKind parser.
+ *
+ * <p>Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
+ * now.
+ */
+ private static class RelationshipKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "relationship";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Relation.CONTENT_ITEM_TYPE,
+ Relation.TYPE,
+ R.string.relationLabelsGroup,
+ Weight.RELATIONSHIP,
+ new RelationActionInflater(),
+ new SimpleInflater(Relation.NAME));
+
+ kind.fieldList.add(
+ new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("assistant".equals(type)) {
+ return buildRelationType(Relation.TYPE_ASSISTANT);
+ }
+ if ("brother".equals(type)) {
+ return buildRelationType(Relation.TYPE_BROTHER);
+ }
+ if ("child".equals(type)) {
+ return buildRelationType(Relation.TYPE_CHILD);
+ }
+ if ("domestic_partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
+ }
+ if ("father".equals(type)) {
+ return buildRelationType(Relation.TYPE_FATHER);
+ }
+ if ("friend".equals(type)) {
+ return buildRelationType(Relation.TYPE_FRIEND);
+ }
+ if ("manager".equals(type)) {
+ return buildRelationType(Relation.TYPE_MANAGER);
+ }
+ if ("mother".equals(type)) {
+ return buildRelationType(Relation.TYPE_MOTHER);
+ }
+ if ("parent".equals(type)) {
+ return buildRelationType(Relation.TYPE_PARENT);
+ }
+ if ("partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_PARTNER);
+ }
+ if ("referred_by".equals(type)) {
+ return buildRelationType(Relation.TYPE_REFERRED_BY);
+ }
+ if ("relative".equals(type)) {
+ return buildRelationType(Relation.TYPE_RELATIVE);
+ }
+ if ("sister".equals(type)) {
+ return buildRelationType(Relation.TYPE_SISTER);
+ }
+ if ("spouse".equals(type)) {
+ return buildRelationType(Relation.TYPE_SPOUSE);
+ }
+ if ("custom".equals(type)) {
+ return buildRelationType(Relation.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Relation.LABEL);
+ }
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/ExchangeAccountType.java b/java/com/android/contacts/common/model/account/ExchangeAccountType.java
new file mode 100644
index 000000000..a27028e80
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/ExchangeAccountType.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class ExchangeAccountType extends BaseAccountType {
+
+ private static final String TAG = "ExchangeAccountType";
+
+ private static final String ACCOUNT_TYPE_AOSP = "com.android.exchange";
+ private static final String ACCOUNT_TYPE_GOOGLE_1 = "com.google.android.exchange";
+ private static final String ACCOUNT_TYPE_GOOGLE_2 = "com.google.android.gm.exchange";
+
+ public ExchangeAccountType(Context context, String authenticatorPackageName, String type) {
+ this.accountType = type;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindEvent(context);
+ addDataKindWebsite(context);
+ addDataKindGroupMembership(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ public static boolean isExchangeType(String type) {
+ return ACCOUNT_TYPE_AOSP.equals(type)
+ || ACCOUNT_TYPE_GOOGLE_1.equals(type)
+ || ACCOUNT_TYPE_GOOGLE_2.equals(type);
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME));
+
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ true));
+
+ boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ if (!displayOrderPrimary) {
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ }
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ R.string.name_phonetic,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindNickname(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeOverallMax = 3;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindStructuredPostal(context);
+
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ if (useJapaneseOrder) {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindIm(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindIm(context);
+
+ // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first
+ kind.typeOverallMax = 3;
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindOrganization(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhoto(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindNote(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindNote(context);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeOverallMax = 1;
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1));
+
+ kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindWebsite(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/ExternalAccountType.java b/java/com/android/contacts/common/model/account/ExternalAccountType.java
new file mode 100644
index 000000000..aca1f70d2
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** A general contacts account type descriptor. */
+public class ExternalAccountType extends BaseAccountType {
+
+ private static final String TAG = "ExternalAccountType";
+
+ private static final String SYNC_META_DATA = "android.content.SyncAdapter";
+
+ /**
+ * The metadata name for so-called "contacts.xml".
+ *
+ * <p>On LMP and later, we also accept the "alternate" name. This is to allow sync adapters to
+ * have a contacts.xml without making it visible on older platforms. If you modify this also
+ * update the corresponding list in ContactsProvider/PhotoPriorityResolver
+ */
+ private static final String[] METADATA_CONTACTS_NAMES =
+ new String[] {
+ "android.provider.ALTERNATE_CONTACTS_STRUCTURE", "android.provider.CONTACTS_STRUCTURE"
+ };
+
+ private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
+ private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
+ private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
+ private static final String TAG_EDIT_SCHEMA = "EditSchema";
+
+ private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
+ private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
+ private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
+ private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
+ private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
+ private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
+ private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
+ private static final String ATTR_DATA_SET = "dataSet";
+ private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
+
+ // The following attributes should only be set in non-sync-adapter account types. They allow
+ // for the account type and resource IDs to be specified without an associated authenticator.
+ private static final String ATTR_ACCOUNT_TYPE = "accountType";
+ private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
+ private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
+
+ private final boolean mIsExtension;
+
+ private String mEditContactActivityClassName;
+ private String mCreateContactActivityClassName;
+ private String mInviteContactActivity;
+ private String mInviteActionLabelAttribute;
+ private int mInviteActionLabelResId;
+ private String mViewContactNotifyService;
+ private String mViewGroupActivity;
+ private String mViewGroupLabelAttribute;
+ private int mViewGroupLabelResId;
+ private List<String> mExtensionPackageNames;
+ private String mAccountTypeLabelAttribute;
+ private String mAccountTypeIconAttribute;
+ private boolean mHasContactsMetadata;
+ private boolean mHasEditSchema;
+
+ public ExternalAccountType(Context context, String resPackageName, boolean isExtension) {
+ this(context, resPackageName, isExtension, null);
+ }
+
+ /**
+ * Constructor used for testing to initialize with any arbitrary XML.
+ *
+ * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by tests.
+ * If null, the metadata is loaded from the specified package.
+ */
+ ExternalAccountType(
+ Context context,
+ String packageName,
+ boolean isExtension,
+ XmlResourceParser injectedMetadata) {
+ this.mIsExtension = isExtension;
+ this.resourcePackageName = packageName;
+ this.syncAdapterPackageName = packageName;
+
+ final XmlResourceParser parser;
+ if (injectedMetadata == null) {
+ parser = loadContactsXml(context, packageName);
+ } else {
+ parser = injectedMetadata;
+ }
+ boolean needLineNumberInErrorLog = true;
+ try {
+ if (parser != null) {
+ inflate(context, parser);
+ }
+
+ // Done parsing; line number no longer needed in error log.
+ needLineNumberInErrorLog = false;
+ if (mHasEditSchema) {
+ checkKindExists(StructuredName.CONTENT_ITEM_TYPE);
+ checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME);
+ checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
+ checkKindExists(Photo.CONTENT_ITEM_TYPE);
+ } else {
+ // Bring in name and photo from fallback source, which are non-optional
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindPhoto(context);
+ }
+ } catch (DefinitionException e) {
+ final StringBuilder error = new StringBuilder();
+ error.append("Problem reading XML");
+ if (needLineNumberInErrorLog && (parser != null)) {
+ error.append(" in line ");
+ error.append(parser.getLineNumber());
+ }
+ error.append(" for external package ");
+ error.append(packageName);
+
+ Log.e(TAG, error.toString(), e);
+ return;
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+
+ mExtensionPackageNames = new ArrayList<String>();
+ mInviteActionLabelResId =
+ resolveExternalResId(
+ context,
+ mInviteActionLabelAttribute,
+ syncAdapterPackageName,
+ ATTR_INVITE_CONTACT_ACTION_LABEL);
+ mViewGroupLabelResId =
+ resolveExternalResId(
+ context,
+ mViewGroupLabelAttribute,
+ syncAdapterPackageName,
+ ATTR_VIEW_GROUP_ACTION_LABEL);
+ titleRes =
+ resolveExternalResId(
+ context, mAccountTypeLabelAttribute, syncAdapterPackageName, ATTR_ACCOUNT_LABEL);
+ iconRes =
+ resolveExternalResId(
+ context, mAccountTypeIconAttribute, syncAdapterPackageName, ATTR_ACCOUNT_ICON);
+
+ // If we reach this point, the account type has been successfully initialized.
+ mIsInitialized = true;
+ }
+
+ /**
+ * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
+ *
+ * <p>This method looks through all services in the package that handle sync adapter intents for
+ * the first one that contains CONTACTS_STRUCTURE metadata. We have to look through all sync
+ * adapters in the package in case there are contacts and other sync adapters (eg, calendar) in
+ * the same package.
+ *
+ * <p>Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case the
+ * account type *will* be initialized with minimal configuration.
+ */
+ public static XmlResourceParser loadContactsXml(Context context, String resPackageName) {
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName);
+ final List<ResolveInfo> intentServices =
+ pm.queryIntentServices(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+ if (intentServices != null) {
+ for (final ResolveInfo resolveInfo : intentServices) {
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo == null) {
+ continue;
+ }
+ for (String metadataName : METADATA_CONTACTS_NAMES) {
+ final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, metadataName);
+ if (parser != null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(
+ TAG,
+ String.format(
+ "Metadata loaded from: %s, %s, %s",
+ serviceInfo.packageName, serviceInfo.name, metadataName));
+ }
+ return parser;
+ }
+ }
+ }
+ }
+
+ // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
+ return null;
+ }
+
+ /** Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata. */
+ public static boolean hasContactsXml(Context context, String resPackageName) {
+ return loadContactsXml(context, resPackageName) != null;
+ }
+
+ /**
+ * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in the
+ * resource package.
+ *
+ * <p>If the argument is in the invalid format or isn't a resource name, it returns -1.
+ *
+ * @param context context
+ * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
+ * @param packageName name of the package containing the resource.
+ * @param xmlAttributeName attribute name which the resource came from. Used for logging.
+ */
+ @VisibleForTesting
+ static int resolveExternalResId(
+ Context context, String resourceName, String packageName, String xmlAttributeName) {
+ if (TextUtils.isEmpty(resourceName)) {
+ return -1; // Empty text is okay.
+ }
+ if (resourceName.charAt(0) != '@') {
+ Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
+ return -1;
+ }
+ final String name = resourceName.substring(1);
+ final Resources res;
+ try {
+ res = context.getPackageManager().getResourcesForApplication(packageName);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Unable to load package " + packageName);
+ return -1;
+ }
+ final int resId = res.getIdentifier(name, null, packageName);
+ if (resId == 0) {
+ Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
+ return -1;
+ }
+ return resId;
+ }
+
+ private void checkKindExists(String mimeType) throws DefinitionException {
+ if (getKindForMimetype(mimeType) == null) {
+ throw new DefinitionException(mimeType + " must be supported");
+ }
+ }
+
+ @Override
+ public boolean isEmbedded() {
+ return false;
+ }
+
+ @Override
+ public boolean isExtension() {
+ return mIsExtension;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return mHasEditSchema;
+ }
+
+ /** Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. */
+ public boolean hasContactsMetadata() {
+ return mHasContactsMetadata;
+ }
+
+ @Override
+ public String getEditContactActivityClassName() {
+ return mEditContactActivityClassName;
+ }
+
+ @Override
+ public String getCreateContactActivityClassName() {
+ return mCreateContactActivityClassName;
+ }
+
+ @Override
+ public String getInviteContactActivityClassName() {
+ return mInviteContactActivity;
+ }
+
+ @Override
+ protected int getInviteContactActionResId() {
+ return mInviteActionLabelResId;
+ }
+
+ @Override
+ public String getViewContactNotifyServiceClassName() {
+ return mViewContactNotifyService;
+ }
+
+ @Override
+ public String getViewGroupActivity() {
+ return mViewGroupActivity;
+ }
+
+ @Override
+ protected int getViewGroupLabelResId() {
+ return mViewGroupLabelResId;
+ }
+
+ @Override
+ public List<String> getExtensionPackageNames() {
+ return mExtensionPackageNames;
+ }
+
+ /**
+ * Inflate this {@link AccountType} from the given parser. This may only load details matching the
+ * publicly-defined schema.
+ */
+ protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ try {
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Drain comments and whitespace
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("No start tag found");
+ }
+
+ String rootTag = parser.getName();
+ if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag)
+ && !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
+ throw new IllegalStateException(
+ "Top level element must be " + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
+ }
+
+ mHasContactsMetadata = true;
+
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attr = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, attr + "=" + value);
+ }
+ if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
+ mEditContactActivityClassName = value;
+ } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
+ mCreateContactActivityClassName = value;
+ } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
+ mInviteContactActivity = value;
+ } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
+ mInviteActionLabelAttribute = value;
+ } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
+ mViewContactNotifyService = value;
+ } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
+ mViewGroupActivity = value;
+ } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
+ mViewGroupLabelAttribute = value;
+ } else if (ATTR_DATA_SET.equals(attr)) {
+ dataSet = value;
+ } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
+ mExtensionPackageNames.add(value);
+ } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
+ accountType = value;
+ } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
+ mAccountTypeLabelAttribute = value;
+ } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
+ mAccountTypeIconAttribute = value;
+ } else {
+ Log.e(TAG, "Unsupported attribute " + attr);
+ }
+ }
+
+ // Parse all children kinds
+ final int startDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > startDepth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) {
+ continue; // Not a direct child tag
+ }
+
+ String tag = parser.getName();
+ if (TAG_EDIT_SCHEMA.equals(tag)) {
+ mHasEditSchema = true;
+ parseEditSchema(context, parser, attrs);
+ } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactsDataKind);
+ final DataKind kind = new DataKind();
+
+ kind.mimeType = a.getString(R.styleable.ContactsDataKind_android_mimeType);
+ final String summaryColumn =
+ a.getString(R.styleable.ContactsDataKind_android_summaryColumn);
+ if (summaryColumn != null) {
+ // Inflate a specific column as summary when requested
+ kind.actionHeader = new SimpleInflater(summaryColumn);
+ }
+ final String detailColumn =
+ a.getString(R.styleable.ContactsDataKind_android_detailColumn);
+ if (detailColumn != null) {
+ // Inflate specific column as summary
+ kind.actionBody = new SimpleInflater(detailColumn);
+ }
+
+ a.recycle();
+
+ addKind(kind);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ throw new DefinitionException("Problem reading XML", e);
+ } catch (IOException e) {
+ throw new DefinitionException("Problem reading XML", e);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/FallbackAccountType.java b/java/com/android/contacts/common/model/account/FallbackAccountType.java
new file mode 100644
index 000000000..976a7b892
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/FallbackAccountType.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+
+public class FallbackAccountType extends BaseAccountType {
+
+ private static final String TAG = "FallbackAccountType";
+
+ private FallbackAccountType(Context context, String resPackageName) {
+ this.accountType = null;
+ this.dataSet = null;
+ this.titleRes = R.string.account_phone;
+ this.iconRes = R.mipmap.ic_contacts_launcher;
+
+ // Note those are only set for unit tests.
+ this.resourcePackageName = resPackageName;
+ this.syncAdapterPackageName = resPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindSipAddress(context);
+ addDataKindGroupMembership(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ public FallbackAccountType(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml. In order to
+ * build {@link DataKind}s with the same resource package name, {@code resPackageName} is
+ * injectable.
+ */
+ static AccountType createWithPackageNameForTest(Context context, String resPackageName) {
+ return new FallbackAccountType(context, resPackageName);
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/GoogleAccountType.java b/java/com/android/contacts/common/model/account/GoogleAccountType.java
new file mode 100644
index 000000000..2f1fe0ed6
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/GoogleAccountType.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class GoogleAccountType extends BaseAccountType {
+
+ /**
+ * The package name that we should load contacts.xml from and rely on to handle G+ account
+ * actions. Even though this points to gms, in some cases gms will still hand off responsibility
+ * to the G+ app.
+ */
+ public static final String PLUS_EXTENSION_PACKAGE_NAME = "com.google.android.gms";
+
+ public static final String ACCOUNT_TYPE = "com.google";
+ private static final String TAG = "GoogleAccountType";
+ private static final List<String> mExtensionPackages =
+ new ArrayList<>(Collections.singletonList(PLUS_EXTENSION_PACKAGE_NAME));
+
+ public GoogleAccountType(Context context, String authenticatorPackageName) {
+ this.accountType = ACCOUNT_TYPE;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindSipAddress(context);
+ addDataKindGroupMembership(context);
+ addDataKindRelation(context);
+ addDataKindEvent(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ @Override
+ public List<String> getExtensionPackageNames() {
+ return mExtensionPackages;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ private DataKind addDataKindRelation(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Relation.CONTENT_ITEM_TYPE,
+ R.string.relationLabelsGroup,
+ Weight.RELATIONSHIP,
+ true));
+ kind.actionHeader = new RelationActionInflater();
+ kind.actionBody = new SimpleInflater(Relation.NAME);
+
+ kind.typeColumn = Relation.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+ kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+ kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+ kind.typeList.add(
+ buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ return kind;
+ }
+
+ private DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+ kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+ kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+ kind.typeList.add(
+ buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+
+ @Override
+ public String getViewContactNotifyServiceClassName() {
+ return "com.google.android.syncadapters.contacts." + "SyncHighResPhotoIntentService";
+ }
+
+ @Override
+ public String getViewContactNotifyServicePackageName() {
+ return "com.google.android.syncadapters.contacts";
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/SamsungAccountType.java b/java/com/android/contacts/common/model/account/SamsungAccountType.java
new file mode 100644
index 000000000..45406bc2b
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/SamsungAccountType.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * A writable account type that can be used to support samsung contacts. This may not perfectly
+ * match Samsung's latest intended account schema.
+ *
+ * <p>This is only used to partially support Samsung accounts. The DataKind labels & fields are
+ * setup to support the values used by Samsung. But, not everything in the Samsung account type is
+ * supported. The Samsung account type includes a "Message Type" mimetype that we have no intention
+ * of showing inside the Contact editor. Similarly, we don't handle the "Ringtone" mimetype here
+ * since managing ringtones is handled in a different flow.
+ */
+public class SamsungAccountType extends BaseAccountType {
+
+ private static final String TAG = "KnownExternalAccountType";
+ private static final String ACCOUNT_TYPE_SAMSUNG = "com.osp.app.signin";
+
+ public SamsungAccountType(Context context, String authenticatorPackageName, String type) {
+ this.accountType = type;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindGroupMembership(context);
+ addDataKindRelation(context);
+ addDataKindEvent(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ /**
+ * Returns {@code TRUE} if this is samsung's account type and Samsung hasn't bothered to define a
+ * contacts.xml to provide a more accurate definition than ours.
+ */
+ public static boolean isSamsungAccountType(Context context, String type, String packageName) {
+ return ACCOUNT_TYPE_SAMSUNG.equals(type)
+ && !ExternalAccountType.hasContactsXml(context, packageName);
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindStructuredPostal(context);
+
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ if (useJapaneseOrder) {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ private DataKind addDataKindRelation(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, R.string.relationLabelsGroup, 160, true));
+ kind.actionHeader = new RelationActionInflater();
+ kind.actionBody = new SimpleInflater(Relation.NAME);
+
+ kind.typeColumn = Relation.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+ kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+ kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+ kind.typeList.add(
+ buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ return kind;
+ }
+
+ private DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+ kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+ kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+ kind.typeList.add(
+ buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/DataItem.java b/java/com/android/contacts/common/model/dataitem/DataItem.java
new file mode 100644
index 000000000..dc746055b
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/DataItem.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.Contacts.Entity;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.model.account.AccountType.EditType;
+
+/** This is the base class for data items, which represents a row from the Data table. */
+public class DataItem implements Collapser.Collapsible<DataItem> {
+
+ private final ContentValues mContentValues;
+ protected DataKind mKind;
+
+ protected DataItem(ContentValues values) {
+ mContentValues = values;
+ }
+
+ /**
+ * Factory for creating subclasses of DataItem objects based on the mimetype in the content
+ * values. Raw contact is the raw contact that this data item is associated with.
+ */
+ public static DataItem createFrom(ContentValues values) {
+ final String mimeType = values.getAsString(Data.MIMETYPE);
+ if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new GroupMembershipDataItem(values);
+ } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredNameDataItem(values);
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhoneDataItem(values);
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EmailDataItem(values);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredPostalDataItem(values);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new ImDataItem(values);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new OrganizationDataItem(values);
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NicknameDataItem(values);
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NoteDataItem(values);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new WebsiteDataItem(values);
+ } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new SipAddressDataItem(values);
+ } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EventDataItem(values);
+ } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new RelationDataItem(values);
+ } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new IdentityDataItem(values);
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhotoDataItem(values);
+ }
+
+ // generic
+ return new DataItem(values);
+ }
+
+ public ContentValues getContentValues() {
+ return mContentValues;
+ }
+
+ public Long getRawContactId() {
+ return mContentValues.getAsLong(Data.RAW_CONTACT_ID);
+ }
+
+ public void setRawContactId(long rawContactId) {
+ mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ }
+
+ /** Returns the data id. */
+ public long getId() {
+ return mContentValues.getAsLong(Data._ID);
+ }
+
+ /** Returns the mimetype of the data. */
+ public String getMimeType() {
+ return mContentValues.getAsString(Data.MIMETYPE);
+ }
+
+ public void setMimeType(String mimeType) {
+ mContentValues.put(Data.MIMETYPE, mimeType);
+ }
+
+ public boolean isPrimary() {
+ Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY);
+ return primary != null && primary != 0;
+ }
+
+ public boolean isSuperPrimary() {
+ Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+ return superPrimary != null && superPrimary != 0;
+ }
+
+ public boolean hasKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return key != null
+ && mContentValues.containsKey(key)
+ && mContentValues.getAsInteger(key) != null;
+ }
+
+ public int getKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return mContentValues.getAsInteger(key);
+ }
+
+ /**
+ * Indicates the carrier presence value for the current {@link DataItem}.
+ *
+ * @return {@link Data#CARRIER_PRESENCE_VT_CAPABLE} if the {@link DataItem} supports carrier video
+ * calling, {@code 0} otherwise.
+ */
+ public int getCarrierPresence() {
+ return mContentValues.getAsInteger(Data.CARRIER_PRESENCE);
+ }
+
+ /**
+ * This builds the data string depending on the type of data item by using the generic DataKind
+ * object underneath.
+ */
+ public String buildDataString(Context context, DataKind kind) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ /**
+ * This builds the data string(intended for display) depending on the type of data item. It
+ * returns the same value as {@link #buildDataString} by default, but certain data items can
+ * override it to provide their version of formatted data strings.
+ *
+ * @return Data string representing the data item, possibly formatted for display
+ */
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ return buildDataString(context, kind);
+ }
+
+ public DataKind getDataKind() {
+ return mKind;
+ }
+
+ public void setDataKind(DataKind kind) {
+ mKind = kind;
+ }
+
+ public Integer getTimesUsed() {
+ return mContentValues.getAsInteger(Entity.TIMES_USED);
+ }
+
+ public Long getLastTimeUsed() {
+ return mContentValues.getAsLong(Entity.LAST_TIME_USED);
+ }
+
+ @Override
+ public void collapseWith(DataItem that) {
+ DataKind thisKind = getDataKind();
+ DataKind thatKind = that.getDataKind();
+ // If this does not have a type and that does, or if that's type is higher precedence,
+ // use that's type
+ if ((!hasKindTypeColumn(thisKind) && that.hasKindTypeColumn(thatKind))
+ || (that.hasKindTypeColumn(thatKind)
+ && getTypePrecedence(thisKind, getKindTypeColumn(thisKind))
+ > getTypePrecedence(thatKind, that.getKindTypeColumn(thatKind)))) {
+ mContentValues.put(thatKind.typeColumn, that.getKindTypeColumn(thatKind));
+ mKind = thatKind;
+ }
+
+ // Choose the max of the maxLines and maxLabelLines values.
+ mKind.maxLinesForDisplay = Math.max(thisKind.maxLinesForDisplay, thatKind.maxLinesForDisplay);
+
+ // If any of the collapsed entries are super primary make the whole thing super primary.
+ if (isSuperPrimary() || that.isSuperPrimary()) {
+ mContentValues.put(Data.IS_SUPER_PRIMARY, 1);
+ mContentValues.put(Data.IS_PRIMARY, 1);
+ }
+
+ // If any of the collapsed entries are primary make the whole thing primary.
+ if (isPrimary() || that.isPrimary()) {
+ mContentValues.put(Data.IS_PRIMARY, 1);
+ }
+
+ // Add up the times used
+ mContentValues.put(
+ Entity.TIMES_USED,
+ (getTimesUsed() == null ? 0 : getTimesUsed())
+ + (that.getTimesUsed() == null ? 0 : that.getTimesUsed()));
+
+ // Use the most recent time
+ mContentValues.put(
+ Entity.LAST_TIME_USED,
+ Math.max(
+ getLastTimeUsed() == null ? 0 : getLastTimeUsed(),
+ that.getLastTimeUsed() == null ? 0 : that.getLastTimeUsed()));
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ return MoreContactUtils.shouldCollapse(
+ getMimeType(),
+ buildDataString(context, mKind),
+ t.getMimeType(),
+ t.buildDataString(context, t.getDataKind()));
+ }
+
+ /**
+ * Return the precedence for the the given {@link EditType#rawValue}, where lower numbers are
+ * higher precedence.
+ */
+ private static int getTypePrecedence(DataKind kind, int rawValue) {
+ for (int i = 0; i < kind.typeList.size(); i++) {
+ final EditType type = kind.typeList.get(i);
+ if (type.rawValue == rawValue) {
+ return i;
+ }
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/DataKind.java b/java/com/android/contacts/common/model/dataitem/DataKind.java
new file mode 100644
index 000000000..3b470a2ae
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/DataKind.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.Data;
+import com.android.contacts.common.model.account.AccountType.EditField;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.AccountType.StringInflater;
+import com.google.common.collect.Iterators;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+/**
+ * Description of a specific data type, usually marked by a unique {@link Data#MIMETYPE}. Includes
+ * details about how to view and edit {@link Data} rows of this kind, including the possible {@link
+ * EditType} labels and editable {@link EditField}.
+ */
+public final class DataKind {
+
+ public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName";
+ public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName";
+ public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName";
+
+ public String resourcePackageName;
+ public String mimeType;
+ public int titleRes;
+ public int iconAltRes;
+ public int iconAltDescriptionRes;
+ public int weight;
+ public boolean editable;
+
+ public StringInflater actionHeader;
+ public StringInflater actionAltHeader;
+ public StringInflater actionBody;
+
+ public String typeColumn;
+
+ /** Maximum number of values allowed in the list. -1 represents infinity. */
+ public int typeOverallMax;
+
+ public List<EditType> typeList;
+ public List<EditField> fieldList;
+
+ public ContentValues defaultValues;
+
+ /**
+ * If this is a date field, this specifies the format of the date when saving. The date includes
+ * year, month and day. If this is not a date field or the date field is not editable, this value
+ * should be ignored.
+ */
+ public SimpleDateFormat dateFormatWithoutYear;
+
+ /**
+ * If this is a date field, this specifies the format of the date when saving. The date includes
+ * month and day. If this is not a date field, the field is not editable or dates without year are
+ * not supported, this value should be ignored.
+ */
+ public SimpleDateFormat dateFormatWithYear;
+
+ /** The number of lines available for displaying this kind of data. Defaults to 1. */
+ public int maxLinesForDisplay;
+
+ public DataKind() {
+ maxLinesForDisplay = 1;
+ }
+
+ public DataKind(String mimeType, int titleRes, int weight, boolean editable) {
+ this.mimeType = mimeType;
+ this.titleRes = titleRes;
+ this.weight = weight;
+ this.editable = editable;
+ this.typeOverallMax = -1;
+ maxLinesForDisplay = 1;
+ }
+
+ public static String toString(SimpleDateFormat format) {
+ return format == null ? "(null)" : format.toPattern();
+ }
+
+ public static String toString(Iterable<?> list) {
+ if (list == null) {
+ return "(null)";
+ } else {
+ return Iterators.toString(list.iterator());
+ }
+ }
+
+ public String getKindString(Context context) {
+ return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DataKind:");
+ sb.append(" resPackageName=").append(resourcePackageName);
+ sb.append(" mimeType=").append(mimeType);
+ sb.append(" titleRes=").append(titleRes);
+ sb.append(" iconAltRes=").append(iconAltRes);
+ sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes);
+ sb.append(" weight=").append(weight);
+ sb.append(" editable=").append(editable);
+ sb.append(" actionHeader=").append(actionHeader);
+ sb.append(" actionAltHeader=").append(actionAltHeader);
+ sb.append(" actionBody=").append(actionBody);
+ sb.append(" typeColumn=").append(typeColumn);
+ sb.append(" typeOverallMax=").append(typeOverallMax);
+ sb.append(" typeList=").append(toString(typeList));
+ sb.append(" fieldList=").append(toString(fieldList));
+ sb.append(" defaultValues=").append(defaultValues);
+ sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear));
+ sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear));
+
+ return sb.toString();
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/EmailDataItem.java b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java
new file mode 100644
index 000000000..2fe297816
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+/**
+ * Represents an email data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+ /* package */ EmailDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getAddress() {
+ return getContentValues().getAsString(Email.ADDRESS);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(Email.DISPLAY_NAME);
+ }
+
+ public String getData() {
+ return getContentValues().getAsString(Email.DATA);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Email.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/EventDataItem.java b/java/com/android/contacts/common/model/dataitem/EventDataItem.java
new file mode 100644
index 000000000..15d9880b1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/EventDataItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.text.TextUtils;
+
+/**
+ * Represents an event data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+ /* package */ EventDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getStartDate() {
+ return getContentValues().getAsString(Event.START_DATE);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Event.LABEL);
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof EventDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final EventDataItem that = (EventDataItem) t;
+ // Events can be different (anniversary, birthday) but have the same start date
+ if (!TextUtils.equals(getStartDate(), that.getStartDate())) {
+ return false;
+ } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+ return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+ } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+ return false;
+ } else if (getKindTypeColumn(mKind) == Event.TYPE_CUSTOM
+ && !TextUtils.equals(getLabel(), that.getLabel())) {
+ // Check if custom types are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 000000000..f921b3c9d
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+ /* package */ GroupMembershipDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getGroupRowId() {
+ return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID);
+ }
+
+ public String getGroupSourceId() {
+ return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java
new file mode 100644
index 000000000..2badf92f7
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+/**
+ * Represents an identity data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+ /* package */ IdentityDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getIdentity() {
+ return getContentValues().getAsString(Identity.IDENTITY);
+ }
+
+ public String getNamespace() {
+ return getContentValues().getAsString(Identity.NAMESPACE);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/ImDataItem.java b/java/com/android/contacts/common/model/dataitem/ImDataItem.java
new file mode 100644
index 000000000..16b9fd094
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/ImDataItem.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.text.TextUtils;
+
+/**
+ * Represents an IM data item, wrapping the columns in {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+ private final boolean mCreatedFromEmail;
+
+ /* package */ ImDataItem(ContentValues values) {
+ super(values);
+ mCreatedFromEmail = false;
+ }
+
+ private ImDataItem(ContentValues values, boolean createdFromEmail) {
+ super(values);
+ mCreatedFromEmail = createdFromEmail;
+ }
+
+ public static ImDataItem createFromEmail(EmailDataItem item) {
+ final ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true);
+ im.setMimeType(Im.CONTENT_ITEM_TYPE);
+ return im;
+ }
+
+ public String getData() {
+ if (mCreatedFromEmail) {
+ return getContentValues().getAsString(Email.DATA);
+ } else {
+ return getContentValues().getAsString(Im.DATA);
+ }
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Im.LABEL);
+ }
+
+ /** Values are one of Im.PROTOCOL_ */
+ public Integer getProtocol() {
+ return getContentValues().getAsInteger(Im.PROTOCOL);
+ }
+
+ public boolean isProtocolValid() {
+ return getProtocol() != null;
+ }
+
+ public String getCustomProtocol() {
+ return getContentValues().getAsString(Im.CUSTOM_PROTOCOL);
+ }
+
+ public int getChatCapability() {
+ Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY);
+ return result == null ? 0 : result;
+ }
+
+ public boolean isCreatedFromEmail() {
+ return mCreatedFromEmail;
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof ImDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final ImDataItem that = (ImDataItem) t;
+ // IM can have the same data put different protocol. These should not collapse.
+ if (!getData().equals(that.getData())) {
+ return false;
+ } else if (!isProtocolValid() || !that.isProtocolValid()) {
+ // Deal with invalid protocol as if it was custom. If either has a non valid
+ // protocol, check to see if the other has a valid that is not custom
+ if (isProtocolValid()) {
+ return getProtocol() == Im.PROTOCOL_CUSTOM;
+ } else if (that.isProtocolValid()) {
+ return that.getProtocol() == Im.PROTOCOL_CUSTOM;
+ }
+ return true;
+ } else if (getProtocol() != that.getProtocol()) {
+ return false;
+ } else if (getProtocol() == Im.PROTOCOL_CUSTOM
+ && !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) {
+ // Check if custom protocols are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java
new file mode 100644
index 000000000..a448be786
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+/**
+ * Represents a nickname data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+ public NicknameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Nickname.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Nickname.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/NoteDataItem.java b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java
new file mode 100644
index 000000000..b55ecc3e5
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+/**
+ * Represents a note data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+ /* package */ NoteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNote() {
+ return getContentValues().getAsString(Note.NOTE);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 000000000..b33124838
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+/**
+ * Represents an organization data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+ /* package */ OrganizationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getCompany() {
+ return getContentValues().getAsString(Organization.COMPANY);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Organization.LABEL);
+ }
+
+ public String getTitle() {
+ return getContentValues().getAsString(Organization.TITLE);
+ }
+
+ public String getDepartment() {
+ return getContentValues().getAsString(Organization.DEPARTMENT);
+ }
+
+ public String getJobDescription() {
+ return getContentValues().getAsString(Organization.JOB_DESCRIPTION);
+ }
+
+ public String getSymbol() {
+ return getContentValues().getAsString(Organization.SYMBOL);
+ }
+
+ public String getPhoneticName() {
+ return getContentValues().getAsString(Organization.PHONETIC_NAME);
+ }
+
+ public String getOfficeLocation() {
+ return getContentValues().getAsString(Organization.OFFICE_LOCATION);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java
new file mode 100644
index 000000000..e1f56456a
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+
+/**
+ * Represents a phone data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+ public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber";
+
+ /* package */ PhoneDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNumber() {
+ return getContentValues().getAsString(Phone.NUMBER);
+ }
+
+ /** Returns the normalized phone number in E164 format. */
+ public String getNormalizedNumber() {
+ return getContentValues().getAsString(Phone.NORMALIZED_NUMBER);
+ }
+
+ public String getFormattedPhoneNumber() {
+ return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Phone.LABEL);
+ }
+
+ public void computeFormattedPhoneNumber(String defaultCountryIso) {
+ final String phoneNumber = getNumber();
+ if (phoneNumber != null) {
+ final String formattedPhoneNumber =
+ PhoneNumberUtilsCompat.formatNumber(
+ phoneNumber, getNormalizedNumber(), defaultCountryIso);
+ getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber);
+ }
+ }
+
+ /**
+ * Returns the formatted phone number (if already computed using {@link
+ * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number.
+ */
+ @Override
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ final String formatted = getFormattedPhoneNumber();
+ if (formatted != null) {
+ return formatted;
+ } else {
+ return getNumber();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java
new file mode 100644
index 000000000..0bf7a318b
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+/**
+ * Represents a photo data item, wrapping the columns in {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+ /* package */ PhotoDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getPhotoFileId() {
+ return getContentValues().getAsLong(Photo.PHOTO_FILE_ID);
+ }
+
+ public byte[] getPhoto() {
+ return getContentValues().getAsByteArray(Photo.PHOTO);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/RelationDataItem.java b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java
new file mode 100644
index 000000000..fdbcbb313
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.text.TextUtils;
+
+/**
+ * Represents a relation data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+ /* package */ RelationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Relation.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Relation.LABEL);
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof RelationDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final RelationDataItem that = (RelationDataItem) t;
+ // Relations can have different types (assistant, father) but have the same name
+ if (!TextUtils.equals(getName(), that.getName())) {
+ return false;
+ } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+ return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+ } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+ return false;
+ } else if (getKindTypeColumn(mKind) == Relation.TYPE_CUSTOM
+ && !TextUtils.equals(getLabel(), that.getLabel())) {
+ // Check if custom types are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 000000000..0ca9eae6d
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+/**
+ * Represents a sip address data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+ /* package */ SipAddressDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getSipAddress() {
+ return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(SipAddress.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 000000000..22bf037f1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Represents a structured name data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+ public StructuredNameDataItem() {
+ super(new ContentValues());
+ getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ }
+
+ /* package */ StructuredNameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+ }
+
+ public void setDisplayName(String name) {
+ getContentValues().put(StructuredName.DISPLAY_NAME, name);
+ }
+
+ public String getGivenName() {
+ return getContentValues().getAsString(StructuredName.GIVEN_NAME);
+ }
+
+ public String getFamilyName() {
+ return getContentValues().getAsString(StructuredName.FAMILY_NAME);
+ }
+
+ public String getPrefix() {
+ return getContentValues().getAsString(StructuredName.PREFIX);
+ }
+
+ public String getMiddleName() {
+ return getContentValues().getAsString(StructuredName.MIDDLE_NAME);
+ }
+
+ public String getSuffix() {
+ return getContentValues().getAsString(StructuredName.SUFFIX);
+ }
+
+ public String getPhoneticGivenName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+ }
+
+ public void setPhoneticGivenName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name);
+ }
+
+ public String getPhoneticMiddleName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+ }
+
+ public void setPhoneticMiddleName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name);
+ }
+
+ public String getPhoneticFamilyName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+ }
+
+ public void setPhoneticFamilyName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name);
+ }
+
+ public String getFullNameStyle() {
+ return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE);
+ }
+
+ public boolean isSuperPrimary() {
+ final ContentValues contentValues = getContentValues();
+ return contentValues == null || !contentValues.containsKey(StructuredName.IS_SUPER_PRIMARY)
+ ? false
+ : contentValues.getAsBoolean(StructuredName.IS_SUPER_PRIMARY);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 000000000..18aae282c
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+ /* package */ StructuredPostalDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getFormattedAddress() {
+ return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(StructuredPostal.LABEL);
+ }
+
+ public String getStreet() {
+ return getContentValues().getAsString(StructuredPostal.STREET);
+ }
+
+ public String getPOBox() {
+ return getContentValues().getAsString(StructuredPostal.POBOX);
+ }
+
+ public String getNeighborhood() {
+ return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD);
+ }
+
+ public String getCity() {
+ return getContentValues().getAsString(StructuredPostal.CITY);
+ }
+
+ public String getRegion() {
+ return getContentValues().getAsString(StructuredPostal.REGION);
+ }
+
+ public String getPostcode() {
+ return getContentValues().getAsString(StructuredPostal.POSTCODE);
+ }
+
+ public String getCountry() {
+ return getContentValues().getAsString(StructuredPostal.COUNTRY);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 000000000..b8400ecd1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+/**
+ * Represents a website data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+ /* package */ WebsiteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getUrl() {
+ return getContentValues().getAsString(Website.URL);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Website.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/preference/ContactsPreferences.java b/java/com/android/contacts/common/preference/ContactsPreferences.java
new file mode 100644
index 000000000..7f0d99acd
--- /dev/null
+++ b/java/com/android/contacts/common/preference/ContactsPreferences.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+/** Manages user preferences for contacts. */
+public class ContactsPreferences implements OnSharedPreferenceChangeListener {
+
+ /** The value for the DISPLAY_ORDER key to show the given name first. */
+ public static final int DISPLAY_ORDER_PRIMARY = 1;
+
+ /** The value for the DISPLAY_ORDER key to show the family name first. */
+ public static final int DISPLAY_ORDER_ALTERNATIVE = 2;
+
+ public static final String DISPLAY_ORDER_KEY = "android.contacts.DISPLAY_ORDER";
+
+ /** The value for the SORT_ORDER key corresponding to sort by given name first. */
+ public static final int SORT_ORDER_PRIMARY = 1;
+
+ public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER";
+
+ /** The value for the SORT_ORDER key corresponding to sort by family name first. */
+ public static final int SORT_ORDER_ALTERNATIVE = 2;
+
+ public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones";
+
+ public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
+
+ /**
+ * Value to use when a preference is unassigned and needs to be read from the shared preferences
+ */
+ private static final int PREFERENCE_UNASSIGNED = -1;
+
+ private final Context mContext;
+ private final SharedPreferences mPreferences;
+ private int mSortOrder = PREFERENCE_UNASSIGNED;
+ private int mDisplayOrder = PREFERENCE_UNASSIGNED;
+ private String mDefaultAccount = null;
+ private ChangeListener mListener = null;
+ private Handler mHandler;
+ private String mDefaultAccountKey;
+ private String mDefaultAccountSavedKey;
+
+ public ContactsPreferences(Context context) {
+ mContext = context;
+ mHandler = new Handler();
+ mPreferences = mContext.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
+ mDefaultAccountKey =
+ mContext.getResources().getString(R.string.contact_editor_default_account_key);
+ mDefaultAccountSavedKey =
+ mContext.getResources().getString(R.string.contact_editor_anything_saved_key);
+ maybeMigrateSystemSettings();
+ }
+
+ public boolean isSortOrderUserChangeable() {
+ return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable);
+ }
+
+ public int getDefaultSortOrder() {
+ if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) {
+ return SORT_ORDER_PRIMARY;
+ } else {
+ return SORT_ORDER_ALTERNATIVE;
+ }
+ }
+
+ public int getSortOrder() {
+ if (!isSortOrderUserChangeable()) {
+ return getDefaultSortOrder();
+ }
+ if (mSortOrder == PREFERENCE_UNASSIGNED) {
+ mSortOrder = mPreferences.getInt(SORT_ORDER_KEY, getDefaultSortOrder());
+ }
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ final Editor editor = mPreferences.edit();
+ editor.putInt(SORT_ORDER_KEY, sortOrder);
+ editor.commit();
+ }
+
+ public boolean isDisplayOrderUserChangeable() {
+ return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable);
+ }
+
+ public int getDefaultDisplayOrder() {
+ if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) {
+ return DISPLAY_ORDER_PRIMARY;
+ } else {
+ return DISPLAY_ORDER_ALTERNATIVE;
+ }
+ }
+
+ public int getDisplayOrder() {
+ if (!isDisplayOrderUserChangeable()) {
+ return getDefaultDisplayOrder();
+ }
+ if (mDisplayOrder == PREFERENCE_UNASSIGNED) {
+ mDisplayOrder = mPreferences.getInt(DISPLAY_ORDER_KEY, getDefaultDisplayOrder());
+ }
+ return mDisplayOrder;
+ }
+
+ public void setDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ final Editor editor = mPreferences.edit();
+ editor.putInt(DISPLAY_ORDER_KEY, displayOrder);
+ editor.commit();
+ }
+
+ public boolean isDefaultAccountUserChangeable() {
+ return mContext.getResources().getBoolean(R.bool.config_default_account_user_changeable);
+ }
+
+ public String getDefaultAccount() {
+ if (!isDefaultAccountUserChangeable()) {
+ return mDefaultAccount;
+ }
+ if (TextUtils.isEmpty(mDefaultAccount)) {
+ final String accountString = mPreferences.getString(mDefaultAccountKey, mDefaultAccount);
+ if (!TextUtils.isEmpty(accountString)) {
+ final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(accountString);
+ mDefaultAccount = accountWithDataSet.name;
+ }
+ }
+ return mDefaultAccount;
+ }
+
+ public void setDefaultAccount(AccountWithDataSet accountWithDataSet) {
+ mDefaultAccount = accountWithDataSet == null ? null : accountWithDataSet.name;
+ final Editor editor = mPreferences.edit();
+ if (TextUtils.isEmpty(mDefaultAccount)) {
+ editor.remove(mDefaultAccountKey);
+ } else {
+ editor.putString(mDefaultAccountKey, accountWithDataSet.stringify());
+ }
+ editor.putBoolean(mDefaultAccountSavedKey, true);
+ editor.commit();
+ }
+
+ public void registerChangeListener(ChangeListener listener) {
+ if (mListener != null) {
+ unregisterChangeListener();
+ }
+
+ mListener = listener;
+
+ // Reset preferences to "unknown" because they may have changed while the
+ // listener was unregistered.
+ mDisplayOrder = PREFERENCE_UNASSIGNED;
+ mSortOrder = PREFERENCE_UNASSIGNED;
+ mDefaultAccount = null;
+
+ mPreferences.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ public void unregisterChangeListener() {
+ if (mListener != null) {
+ mListener = null;
+ }
+
+ mPreferences.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
+ // This notification is not sent on the Ui thread. Use the previously created Handler
+ // to switch to the Ui thread
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ refreshValue(key);
+ }
+ });
+ }
+
+ /**
+ * Forces the value for the given key to be looked up from shared preferences and notifies the
+ * registered {@link ChangeListener}
+ *
+ * @param key the {@link SharedPreferences} key to look up
+ */
+ public void refreshValue(String key) {
+ if (DISPLAY_ORDER_KEY.equals(key)) {
+ mDisplayOrder = PREFERENCE_UNASSIGNED;
+ mDisplayOrder = getDisplayOrder();
+ } else if (SORT_ORDER_KEY.equals(key)) {
+ mSortOrder = PREFERENCE_UNASSIGNED;
+ mSortOrder = getSortOrder();
+ } else if (mDefaultAccountKey.equals(key)) {
+ mDefaultAccount = null;
+ mDefaultAccount = getDefaultAccount();
+ }
+ if (mListener != null) {
+ mListener.onChange();
+ }
+ }
+
+ /**
+ * If there are currently no preferences (which means this is the first time we are run), For sort
+ * order and display order, check to see if there are any preferences stored in system settings
+ * (pre-L) which can be copied into our own SharedPreferences. For default account setting, check
+ * to see if there are any preferences stored in the previous SharedPreferences which can be
+ * copied into current SharedPreferences.
+ */
+ private void maybeMigrateSystemSettings() {
+ if (!mPreferences.contains(SORT_ORDER_KEY)) {
+ int sortOrder = getDefaultSortOrder();
+ try {
+ sortOrder = Settings.System.getInt(mContext.getContentResolver(), SORT_ORDER_KEY);
+ } catch (SettingNotFoundException e) {
+ }
+ setSortOrder(sortOrder);
+ }
+
+ if (!mPreferences.contains(DISPLAY_ORDER_KEY)) {
+ int displayOrder = getDefaultDisplayOrder();
+ try {
+ displayOrder = Settings.System.getInt(mContext.getContentResolver(), DISPLAY_ORDER_KEY);
+ } catch (SettingNotFoundException e) {
+ }
+ setDisplayOrder(displayOrder);
+ }
+
+ if (!mPreferences.contains(mDefaultAccountKey)) {
+ final SharedPreferences previousPrefs =
+ PreferenceManager.getDefaultSharedPreferences(mContext);
+ final String defaultAccount = previousPrefs.getString(mDefaultAccountKey, null);
+ if (!TextUtils.isEmpty(defaultAccount)) {
+ final AccountWithDataSet accountWithDataSet =
+ AccountWithDataSet.unstringify(defaultAccount);
+ setDefaultAccount(accountWithDataSet);
+ }
+ }
+ }
+
+ public interface ChangeListener {
+
+ void onChange();
+ }
+}
diff --git a/java/com/android/contacts/common/preference/DisplayOrderPreference.java b/java/com/android/contacts/common/preference/DisplayOrderPreference.java
new file mode 100644
index 000000000..8dda57f9f
--- /dev/null
+++ b/java/com/android/contacts/common/preference/DisplayOrderPreference.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import com.android.contacts.common.R;
+
+/** Custom preference: view-name-as (first name first or last name first). */
+public final class DisplayOrderPreference extends ListPreference {
+
+ private ContactsPreferences mPreferences;
+ private Context mContext;
+
+ public DisplayOrderPreference(Context context) {
+ super(context);
+ prepare();
+ }
+
+ public DisplayOrderPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ prepare();
+ }
+
+ private void prepare() {
+ mContext = getContext();
+ mPreferences = new ContactsPreferences(mContext);
+ setEntries(
+ new String[] {
+ mContext.getString(R.string.display_options_view_given_name_first),
+ mContext.getString(R.string.display_options_view_family_name_first),
+ });
+ setEntryValues(
+ new String[] {
+ String.valueOf(ContactsPreferences.DISPLAY_ORDER_PRIMARY),
+ String.valueOf(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE),
+ });
+ setValue(String.valueOf(mPreferences.getDisplayOrder()));
+ }
+
+ @Override
+ protected boolean shouldPersist() {
+ return false; // This preference takes care of its own storage
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ switch (mPreferences.getDisplayOrder()) {
+ case ContactsPreferences.DISPLAY_ORDER_PRIMARY:
+ return mContext.getString(R.string.display_options_view_given_name_first);
+ case ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE:
+ return mContext.getString(R.string.display_options_view_family_name_first);
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ int newValue = Integer.parseInt(value);
+ if (newValue != mPreferences.getDisplayOrder()) {
+ mPreferences.setDisplayOrder(newValue);
+ notifyChanged();
+ }
+ return true;
+ }
+
+ @Override
+ // UX recommendation is not to show cancel button on such lists.
+ protected void onPrepareDialogBuilder(Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+ builder.setNegativeButton(null, null);
+ }
+}
diff --git a/java/com/android/contacts/common/preference/SortOrderPreference.java b/java/com/android/contacts/common/preference/SortOrderPreference.java
new file mode 100644
index 000000000..9b6f57860
--- /dev/null
+++ b/java/com/android/contacts/common/preference/SortOrderPreference.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import com.android.contacts.common.R;
+
+/** Custom preference: sort-by. */
+public final class SortOrderPreference extends ListPreference {
+
+ private ContactsPreferences mPreferences;
+ private Context mContext;
+
+ public SortOrderPreference(Context context) {
+ super(context);
+ prepare();
+ }
+
+ public SortOrderPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ prepare();
+ }
+
+ private void prepare() {
+ mContext = getContext();
+ mPreferences = new ContactsPreferences(mContext);
+ setEntries(
+ new String[] {
+ mContext.getString(R.string.display_options_sort_by_given_name),
+ mContext.getString(R.string.display_options_sort_by_family_name),
+ });
+ setEntryValues(
+ new String[] {
+ String.valueOf(ContactsPreferences.SORT_ORDER_PRIMARY),
+ String.valueOf(ContactsPreferences.SORT_ORDER_ALTERNATIVE),
+ });
+ setValue(String.valueOf(mPreferences.getSortOrder()));
+ }
+
+ @Override
+ protected boolean shouldPersist() {
+ return false; // This preference takes care of its own storage
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ switch (mPreferences.getSortOrder()) {
+ case ContactsPreferences.SORT_ORDER_PRIMARY:
+ return mContext.getString(R.string.display_options_sort_by_given_name);
+ case ContactsPreferences.SORT_ORDER_ALTERNATIVE:
+ return mContext.getString(R.string.display_options_sort_by_family_name);
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ int newValue = Integer.parseInt(value);
+ if (newValue != mPreferences.getSortOrder()) {
+ mPreferences.setSortOrder(newValue);
+ notifyChanged();
+ }
+ return true;
+ }
+
+ @Override
+ // UX recommendation is not to show cancel button on such lists.
+ protected void onPrepareDialogBuilder(Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+ builder.setNegativeButton(null, null);
+ }
+}
diff --git a/java/com/android/contacts/common/res/color/popup_menu_color.xml b/java/com/android/contacts/common/res/color/popup_menu_color.xml
new file mode 100644
index 000000000..c52bd5b50
--- /dev/null
+++ b/java/com/android/contacts/common/res/color/popup_menu_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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:alpha="0.5" android:color="#ff000000" android:state_enabled="false"/>
+ <item android:color="#ff000000"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/color/tab_text_color.xml b/java/com/android/contacts/common/res/color/tab_text_color.xml
new file mode 100644
index 000000000..71ef3e903
--- /dev/null
+++ b/java/com/android/contacts/common/res/color/tab_text_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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:color="@color/actionbar_text_color" android:state_selected="true"/>
+ <item android:color="@color/actionbar_unselected_text_color"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png
new file mode 100644
index 000000000..d86b2195a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..ddbb2c459
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..d5942dcad
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png
new file mode 100644
index 000000000..4dc506515
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..503e58e22
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png
new file mode 100644
index 000000000..969552935
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png
new file mode 100644
index 000000000..540ab4dee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..017e4bbf7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..703d30b92
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..c7b1113cf
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png
new file mode 100644
index 000000000..deb3a6dc1
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..06bd18fbb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..d829d11e2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..1ba12950c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..5ff3ac574
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..b4ebfc7b2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..03fd2fb10
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..e8cb0f5fe
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png
new file mode 100644
index 000000000..45137967c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..1c9bb81fa
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png
new file mode 100644
index 000000000..57177b7c6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png
new file mode 100644
index 000000000..56708b0ba
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..10ae5a70c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png
new file mode 100644
index 000000000..84b1227bd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..ccdda6701
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..3aa29b852
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..603ddc895
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png
new file mode 100644
index 000000000..97905c9f5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..c74bfab13
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..4ea7afa00
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png
new file mode 100644
index 000000000..cddf9be75
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..86578be45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..e9afcc924
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..2054530ed
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..a0f17568e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png
new file mode 100644
index 000000000..ae937176e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png
new file mode 100644
index 000000000..0d80482a9
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..4139942d6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..569d28f54
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png
new file mode 100644
index 000000000..5ec4c96a7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png
new file mode 100644
index 000000000..d86d61164
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..4139942d6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..065ff62ce
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png
new file mode 100644
index 000000000..013d5e711
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..947f03cec
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..6d09d7278
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..63c7456f0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png
new file mode 100644
index 000000000..f709f2ce4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..4139942d6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..af5855420
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png
new file mode 100644
index 000000000..cb801ac1b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png
new file mode 100644
index 000000000..2b23b1ec5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..1a21fb400
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..3dddca516
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png
new file mode 100644
index 000000000..77f9de5e3
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..9d359db9f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png
new file mode 100644
index 000000000..590a728ad
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png
new file mode 100644
index 000000000..8a2df3992
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..ad268bf2f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..b3000d31e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..353e06495
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png
new file mode 100644
index 000000000..201ad40cb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..9aac6d79b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..39c16ed2d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..841509682
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..b8fc39aee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..736dfd6fa
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..8c44e7015
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..c16c6c5de
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..c67170e31
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png
new file mode 100644
index 000000000..3072b7569
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png
new file mode 100644
index 000000000..f0b1c725d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..38e0a2882
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png
new file mode 100644
index 000000000..fc4ddd32c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..1b43a07d0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..af75db4b4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..64995d147
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png
new file mode 100644
index 000000000..dc9655b6d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..a5a30213d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..3bf8e0362
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png
new file mode 100644
index 000000000..7d5d66de3
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..86578be45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..3226ab760
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..061904c42
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..1d9371de0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png
new file mode 100644
index 000000000..64bd6912c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..046b24a96
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..1ff337370
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..2eb7c7ebc
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png
new file mode 100644
index 000000000..71f782701
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..bb7327251
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..6256300b4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png
new file mode 100644
index 000000000..ef45e933a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..40eed1d12
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png
new file mode 100644
index 000000000..5769f1178
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png
new file mode 100644
index 000000000..48e75beee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..09c0e3efd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..e188d4a37
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..c571b2e3e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png
new file mode 100644
index 000000000..d2f709942
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..ce5f704ec
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..3d0580f93
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..f91b71847
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..2fbd458e9
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..2cdb2d7a1
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..65a6b7bbb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..48483a0b6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png
new file mode 100644
index 000000000..906791177
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..e053c757a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png
new file mode 100644
index 000000000..763767b4f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png
new file mode 100644
index 000000000..aea15f0be
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..7e7c289d4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png
new file mode 100644
index 000000000..fdfafed9a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..43319dc92
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..2d43c4d5b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..d2671edf7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png
new file mode 100644
index 000000000..c1783de67
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..ca9d7d66b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..eda10e612
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png
new file mode 100644
index 000000000..b65272542
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..86578be45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..5532e88c2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..f4af92657
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..8fb0636cf
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png
new file mode 100644
index 000000000..f4f00ca0f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png
new file mode 100644
index 000000000..142c5457d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..72c51b0d5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..8d67e448f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png
new file mode 100644
index 000000000..90ead2e45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..2656cad18
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png
new file mode 100644
index 000000000..670bf796c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png
new file mode 100644
index 000000000..24142c729
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..03cad4c90
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..f44df1afd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..c41a5fcff
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png
new file mode 100644
index 000000000..436a82da6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..a70c60c03
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..c64b9defe
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..ff1759b8f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..878e694ad
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..ed4138f15
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..0fec2f2b5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..fa682b11b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png
new file mode 100644
index 000000000..6c45bc8e6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..955f7383b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png
new file mode 100644
index 000000000..0a79824b8
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png
new file mode 100644
index 000000000..184f7418d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..8f744f039
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png
new file mode 100644
index 000000000..6a6cdeeaa
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..89d29b7f5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..55f1d1369
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..8d897ba5a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png
new file mode 100644
index 000000000..4ab5ad0ee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..d0979e9eb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..52c00ddcd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..3e4ca684e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..230d649bf
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..1352a1702
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png
new file mode 100644
index 000000000..7ddf14a0d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png
new file mode 100644
index 000000000..2ffb2ecae
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..ae01a04ae
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..1741675de
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png
new file mode 100644
index 000000000..b0e020573
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..903c1623d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png
new file mode 100644
index 000000000..3a5540ff6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png
new file mode 100644
index 000000000..d3ff0ecb6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..5b96af5b7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..3a82cab3b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png
new file mode 100644
index 000000000..fa7c17ac4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png
new file mode 100644
index 000000000..33d40d8b6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..2fa2cca80
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png
new file mode 100644
index 000000000..b072ad11f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..d90782a32
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png
new file mode 100644
index 000000000..0643ea55f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..1d6e1aa0f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable/dialog_background_material.xml b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml
new file mode 100644
index 000000000..1b71cd63a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:inset="16dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="2dp"/>
+ <solid android:color="@color/call_subject_history_background"/>
+ </shape>
+</inset>
diff --git a/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml
new file mode 100644
index 000000000..67645ff91
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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:drawable="@drawable/ic_scroll_handle_pressed" android:state_pressed="true"/>
+ <item android:drawable="@drawable/ic_scroll_handle_default"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml
new file mode 100644
index 000000000..56fab8f6f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_arrow_back_24dp"
+ android:tint="@color/actionbar_icon_color"/> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/ic_call.xml b/java/com/android/contacts/common/res/drawable/ic_call.xml
new file mode 100644
index 000000000..0fedd452f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_call.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_call_24dp"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml
new file mode 100644
index 000000000..3c6c8b534
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_message_24dp"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_more_vert.xml b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml
new file mode 100644
index 000000000..fcc3d9e4f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0"
+ android:width="24dp">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
+</vector>
diff --git a/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml
new file mode 100644
index 000000000..0af90edb3
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_person_add_24dp"
+ android:tint="@color/actionbar_icon_color"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml
new file mode 100644
index 000000000..ac932f87c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_scroll_handle"
+ android:tint="@color/dialer_secondary_text_color"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml
new file mode 100644
index 000000000..4838de58a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_scroll_handle"
+ android:tint="@color/dialtacts_theme_color"/> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml
new file mode 100644
index 000000000..801806044
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 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.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_person_add_24dp"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml
new file mode 100644
index 000000000..f1b5cba43
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_videocam"
+ android:tint="@color/search_video_call_icon_tint"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_all.xml b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml
new file mode 100644
index 000000000..9cc6fbc96
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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:drawable="@drawable/ic_menu_person_lt" android:state_selected="false"/>
+ <item android:drawable="@drawable/ic_menu_person_dk" android:state_selected="true"/>
+</selector>
+
diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml
new file mode 100644
index 000000000..6b3e7a415
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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:drawable="@drawable/ic_menu_group_lt" android:state_selected="false"/>
+ <item android:drawable="@drawable/ic_menu_group_dk" android:state_selected="true"/>
+</selector>
+
diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml
new file mode 100644
index 000000000..a12e0993e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml
@@ -0,0 +1,21 @@
+<?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:drawable="@drawable/ic_menu_star_lt" android:state_selected="false"/>
+ <item android:drawable="@drawable/ic_menu_star_dk" android:state_selected="true"/>
+</selector>
+
diff --git a/java/com/android/contacts/common/res/drawable/ic_work_profile.xml b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml
new file mode 100644
index 000000000..fc21100c0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="16dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48"
+ android:width="16dp">
+
+
+ <path
+ android:fillColor="#757575"
+ android:pathData="M28 33h-8v-3H6v8c0 2.2 1.8 4 4 4h28c2.2 0 4-1.8
+4-4v-8H28v3zm12-21h-7V9l-3-3H18l-3 3.1V12H8c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4
+4h12v-3h8v3h12c2.2 0 4-1.8 4-4v-8c0-2.2-1.8-4-4-4zm-10 0H18V9h12v3z"/>
+ <path
+ android:pathData="M0 0h48v48H0z"/>
+</vector>
diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml
new file mode 100644
index 000000000..94e309507
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackgroundBorderless -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_material_dark"/> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml
new file mode 100644
index 000000000..91ab763a5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackground -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_material_dark">
+ <item android:id="@android:id/mask">
+ <color android:color="@android:color/white"/>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_light.xml b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml
new file mode 100644
index 000000000..d41accb02
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackground -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_material_light">
+ <item android:id="@android:id/mask">
+ <color android:color="@android:color/white"/>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml
new file mode 100644
index 000000000..5b774fd20
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml
@@ -0,0 +1,20 @@
+<?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:drawable="@drawable/list_activated_holo" android:state_activated="true"/>
+ <item android:drawable="@drawable/list_background_holo"/>
+</selector>
diff --git a/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml
new file mode 100644
index 000000000..35fff99c2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<transition xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/list_pressed_holo_light"/>
+ <item android:drawable="@drawable/list_longpressed_holo_light"/>
+</transition>
diff --git a/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml
new file mode 100644
index 000000000..27614a1ac
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <size android:width="2dp"/>
+ <solid android:color="@color/dialtacts_theme_color"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/unread_count_background.xml b/java/com/android/contacts/common/res/drawable/unread_count_background.xml
new file mode 100644
index 000000000..4fc6b9b60
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/unread_count_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/tab_unread_count_background_radius"/>
+ <solid android:color="@color/tab_unread_count_background_color"/>
+</shape>
diff --git a/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml
new file mode 100644
index 000000000..bef30a434
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/tab_ripple_color">
+ <item android:id="@android:id/mask">
+ <color android:color="@android:color/white"/>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
new file mode 100644
index 000000000..2aa97722d
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<!-- layoutDirection set to ltr as a workaround to a framework bug (b/22010411) causing view with
+ layout_centerInParent inside a RelativeLayout to expand to screen width when RTL is active -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/view_pager_tab_background"
+ android:layoutDirection="ltr">
+ <!-- The tab icon -->
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"/>
+ <TextView
+ android:id="@+id/count"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/tab_unread_count_background_size"
+ android:layout_marginTop="@dimen/tab_unread_count_margin_top"
+ android:layout_marginStart="@dimen/tab_unread_count_margin_left"
+ android:layout_toStartOf="@id/icon"
+ android:paddingLeft="@dimen/tab_unread_count_text_padding"
+ android:paddingRight="@dimen/tab_unread_count_text_padding"
+ android:background="@drawable/unread_count_background"
+ android:fontFamily="sans-serif-medium"
+ android:importantForAccessibility="no"
+ android:layoutDirection="locale"
+ android:minWidth="@dimen/tab_unread_count_background_size"
+ android:textAlignment="center"
+ android:textColor="@color/tab_accent_color"
+ android:textSize="@dimen/tab_unread_count_text_size"/>
+</RelativeLayout>
+
diff --git a/java/com/android/contacts/common/res/layout/account_filter_header.xml b/java/com/android/contacts/common/res/layout/account_filter_header.xml
new file mode 100644
index 000000000..a12ab08fd
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/account_filter_header.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- Layout showing the type of account filter
+ (e.g. All contacts filter, custom filter, etc.),
+ which is the header of all contact lists. -->
+
+<!-- Solely used to set a background color -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/background_primary">
+ <!-- Used to show the touch feedback -->
+ <FrameLayout
+ android:id="@+id/account_filter_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/contact_browser_list_header_left_margin"
+ android:layout_marginEnd="@dimen/contact_browser_list_header_right_margin"
+ android:paddingTop="@dimen/list_header_extra_top_padding"
+ android:background="?android:attr/selectableItemBackground"
+ android:visibility="gone">
+ <!-- Shows the text and underlining -->
+ <TextView
+ android:id="@+id/account_filter_header"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:paddingStart="@dimen/contact_browser_list_item_text_indent"
+ android:paddingLeft="@dimen/contact_browser_list_item_text_indent"/>
+ </FrameLayout>
+</FrameLayout>
diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml
new file mode 100644
index 000000000..587626e8d
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/detail_network_icon_size"
+ android:layout_height="@dimen/detail_network_icon_size"
+ android:layout_margin="16dip"
+ android:layout_gravity="center_vertical"/>
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml
new file mode 100644
index 000000000..33821166e
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/detail_network_icon_size"
+ android:layout_height="@dimen/detail_network_icon_size"
+ android:layout_margin="24dip"
+ android:layout_gravity="center_vertical"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/call_subject_history.xml b/java/com/android/contacts/common/res/layout/call_subject_history.xml
new file mode 100644
index 000000000..733f1d8b6
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/call_subject_history.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+
+ <ListView
+ android:id="@+id/subject_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@color/call_subject_history_background"
+ android:divider="@null"
+ android:elevation="8dp"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml
new file mode 100644
index 000000000..c378f24b2
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_subject_history_item_padding"
+ android:paddingBottom="@dimen/call_subject_history_item_padding"
+ android:paddingStart="@dimen/call_subject_dialog_margin"
+ android:paddingEnd="@dimen/call_subject_dialog_margin"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textSize="@dimen/call_subject_dialog_primary_text_size"/>
diff --git a/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml
new file mode 100644
index 000000000..02a5c809c
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 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.
+-->
+
+<!-- The actual padding is embedded in a FrameLayout since we cannot change the
+ visibility of a header view in a ListView without having a parent view. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <View
+ android:id="@+id/contact_detail_list_padding"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_header_extra_top_padding"/>
+</FrameLayout>
diff --git a/java/com/android/contacts/common/res/layout/contact_list_card.xml b/java/com/android/contacts/common/res/layout/contact_list_card.xml
new file mode 100644
index 000000000..a04f4cad9
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/contact_list_card.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:visibility="invisible">
+ <View
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/contact_list_space_layout_weight"
+ android:background="@color/background_primary"/>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/contact_list_card_layout_weight"
+ android:background="@color/contact_all_list_background_color"
+ android:elevation="@dimen/contact_list_card_elevation"/>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/contact_list_space_layout_weight"
+ android:background="@color/background_primary"/>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/contact_list_content.xml b/java/com/android/contacts/common/res/layout/contact_list_content.xml
new file mode 100644
index 000000000..3ee27a0ad
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/contact_list_content.xml
@@ -0,0 +1,61 @@
+<?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.
+-->
+
+<!-- android:paddingTop is used instead of android:layout_marginTop. It looks
+ android:layout_marginTop is ignored when used with <fragment></fragment>, which
+ only happens in Tablet UI since we rely on ViewPager in Phone UI.
+ Instead, android:layout_marginTop inside <fragment /> is effective. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/contact_browser_background"
+ android:orientation="vertical">
+
+ <!-- Shown only when an Account filter is set.
+ - paddingTop should be here to show "shade" effect correctly. -->
+ <include layout="@layout/account_filter_header"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+ <include layout="@layout/contact_list_card"/>
+ <view
+ android:id="@android:id/list"
+ class="com.android.contacts.common.list.PinnedHeaderListView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="?attr/contact_browser_list_padding_left"
+ android:layout_marginEnd="?attr/contact_browser_list_padding_right"
+ android:layout_marginLeft="?attr/contact_browser_list_padding_left"
+ android:layout_marginRight="?attr/contact_browser_list_padding_right"
+ android:paddingTop="?attr/list_item_padding_top"
+ android:clipToPadding="false"
+ android:fadingEdge="none"
+ android:fastScrollEnabled="true"/>
+ <ProgressBar
+ android:id="@+id/search_progress"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+ </FrameLayout>
+
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/default_account_checkbox.xml b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml
new file mode 100644
index 000000000..b7c0cf644
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/default_account_checkbox_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ android:orientation="vertical">
+ <CheckBox
+ android:id="@+id/default_account_checkbox_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dip"
+ android:layout_marginLeft="13dip"
+ android:paddingStart="15dip"
+ android:gravity="center"
+ android:text="@string/set_default_account"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/dialer_secondary_text_color"
+ />
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/dialog_call_subject.xml b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml
new file mode 100644
index 000000000..709bb50cb
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 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
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_subject_dialog"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <!-- The call subject dialog will be centered in the space above the subject list. -->
+ <LinearLayout
+ android:id="@+id/dialog_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:background="@drawable/dialog_background_material"
+ android:clickable="true"
+ android:elevation="16dp"
+ android:orientation="vertical"
+ android:theme="@android:style/Theme.Material.Light.Dialog">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_margin"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:orientation="horizontal">
+
+ <QuickContactBadge
+ android:id="@+id/contact_photo"
+ android:layout_width="@dimen/call_subject_dialog_contact_photo_size"
+ android:layout_height="@dimen/call_subject_dialog_contact_photo_size"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:layout_gravity="top"
+ android:focusable="true"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_between_line_margin"
+ android:layout_gravity="center_vertical"
+ android:singleLine="true"
+ android:textColor="@color/dialer_secondary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ <EditText
+ android:id="@+id/call_subject"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginTop="@dimen/call_subject_dialog_edit_spacing"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:layout_gravity="top"
+ android:background="@null"
+ android:gravity="top"
+ android:hint="@string/call_subject_hint"
+ android:textColor="@color/dialer_secondary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+ />
+
+ <TextView
+ android:id="@+id/character_limit"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_margin"
+ android:layout_marginBottom="@dimen/call_subject_dialog_margin"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:singleLine="true"
+ android:textColor="@color/dialer_secondary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@color/call_subject_divider"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_margin"
+ android:layout_marginBottom="@dimen/call_subject_dialog_margin"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin">
+
+ <ImageView
+ android:id="@+id/history_button"
+ android:layout_width="25dp"
+ android:layout_height="25dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_history_white_drawable_24dp"
+ android:tint="@color/call_subject_history_icon"/>
+
+ <TextView
+ android:id="@+id/send_and_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:singleLine="true"
+ android:text="@string/send_and_call_button"
+ android:textColor="@color/call_subject_button"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+
+ </RelativeLayout>
+ </LinearLayout>
+ </RelativeLayout>
+ <!-- The subject list is pinned to the bottom of the screen. -->
+ <ListView
+ android:id="@+id/subject_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/dialog_view"
+ android:background="@color/call_subject_history_background"
+ android:divider="@null"
+ android:elevation="8dp"/>
+
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/directory_header.xml b/java/com/android/contacts/common/res/layout/directory_header.xml
new file mode 100644
index 000000000..b8f5163c0
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/directory_header.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<!-- Layout used for list section separators. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/directory_header"
+ style="@style/DirectoryHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/directory_header_extra_top_padding"
+ android:paddingBottom="@dimen/directory_header_extra_bottom_padding"
+ android:paddingStart="?attr/list_item_padding_left"
+ android:paddingEnd="?attr/list_item_padding_right"
+ android:paddingLeft="?attr/list_item_padding_left"
+ android:paddingRight="?attr/list_item_padding_right"
+ android:background="?attr/contact_browser_background"
+ android:minHeight="@dimen/list_section_divider_min_height">
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/DirectoryHeaderStyle"/>
+ <TextView
+ android:id="@+id/display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/DirectoryHeaderStyle"/>
+ <TextView
+ android:id="@+id/count"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:paddingTop="1dip"
+ android:gravity="end"
+ android:singleLine="true"
+ android:textAppearance="@style/DirectoryHeaderStyle"/>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/list_separator.xml b/java/com/android/contacts/common/res/layout/list_separator.xml
new file mode 100644
index 000000000..ab60605c5
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/list_separator.xml
@@ -0,0 +1,27 @@
+<?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.
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/title"
+ android:textStyle="bold"
+ android:paddingTop="16dip"
+ android:paddingBottom="15dip"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
+ android:paddingLeft="16dip"
+ android:paddingRight="16dip"
+ android:textColor="@color/frequently_contacted_title_color"
+ android:textSize="@dimen/frequently_contacted_title_text_size"/>
diff --git a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
new file mode 100644
index 000000000..8a3bd6088
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_box_expanded"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ImageButton
+ android:id="@+id/search_back_button"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="@dimen/search_box_icon_size"
+ android:layout_margin="@dimen/search_box_navigation_icon_margin"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/action_menu_back_from_search"
+ android:src="@drawable/ic_back_arrow"
+ android:tint="@color/contactscommon_actionbar_background_color"/>
+
+ <EditText
+ android:id="@+id/search_view"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/search_box_icon_size"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/search_box_text_left_margin"
+ android:background="@null"
+ android:fontFamily="@string/search_font_family"
+ android:imeOptions="flagNoExtractUi"
+ android:inputType="textFilter"
+ android:singleLine="true"
+ android:textColor="@color/searchbox_text_color"
+ android:textColorHint="@color/searchbox_hint_text_color"
+ android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+ android:textSize="@dimen/search_text_size"/>
+
+ <ImageView
+ android:id="@+id/search_close_button"
+ android:layout_width="@dimen/search_box_close_icon_size"
+ android:layout_height="@dimen/search_box_close_icon_size"
+ android:padding="@dimen/search_box_close_icon_padding"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/description_clear_search"
+ android:src="@drawable/ic_close_dk"
+ android:tint="@color/searchbox_icon_tint"/>
+
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/select_account_list_item.xml b/java/com/android/contacts/common/res/layout/select_account_list_item.xml
new file mode 100644
index 000000000..fbd31e573
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/select_account_list_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 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.
+-->
+
+<!-- Layout of a single item in the InCallUI Account Chooser Dialog. -->
+<com.android.contacts.common.widget.ActivityTouchLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:scaleType="center"/>
+
+ <LinearLayout
+ android:id="@+id/text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="8dp"
+ android:gravity="start|center_vertical"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/dialer_primary_text_color"/>
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+</com.android.contacts.common.widget.ActivityTouchLinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/unread_count_tab.xml b/java/com/android/contacts/common/res/layout/unread_count_tab.xml
new file mode 100644
index 000000000..83481ee2d
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/unread_count_tab.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 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.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/view_pager_tab_background">
+ <!-- The tab icon -->
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"/>
+ <TextView
+ android:id="@+id/count"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/tab_unread_count_background_size"
+ android:layout_marginTop="@dimen/tab_unread_count_margin_top"
+ android:layout_marginStart="@dimen/tab_unread_count_margin_left"
+ android:layout_toEndOf="@id/icon"
+ android:paddingLeft="@dimen/tab_unread_count_text_padding"
+ android:paddingRight="@dimen/tab_unread_count_text_padding"
+ android:background="@drawable/unread_count_background"
+ android:fontFamily="sans-serif-medium"
+ android:gravity="center"
+ android:importantForAccessibility="no"
+ android:minWidth="@dimen/tab_unread_count_background_size"
+ android:textAlignment="center"
+ android:textColor="@color/tab_accent_color"
+ android:textSize="@dimen/tab_unread_count_text_size"/>
+</RelativeLayout>
diff --git a/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..64eff002f
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..b4ee8215a
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..6feeadfbe
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..01a3fde9d
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..328e067ee
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml
new file mode 100644
index 000000000..e05c6d658
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+
+ <!-- If true, phonetic name is included in the contact editor by default -->
+ <bool name="config_editor_include_phonetic_name">true</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml
new file mode 100644
index 000000000..8def55498
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">false</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">false</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values-land/integers.xml b/java/com/android/contacts/common/res/values-land/integers.xml
new file mode 100644
index 000000000..26bac6222
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-land/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">60</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml
new file mode 100644
index 000000000..be4eb0bb0
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">20</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw600dp/dimens.xml b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml
new file mode 100644
index 000000000..cf67a1e72
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (C) 2012 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
+ -->
+
+<resources>
+ <dimen name="detail_item_side_margin">0dip</dimen>
+
+ <dimen name="contact_browser_list_header_left_margin">@dimen/list_visible_scrollbar_padding
+ </dimen>
+ <dimen name="contact_browser_list_header_right_margin">24dip</dimen>
+ <dimen name="contact_browser_list_top_margin">16dip</dimen>
+
+ <!-- Right margin of the floating action button -->
+ <dimen name="floating_action_button_margin_right">32dp</dimen>
+ <!-- Bottom margin of the floating action button -->
+ <dimen name="floating_action_button_margin_bottom">32dp</dimen>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw600dp/integers.xml b/java/com/android/contacts/common/res/values-sw600dp/integers.xml
new file mode 100644
index 000000000..31aeee995
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw600dp/integers.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <!-- Yikes, there is less space on a tablet! This makes the search experience rather
+ poor. Another reason to get rid of the exist tablet layout. -->
+ <integer name="snippet_length_before_tokenize">15</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml
new file mode 100644
index 000000000..577716d24
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">4</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">30</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw720dp/integers.xml b/java/com/android/contacts/common/res/values-sw720dp/integers.xml
new file mode 100644
index 000000000..05e309351
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw720dp/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">2</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">20</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml
new file mode 100644
index 000000000..08023992b
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml
new file mode 100644
index 000000000..08023992b
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values/animation_constants.xml b/java/com/android/contacts/common/res/values/animation_constants.xml
new file mode 100644
index 000000000..9eec7d6c8
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/animation_constants.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 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
+ -->
+<resources>
+ <integer name="floating_action_button_animation_duration">250</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/attrs.xml b/java/com/android/contacts/common/res/values/attrs.xml
new file mode 100644
index 000000000..44d04f025
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/attrs.xml
@@ -0,0 +1,83 @@
+<!--
+ ~ Copyright (C) 2012 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.
+ -->
+
+<resources>
+ <declare-styleable name="Theme">
+ <attr name="android:textColorSecondary"/>
+ </declare-styleable>
+
+ <declare-styleable name="ContactsDataKind">
+ <!-- Mime-type handled by this mapping. -->
+ <attr name="android:mimeType"/>
+ <!-- Icon used to represent data of this kind. -->
+ <attr name="android:icon"/>
+ <!-- Column in data table that summarizes this data. -->
+ <attr name="android:summaryColumn"/>
+ <!-- Column in data table that contains details for this data. -->
+ <attr name="android:detailColumn"/>
+ <!-- Flag indicating that detail should be built from SocialProvider. -->
+ <attr name="android:detailSocialSummary"/>
+ <!-- Resource representing the term "All Contacts" (e.g. "All Friends" or
+ "All connections"). Optional (Default is "All Contacts"). -->
+ <attr name="android:allContactsName"/>
+ </declare-styleable>
+
+ <declare-styleable name="ContactListItemView">
+ <attr format="dimension" name="list_item_height"/>
+ <attr format="dimension" name="list_section_header_height"/>
+ <attr format="reference" name="activated_background"/>
+ <attr format="reference" name="section_header_background"/>
+ <attr format="dimension" name="list_item_padding_top"/>
+ <attr format="dimension" name="list_item_padding_right"/>
+ <attr format="dimension" name="list_item_padding_bottom"/>
+ <attr format="dimension" name="list_item_padding_left"/>
+ <attr format="dimension" name="list_item_gap_between_image_and_text"/>
+ <attr format="dimension" name="list_item_gap_between_label_and_data"/>
+ <attr format="dimension" name="list_item_presence_icon_margin"/>
+ <attr format="dimension" name="list_item_presence_icon_size"/>
+ <attr format="dimension" name="list_item_photo_size"/>
+ <attr format="dimension" name="list_item_profile_photo_size"/>
+ <attr format="color" name="list_item_prefix_highlight_color"/>
+ <attr format="color" name="list_item_background_color"/>
+ <attr format="dimension" name="list_item_header_text_indent"/>
+ <attr format="color" name="list_item_header_text_color"/>
+ <attr format="dimension" name="list_item_header_text_size"/>
+ <attr format="dimension" name="list_item_header_height"/>
+ <attr format="color" name="list_item_name_text_color"/>
+ <attr format="dimension" name="list_item_name_text_size"/>
+ <attr format="dimension" name="list_item_text_indent"/>
+ <attr format="dimension" name="list_item_text_offset_top"/>
+ <attr format="integer" name="list_item_data_width_weight"/>
+ <attr format="integer" name="list_item_label_width_weight"/>
+ <attr format="dimension" name="list_item_video_call_icon_size"/>
+ <attr format="dimension" name="list_item_video_call_icon_margin"/>
+ </declare-styleable>
+
+ <declare-styleable name="ContactBrowser">
+ <attr format="dimension" name="contact_browser_list_padding_left"/>
+ <attr format="dimension" name="contact_browser_list_padding_right"/>
+ <attr format="reference" name="contact_browser_background"/>
+ </declare-styleable>
+
+ <declare-styleable name="ProportionalLayout">
+ <attr format="string" name="direction"/>
+ <attr format="float" name="ratio"/>
+ </declare-styleable>
+
+ <declare-styleable name="Favorites">
+ <attr format="dimension" name="favorites_padding_bottom"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/colors.xml b/java/com/android/contacts/common/res/values/colors.xml
new file mode 100644
index 000000000..7524eff58
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/colors.xml
@@ -0,0 +1,158 @@
+<!--
+ ~ Copyright (C) 2012 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
+ -->
+
+<resources>
+ <!-- Background color corresponding to the holo list 9-patch. -->
+ <color name="holo_list_background_color">#eeeeee</color>
+
+ <color name="focus_color">#44ff0000</color>
+
+ <!-- Color of ripples used for views with dark backgrounds -->
+ <color name="dialer_ripple_material_dark">#a0ffffff</color>
+
+ <!-- Color of ripples used for views with light backgrounds -->
+ <color name="dialer_ripple_material_light">#30000000</color>
+
+ <!-- Divider color for header separator -->
+ <color name="primary_text_color">#363636</color>
+
+ <color name="secondary_text_color">@color/dialer_secondary_text_color</color>
+
+ <!-- Text color for section header. -->
+ <color name="section_header_text_color">#2A56C6</color>
+
+ <!-- Divider color for header separator -->
+ <color name="main_header_separator_color">#AAAAAA</color>
+
+ <!-- Divider color for header separator -->
+ <color name="secondary_header_separator_color">#D0D0D0</color>
+
+ <!-- Color of the theme of the People app -->
+ <color name="people_app_theme_color">#363636</color>
+
+ <!-- Color of image view placeholder. -->
+ <color name="image_placeholder">#DDDDDD</color>
+
+ <!-- Color of the semi-transparent shadow box on contact tiles -->
+ <color name="contact_tile_shadow_box_color">#7F000000</color>
+
+ <!-- Color of the status message for starred contacts in the People app -->
+ <color name="people_contact_tile_status_color">#CCCCCC</color>
+
+ <color name="shortcut_overlay_text_background">#7f000000</color>
+
+ <color name="textColorIconOverlay">#fff</color>
+ <color name="textColorIconOverlayShadow">#000</color>
+
+
+ <array name="letter_tile_colors">
+ <item>#DB4437</item>
+ <item>#E91E63</item>
+ <item>#9C27B0</item>
+ <item>#673AB7</item>
+ <item>#3F51B5</item>
+ <item>#4285F4</item>
+ <item>#039BE5</item>
+ <item>#0097A7</item>
+ <item>#009688</item>
+ <item>#0F9D58</item>
+ <item>#689F38</item>
+ <item>#EF6C00</item>
+ <item>#FF5722</item>
+ <item>#757575</item>
+ </array>
+
+ <!-- Darker versions of letter_tile_colors, two shades darker. These colors are used
+ for settings secondary activity colors. -->
+ <array name="letter_tile_colors_dark">
+ <item>#C53929</item>
+ <item>#C2185B</item>
+ <item>#7B1FA2</item>
+ <item>#512DA8</item>
+ <item>#303F9F</item>
+ <item>#3367D6</item>
+ <item>#0277BD</item>
+ <item>#006064</item>
+ <item>#00796B</item>
+ <item>#0B8043</item>
+ <item>#33691E</item>
+ <item>#E65100</item>
+ <item>#E64A19</item>
+ <item>#424242</item>
+ </array>
+
+ <!-- The default color used for tinting photos when no color can be extracted via Palette,
+ this is Blue Grey 500 -->
+ <color name="quickcontact_default_photo_tint_color">#607D8B</color>
+ <!-- The default secondary color when no color can be extracted via Palette,
+ this is Blue Grey 700 -->
+ <color name="quickcontact_default_photo_tint_color_dark">#455A64</color>
+
+
+ <color name="letter_tile_default_color">#cccccc</color>
+
+ <color name="letter_tile_font_color">#ffffff</color>
+
+ <color name="contactscommon_actionbar_background_color">@color/dialer_theme_color</color>
+ <!-- Color for icons in the actionbar -->
+ <color name="actionbar_icon_color">#ffffff</color>
+ <!-- Darker version of the actionbar color. Used for the status bar and navigation bar colors. -->
+ <color name="actionbar_background_color_dark">#008aa1</color>
+
+ <color name="tab_ripple_color">#ffffff</color>
+ <color name="tab_accent_color">@color/tab_ripple_color</color>
+ <color name="tab_selected_underline_color">#f50057</color>
+ <color name="tab_unread_count_background_color">#1C3AA9</color>
+
+ <!-- Color of the title to the Frequently Contacted section -->
+ <color name="frequently_contacted_title_color">@color/contactscommon_actionbar_background_color
+ </color>
+
+ <!-- Color of action bar text. Ensure this stays in sync with packages/Telephony
+ phone_settings_actionbar_text_color-->
+ <color name="actionbar_text_color">#ffffff</color>
+ <color name="actionbar_unselected_text_color">#a6ffffff</color>
+
+ <!-- Text color of the search box text as entered by user -->
+ <color name="searchbox_text_color">#000000</color>
+ <!-- Background color of the search box -->
+ <color name="searchbox_background_color">#ffffff</color>
+
+ <color name="searchbox_hint_text_color">#737373</color>
+ <color name="searchbox_icon_tint">@color/searchbox_hint_text_color</color>
+
+ <color name="search_shortcut_icon_color">@color/dialtacts_theme_color</color>
+
+ <!-- Color of the background of the contact detail and editor pages -->
+ <color name="background_primary">#f9f9f9</color>
+ <color name="contact_all_list_background_color">#FFFFFF</color>
+
+ <!-- Text color used for character counter when the max limit has been exceeded -->
+ <color name="call_subject_limit_exceeded">#d1041c</color>
+
+ <!-- Tint color for the call subject history icon. -->
+ <color name="call_subject_history_icon">#000000</color>
+
+ <!-- Divider line on the call subject dialog. -->
+ <color name="call_subject_divider">#d8d8d8</color>
+
+ <!-- Text color for the SEND & CALL button on the call subject dialog. -->
+ <color name="call_subject_button">#00c853</color>
+
+ <!-- Background color for the call subject history view. -->
+ <color name="call_subject_history_background">#ffffff</color>
+ <color name="search_video_call_icon_tint">@color/searchbox_hint_text_color</color>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/dimens.xml b/java/com/android/contacts/common/res/values/dimens.xml
new file mode 100644
index 000000000..642eb31a4
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/dimens.xml
@@ -0,0 +1,161 @@
+<!--
+ ~ Copyright (C) 2012 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
+ -->
+
+<resources>
+
+ <!-- Padding between the action bar's bottom edge and the first header
+ in contacts/group lists. -->
+ <dimen name="list_header_extra_top_padding">0dip</dimen>
+
+ <!-- Minimum height used with @drawable/list_section_divider_holo_custom.
+ Right now the drawable has implicit 32dip minimal height, which is confusing.
+ This value is for making the hidden configuration explicit in xml. -->
+ <dimen name="list_section_divider_min_height">32dip</dimen>
+
+ <dimen name="directory_header_extra_top_padding">18dp</dimen>
+ <dimen name="directory_header_extra_bottom_padding">8dp</dimen>
+
+ <!-- Horizontal padding in between contact tiles -->
+ <dimen name="contact_tile_divider_padding">23dip</dimen>
+ <!-- Horizontal whitespace (both padding and margin) before the first tile and after the last tile -->
+ <dimen name="contact_tile_start_end_whitespace">16dip</dimen>
+
+ <!-- Left and right padding for a contact detail item -->
+ <dimen name="detail_item_side_margin">16dip</dimen>
+
+ <!-- ContactTile Layouts -->
+ <!--
+ Use sp instead of dip so that the shadowbox heights can all scale uniformly
+ when the font size is scaled for accessibility purposes
+ -->
+ <dimen name="contact_tile_shadowbox_height">48sp</dimen>
+
+ <!-- Top padding of the ListView in the contact tile list -->
+ <dimen name="contact_tile_list_padding_top">0dip</dimen>
+
+ <!-- Padding to be used between a visible scrollbar and the contact list -->
+ <dimen name="list_visible_scrollbar_padding">32dip</dimen>
+
+ <dimen name="contact_browser_list_header_left_margin">16dip</dimen>
+ <dimen name="contact_browser_list_header_right_margin">@dimen/list_visible_scrollbar_padding
+ </dimen>
+ <dimen name="contact_browser_list_item_text_indent">8dip</dimen>
+ <!-- Width of a contact list item section header. -->
+ <dimen name="contact_list_section_header_width">56dp</dimen>
+
+ <!-- Size of the shortcut icon. 0dip means: use the system default -->
+ <dimen name="shortcut_icon_size">0dip</dimen>
+
+ <!-- Text size of shortcut icon overlay text -->
+ <dimen name="shortcut_overlay_text_size">12dp</dimen>
+
+ <!-- Extra vertical padding for darkened background behind shortcut icon overlay text -->
+ <dimen name="shortcut_overlay_text_background_padding">1dp</dimen>
+
+ <!-- Width of height of an icon from a third-party app in the networks section of the contact card. -->
+ <dimen name="detail_network_icon_size">32dip</dimen>
+
+ <!-- Empty message margins -->
+ <dimen name="empty_message_top_margin">48dip</dimen>
+
+ <!-- contact browser list margins -->
+ <dimen name="contact_browser_list_item_text_size">16sp</dimen>
+ <dimen name="contact_browser_list_item_photo_size">40dp</dimen>
+ <dimen name="contact_browser_list_item_gap_between_image_and_text">15dp</dimen>
+ <dimen name="contact_browser_list_top_margin">12dp</dimen>
+
+ <!-- Dimensions for "No contacts" string in PhoneFavoriteFragment for the All contacts
+ with phone numbers section
+ -->
+ <dimen name="contact_phone_list_empty_description_size">20sp</dimen>
+ <dimen name="contact_phone_list_empty_description_padding">10dip</dimen>
+
+ <!-- Dimensions for contact letter tiles -->
+ <dimen name="tile_letter_font_size">40dp</dimen>
+ <dimen name="tile_letter_font_size_small">20dp</dimen>
+ <dimen name="tile_divider_width">1dp</dimen>
+ <item name="letter_to_tile_ratio" type="dimen">67%</item>
+
+ <!-- Height of the floating action button -->
+ <dimen name="floating_action_button_height">56dp</dimen>
+ <!-- Width of the floating action button -->
+ <dimen name="floating_action_button_width">56dp</dimen>
+ <!-- Z translation of the floating action button -->
+ <dimen name="floating_action_button_translation_z">8dp</dimen>
+ <!-- Padding to be applied to the bottom of lists to make space for the floating action
+ button -->
+ <dimen name="floating_action_button_list_bottom_padding">88dp</dimen>
+ <!-- Right margin of the floating action button -->
+ <dimen name="floating_action_button_margin_right">16dp</dimen>
+ <!-- Bottom margin of the floating action button -->
+ <dimen name="floating_action_button_margin_bottom">16dp</dimen>
+
+ <!-- Height of the selection indicator of a tab. -->
+ <dimen name="tab_selected_underline_height">2dp</dimen>
+ <!-- Size of text in tabs. -->
+ <dimen name="tab_text_size">14sp</dimen>
+ <dimen name="tab_elevation">2dp</dimen>
+ <dimen name="tab_unread_count_background_size">16dp</dimen>
+ <dimen name="tab_unread_count_background_radius">2dp</dimen>
+ <dimen name="tab_unread_count_margin_left">0dp</dimen>
+ <dimen name="tab_unread_count_margin_top">2dp</dimen>
+ <dimen name="tab_unread_count_text_size">12sp</dimen>
+ <dimen name="tab_unread_count_text_padding">2dp</dimen>
+
+ <!-- Padding around the icon in the search box. -->
+ <dimen name="search_box_icon_margin">4dp</dimen>
+ <!-- Size of the icon (voice search, back arrow) in the search box. -->
+ <dimen name="search_box_icon_size">48dp</dimen>
+ <!-- Size of the close icon.-->
+ <dimen name="search_box_close_icon_size">56dp</dimen>
+ <!-- Padding around the close button. It's visible size without padding is 24dp. -->
+ <dimen name="search_box_close_icon_padding">16dp</dimen>
+ <!-- Padding around back arrow icon in the search box -->
+ <dimen name="search_box_navigation_icon_margin">14dp</dimen>
+ <!-- Left margin of the text field in the search box. -->
+ <dimen name="search_box_text_left_margin">15dp</dimen>
+ <!-- Search box text size -->
+ <dimen name="search_text_size">20sp</dimen>
+
+ <!-- Top margin for the Frequently Contacted section title -->
+ <dimen name="frequently_contacted_title_top_margin_when_first_row">16dp</dimen>
+ <!-- Top margin for the Frequently Contacted section title, when the title is the first
+ item in the list -->
+ <dimen name="frequently_contacted_title_top_margin">57dp</dimen>
+
+ <dimen name="frequently_contacted_title_text_size">24sp</dimen>
+
+ <!-- Size of icon for contacts number shortcuts -->
+ <dimen name="search_shortcut_radius">40dp</dimen>
+
+ <dimen name="contact_list_card_elevation">2dp</dimen>
+
+ <!-- Padding used around the periphery of the call subject dialog, as well as in between the
+ items. -->
+ <dimen name="call_subject_dialog_margin">20dp</dimen>
+ <!-- Padding used between lines of text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_between_line_margin">8dp</dimen>
+ <!-- Size of the contact photo in the call subject dialog. -->
+ <dimen name="call_subject_dialog_contact_photo_size">50dp</dimen>
+ <!-- Margin above the edit text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_edit_spacing">60dp</dimen>
+ <!-- Size of primary text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_primary_text_size">16sp</dimen>
+ <!-- Size of secondary text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_secondary_text_size">14sp</dimen>
+ <!-- Row padding for call subject history items. -->
+ <dimen name="call_subject_history_item_padding">15dp</dimen>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/donottranslate_config.xml b/java/com/android/contacts/common/res/values/donottranslate_config.xml
new file mode 100644
index 000000000..324437928
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/donottranslate_config.xml
@@ -0,0 +1,95 @@
+<!--
+ ~ Copyright (C) 2012 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
+ -->
+
+<resources>
+ <!-- Flag indicating whether Contacts app is allowed to import contacts -->
+ <bool name="config_allow_import_from_vcf_file">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">true</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">true</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a default account -->
+ <bool name="config_default_account_user_changeable">true</bool>
+
+ <!-- Contacts preferences key for contact editor default account -->
+ <string name="contact_editor_default_account_key">ContactEditorUtils_default_account</string>
+
+ <!-- Contacts preferences key for contact editor anything saved -->
+ <string name="contact_editor_anything_saved_key">ContactEditorUtils_anything_saved</string>
+
+ <!-- The type of VCard for export. If you want to let the app emit vCard which is
+ specific to some vendor (like DoCoMo), specify this type (e.g. "docomo") -->
+ <string name="config_export_vcard_type" translatable="false">default</string>
+
+ <!-- The type of vcard for improt. If the vcard importer cannot guess the exact type
+ of a vCard type, the improter uses this type. -->
+ <string name="config_import_vcard_type" translatable="false">default</string>
+
+ <!-- Prefix of exported VCard file -->
+ <string name="config_export_file_prefix" translatable="false"></string>
+
+ <!-- Suffix of exported VCard file. Attached before an extension -->
+ <string name="config_export_file_suffix" translatable="false"></string>
+
+ <!-- Extension for exported VCard files -->
+ <string name="config_export_file_extension">vcf</string>
+
+ <!-- The filename that is suggested that users use when exporting vCards. Should include the .vcf extension. -->
+ <string name="exporting_vcard_filename" translatable="false">contacts.vcf</string>
+
+ <!-- Minimum number of exported VCard file index -->
+ <integer name="config_export_file_min_index">1</integer>
+
+ <!-- Maximum number of exported VCard file index -->
+ <integer name="config_export_file_max_index">99999</integer>
+
+ <!-- The list (separated by ',') of extensions should be checked in addition to
+ config_export_extension. e.g. If "aaa" is added to here and 00001.vcf and 00002.aaa
+ exist in a target directory, 00003.vcf becomes a next file name candidate.
+ Without this configuration, 00002.vcf becomes the candidate.-->
+ <string name="config_export_extensions_to_consider" translatable="false"></string>
+
+ <!-- If true, enable the "import contacts from SIM" feature if the device
+ has an appropriate SIM or ICC card.
+ Setting this flag to false in a resource overlay allows you to
+ entirely disable SIM import on a per-product basis. -->
+ <bool name="config_allow_sim_import">true</bool>
+
+ <!-- Flag indicating whether Contacts app is allowed to export contacts -->
+ <bool name="config_allow_export">true</bool>
+
+ <!-- Flag indicating whether Contacts app is allowed to share contacts with devices outside -->
+ <bool name="config_allow_share_contacts">true</bool>
+
+ <string name="pref_build_version_key">pref_build_version</string>
+ <string name="pref_open_source_licenses_key">pref_open_source_licenses</string>
+ <string name="pref_privacy_policy_key">pref_privacy_policy</string>
+ <string name="pref_terms_of_service_key">pref_terms_of_service</string>
+
+ <string name="star_sign">★</string>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/ids.xml b/java/com/android/contacts/common/res/values/ids.xml
new file mode 100644
index 000000000..871f5a636
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/ids.xml
@@ -0,0 +1,30 @@
+<!--
+ Copyright (C) 2012 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.
+ -->
+
+<resources>
+ <!-- For Debug Purpose -->
+ <item name="cliv_name_textview" type="id"/>
+ <item name="cliv_label_textview" type="id"/>
+ <item name="cliv_data_view" type="id"/>
+
+ <!-- For tag ids used by ContactPhotoManager to tag views with contact details -->
+ <item name="tag_display_name" type="id"/>
+ <item name="tag_identifier" type="id"/>
+ <item name="tag_contact_type" type="id"/>
+
+ <item name="contact_tile_image" type="id"/>
+ <item name="contact_tile_name" type="id"/>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/integers.xml b/java/com/android/contacts/common/res/values/integers.xml
new file mode 100644
index 000000000..d38ad1da0
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/integers.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 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.
+ -->
+
+<resources>
+
+ <!-- Determines the number of columns in a ContactTileRow in the favorites tab -->
+ <integer name="contact_tile_column_count_in_favorites">2</integer>
+ <integer name="contact_tile_column_count_in_favorites_new">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">30</integer>
+
+ <!-- Layout weight of space elements in contact list view.
+ Default to 0 to indicate no padding-->
+ <integer name="contact_list_space_layout_weight">0</integer>
+ <!-- Layout weight of card in contact list view.
+ Default to 0 to indicate no padding -->
+ <integer name="contact_list_card_layout_weight">0</integer>
+
+ <!-- Duration of the animations on the call subject dialog. -->
+ <integer name="call_subject_animation_duration">250</integer>
+
+ <!-- A big number to make sure "About contacts" always showing at the bottom of Settings.-->
+ <integer name="about_contacts_order_number">100</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/strings.xml b/java/com/android/contacts/common/res/values/strings.xml
new file mode 100644
index 000000000..15e1f15d9
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/strings.xml
@@ -0,0 +1,798 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 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
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
+ <string name="toast_text_copied">Text copied</string>
+ <!-- Option displayed in context menu to copy long pressed item to clipboard [CHAR LIMIT=64] -->
+ <string name="copy_text">Copy to clipboard</string>
+
+ <!-- Action string for calling a custom phone number -->
+ <string name="call_custom">Call
+ <xliff:g id="custom">%s</xliff:g>
+ </string>
+ <!-- Action string for calling a home phone number -->
+ <string name="call_home">Call home</string>
+ <!-- Action string for calling a mobile phone number -->
+ <string name="call_mobile">Call mobile</string>
+ <!-- Action string for calling a work phone number -->
+ <string name="call_work">Call work</string>
+ <!-- Action string for calling a work fax phone number -->
+ <string name="call_fax_work">Call work fax</string>
+ <!-- Action string for calling a home fax phone number -->
+ <string name="call_fax_home">Call home fax</string>
+ <!-- Action string for calling a pager phone number -->
+ <string name="call_pager">Call pager</string>
+ <!-- Action string for calling an other phone number -->
+ <string name="call_other">Call</string>
+ <!-- Action string for calling a callback number -->
+ <string name="call_callback">Call callback</string>
+ <!-- Action string for calling a car phone number -->
+ <string name="call_car">Call car</string>
+ <!-- Action string for calling a company main phone number -->
+ <string name="call_company_main">Call company main</string>
+ <!-- Action string for calling a ISDN phone number -->
+ <string name="call_isdn">Call ISDN</string>
+ <!-- Action string for calling a main phone number -->
+ <string name="call_main">Call main</string>
+ <!-- Action string for calling an other fax phone number -->
+ <string name="call_other_fax">Call fax</string>
+ <!-- Action string for calling a radio phone number -->
+ <string name="call_radio">Call radio</string>
+ <!-- Action string for calling a Telex phone number -->
+ <string name="call_telex">Call telex</string>
+ <!-- Action string for calling a TTY/TDD phone number -->
+ <string name="call_tty_tdd">Call TTY/TDD</string>
+ <!-- Action string for calling a work mobile phone number -->
+ <string name="call_work_mobile">Call work mobile</string>
+ <!-- Action string for calling a work pager phone number -->
+ <string name="call_work_pager">Call work pager</string>
+ <!-- Action string for calling an assistant phone number -->
+ <string name="call_assistant">Call
+ <xliff:g id="assistant">%s</xliff:g>
+ </string>
+ <!-- Action string for calling a MMS phone number -->
+ <string name="call_mms">Call MMS</string>
+ <!-- Action string for calling a contact by shortcut -->
+ <string name="call_by_shortcut"><xliff:g id="contact_name">%s</xliff:g> (Call)</string>
+
+ <!-- Action string for sending an SMS to a custom phone number -->
+ <string name="sms_custom">Text
+ <xliff:g id="custom">%s</xliff:g>
+ </string>
+ <!-- Action string for sending an SMS to a home phone number -->
+ <string name="sms_home">Text home</string>
+ <!-- Action string for sending an SMS to a mobile phone number -->
+ <string name="sms_mobile">Text mobile</string>
+ <!-- Action string for sending an SMS to a work phone number -->
+ <string name="sms_work">Text work</string>
+ <!-- Action string for sending an SMS to a work fax phone number -->
+ <string name="sms_fax_work">Text work fax</string>
+ <!-- Action string for sending an SMS to a home fax phone number -->
+ <string name="sms_fax_home">Text home fax</string>
+ <!-- Action string for sending an SMS to a pager phone number -->
+ <string name="sms_pager">Text pager</string>
+ <!-- Action string for sending an SMS to an other phone number -->
+ <string name="sms_other">Text</string>
+ <!-- Action string for sending an SMS to a callback number -->
+ <string name="sms_callback">Text callback</string>
+ <!-- Action string for sending an SMS to a car phone number -->
+ <string name="sms_car">Text car</string>
+ <!-- Action string for sending an SMS to a company main phone number -->
+ <string name="sms_company_main">Text company main</string>
+ <!-- Action string for sending an SMS to a ISDN phone number -->
+ <string name="sms_isdn">Text ISDN</string>
+ <!-- Action string for sending an SMS to a main phone number -->
+ <string name="sms_main">Text main</string>
+ <!-- Action string for sending an SMS to an other fax phone number -->
+ <string name="sms_other_fax">Text fax</string>
+ <!-- Action string for sending an SMS to a radio phone number -->
+ <string name="sms_radio">Text radio</string>
+ <!-- Action string for sending an SMS to a Telex phone number -->
+ <string name="sms_telex">Text telex</string>
+ <!-- Action string for sending an SMS to a TTY/TDD phone number -->
+ <string name="sms_tty_tdd">Text TTY/TDD</string>
+ <!-- Action string for sending an SMS to a work mobile phone number -->
+ <string name="sms_work_mobile">Text work mobile</string>
+ <!-- Action string for sending an SMS to a work pager phone number -->
+ <string name="sms_work_pager">Text work pager</string>
+ <!-- Action string for sending an SMS to an assistant phone number -->
+ <string name="sms_assistant">Text
+ <xliff:g id="assistant">%s</xliff:g>
+ </string>
+ <!-- Action string for sending an SMS to a MMS phone number -->
+ <string name="sms_mms">Text MMS</string>
+ <!-- Action string for sending an SMS to a contact by shortcut -->
+ <string name="sms_by_shortcut"><xliff:g id="contact_name">%s</xliff:g> (Message)</string>
+
+ <!-- Title of the confirmation dialog for clearing frequents. [CHAR LIMIT=37] -->
+ <string name="clearFrequentsConfirmation_title">Clear frequently contacted?</string>
+
+ <!-- Confirmation dialog for clearing frequents. [CHAR LIMIT=NONE] -->
+ <string name="clearFrequentsConfirmation">You\'ll clear the frequently contacted list in the
+ Contacts and Phone apps, and force email apps to learn your addressing preferences from
+ scratch.
+ </string>
+
+ <!-- Title of the "Clearing frequently contacted" progress-dialog [CHAR LIMIT=35] -->
+ <string name="clearFrequentsProgress_title">Clearing frequently contacted\u2026</string>
+
+ <!-- Used to display as default status when the contact is available for chat [CHAR LIMIT=19] -->
+ <string name="status_available">Available</string>
+
+ <!-- Used to display as default status when the contact is away or idle for chat [CHAR LIMIT=19] -->
+ <string name="status_away">Away</string>
+
+ <!-- Used to display as default status when the contact is busy or Do not disturb for chat [CHAR LIMIT=19] -->
+ <string name="status_busy">Busy</string>
+
+ <!-- Directory partition name (also exists in contacts) -->
+ <string name="contactsList">Contacts</string>
+
+ <!-- The name of the invisible local contact directory -->
+ <string name="local_invisible_directory">Other</string>
+
+ <!-- The label in section header in the contact list for a contact directory [CHAR LIMIT=128] -->
+ <string name="directory_search_label">Directory</string>
+
+ <!-- The label in section header in the contact list for a work contact directory [CHAR LIMIT=128] -->
+ <string name="directory_search_label_work">Work directory</string>
+
+ <!-- The label in section header in the contact list for a local contacts [CHAR LIMIT=128] -->
+ <string name="local_search_label">All contacts</string>
+
+ <!-- String describing the text on the header of the profile contact in the contacts list
+ This may be programatically capitalized. [CHAR LIMIT=20] -->
+ <string msgid="9154761216179882405" name="user_profile_contacts_list_header">Me</string>
+
+ <!-- Title shown in the search result activity of contacts app while searching. [CHAR LIMIT=20]
+ (also in contacts) -->
+ <string name="search_results_searching">Searching\u2026</string>
+
+ <!-- Displayed at the top of search results indicating that more contacts were found than shown [CHAR LIMIT=64] -->
+ <string name="foundTooManyContacts">More than <xliff:g id="count">%d</xliff:g> found.</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts found when "Only contacts with phones" not selected. [CHAR LIMIT=30]
+ (also in contacts) -->
+ <string name="listFoundAllContactsZero">No contacts</string>
+
+ <!-- Displayed at the top of the contacts showing the total number of contacts found when typing search query -->
+ <plurals name="searchFoundContacts">
+ <item quantity="one">1 found</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> found</item>
+ </plurals>
+
+ <!-- String describing the text for photo of a contact in a contacts list.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_quick_contact_for">Quick contact for <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- Shown as the display name for a person when the name is missing or unknown. [CHAR LIMIT=18]-->
+ <string name="missing_name">(No name)</string>
+
+ <!-- The text displayed on the divider for the Favorites tab in Phone app indicating that items below it are frequently called as opposed to starred contacts [CHAR LIMIT = 39] -->
+ <string name="favoritesFrequentCalled">Frequently called</string>
+
+ <!-- The text displayed on the divider for the Favorites tab in People app indicating that items below it are frequently contacted [CHAR LIMIT = 39] -->
+ <string name="favoritesFrequentContacted">Frequently contacted</string>
+
+ <!-- String describing a contact picture that introduces users to the contact detail screen.
+
+ Used by AccessibilityService to announce the purpose of the button.
+
+ [CHAR LIMIT=NONE]
+ -->
+ <string msgid="2795575601596468581" name="description_view_contact_detail">View contact</string>
+
+ <!-- Contact list filter selection indicating that the list shows all contacts with phone numbers [CHAR LIMIT=64] -->
+ <string name="list_filter_phones">All contacts with phone numbers</string>
+
+ <!-- Contact list filter selection indicating that the list shows all work contacts with phone numbers [CHAR LIMIT=64] -->
+ <string name="list_filter_phones_work">Work profile contacts</string>
+
+ <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=25] -->
+ <string name="view_updates_from_group">View updates</string>
+
+ <!-- Title for data source when creating or editing a contact that doesn't
+ belong to a specific account. This contact will only exist on the device
+ and will not be synced. -->
+ <string name="account_phone">Device-only, unsynced</string>
+
+ <!-- Header that expands to list all name types when editing a structured name of a contact
+ [CHAR LIMIT=20] -->
+ <string name="nameLabelsGroup">Name</string>
+
+ <!-- Header that expands to list all nickname types when editing a nickname of a contact
+ [CHAR LIMIT=20] -->
+ <string name="nicknameLabelsGroup">Nickname</string>
+
+ <!-- Field title for the full name of a contact [CHAR LIMIT=64]-->
+ <string name="full_name">Name</string>
+ <!-- Field title for the given name of a contact -->
+ <string name="name_given">First name</string>
+ <!-- Field title for the family name of a contact -->
+ <string name="name_family">Last name</string>
+ <!-- Field title for the prefix name of a contact -->
+ <string name="name_prefix">Name prefix</string>
+ <!-- Field title for the middle name of a contact -->
+ <string name="name_middle">Middle name</string>
+ <!-- Field title for the suffix name of a contact -->
+ <string name="name_suffix">Name suffix</string>
+
+ <!-- Field title for the phonetic name of a contact [CHAR LIMIT=64]-->
+ <string name="name_phonetic">Phonetic name</string>
+
+ <!-- Field title for the phonetic given name of a contact -->
+ <string name="name_phonetic_given">Phonetic first name</string>
+ <!-- Field title for the phonetic middle name of a contact -->
+ <string name="name_phonetic_middle">Phonetic middle name</string>
+ <!-- Field title for the phonetic family name of a contact -->
+ <string name="name_phonetic_family">Phonetic last name</string>
+
+ <!-- Header that expands to list all of the types of phone numbers when editing or creating a
+ phone number for a contact [CHAR LIMIT=20] -->
+ <string name="phoneLabelsGroup">Phone</string>
+
+ <!-- Header that expands to list all of the types of email addresses when editing or creating
+ an email address for a contact [CHAR LIMIT=20] -->
+ <string name="emailLabelsGroup">Email</string>
+
+ <!-- Header that expands to list all of the types of postal addresses when editing or creating
+ an postal address for a contact [CHAR LIMIT=20] -->
+ <string name="postalLabelsGroup">Address</string>
+
+ <!-- Header that expands to list all of the types of IM account when editing or creating an IM
+ account for a contact [CHAR LIMIT=20] -->
+ <string name="imLabelsGroup">IM</string>
+
+ <!-- Header that expands to list all organization types when editing an organization of a
+ contact [CHAR LIMIT=20] -->
+ <string name="organizationLabelsGroup">Organization</string>
+
+ <!-- Header for the list of all relationships for a contact [CHAR LIMIT=20] -->
+ <string name="relationLabelsGroup">Relationship</string>
+
+ <!-- Header that expands to list all event types when editing an event of a contact
+ [CHAR LIMIT=20] -->
+ <string name="eventLabelsGroup">Special date</string>
+
+ <!-- Generic action string for text messaging a contact. Used by AccessibilityService to
+ announce the purpose of the view. [CHAR LIMIT=NONE] -->
+ <string name="sms">Text message</string>
+
+ <!-- Field title for the full postal address of a contact [CHAR LIMIT=64]-->
+ <string name="postal_address">Address</string>
+
+ <!-- Hint text for the organization name when editing -->
+ <string name="ghostData_company">Company</string>
+
+ <!-- Hint text for the organization title when editing -->
+ <string name="ghostData_title">Title</string>
+
+ <!-- The label describing the Notes field of a contact. This field allows free form text entry
+ about a contact -->
+ <string name="label_notes">Notes</string>
+
+ <!-- The label describing the SIP address field of a contact. [CHAR LIMIT=20] -->
+ <string name="label_sip_address">SIP</string>
+
+ <!-- Header that expands to list all website types when editing a website of a contact
+ [CHAR LIMIT=20] -->
+ <string name="websiteLabelsGroup">Website</string>
+
+ <!-- Header for the list of all groups for a contact [CHAR LIMIT=20] -->
+ <string name="groupsLabel">Groups</string>
+
+ <!-- Action string for sending an email to a home email address -->
+ <string name="email_home">Email home</string>
+ <!-- Action string for sending an email to a mobile email address -->
+ <string name="email_mobile">Email mobile</string>
+ <!-- Action string for sending an email to a work email address -->
+ <string name="email_work">Email work</string>
+ <!-- Action string for sending an email to an other email address -->
+ <string name="email_other">Email</string>
+ <!-- Action string for sending an email to a custom email address -->
+ <string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
+
+ <!-- Generic action string for sending an email -->
+ <string name="email">Email</string>
+
+ <!-- Field title for the street of a structured postal address of a contact -->
+ <string name="postal_street">Street</string>
+ <!-- Field title for the PO box of a structured postal address of a contact -->
+ <string name="postal_pobox">PO box</string>
+ <!-- Field title for the neighborhood of a structured postal address of a contact -->
+ <string name="postal_neighborhood">Neighborhood</string>
+ <!-- Field title for the city of a structured postal address of a contact -->
+ <string name="postal_city">City</string>
+ <!-- Field title for the region, or state, of a structured postal address of a contact -->
+ <string name="postal_region">State</string>
+ <!-- Field title for the postal code of a structured postal address of a contact -->
+ <string name="postal_postcode">ZIP code</string>
+ <!-- Field title for the country of a structured postal address of a contact -->
+ <string name="postal_country">Country</string>
+
+ <!-- Action string for viewing a home postal address -->
+ <string name="map_home">View home address</string>
+ <!-- Action string for viewing a work postal address -->
+ <string name="map_work">View work address</string>
+ <!-- Action string for viewing an other postal address -->
+ <string name="map_other">View address</string>
+ <!-- Action string for viewing a custom postal address -->
+ <string name="map_custom">View <xliff:g id="custom">%s</xliff:g> address</string>
+
+ <!-- Action string for starting an IM chat with the AIM protocol -->
+ <string name="chat_aim">Chat using AIM</string>
+ <!-- Action string for starting an IM chat with the MSN or Windows Live protocol -->
+ <string name="chat_msn">Chat using Windows Live</string>
+ <!-- Action string for starting an IM chat with the Yahoo protocol -->
+ <string name="chat_yahoo">Chat using Yahoo</string>
+ <!-- Action string for starting an IM chat with the Skype protocol -->
+ <string name="chat_skype">Chat using Skype</string>
+ <!-- Action string for starting an IM chat with the QQ protocol -->
+ <string name="chat_qq">Chat using QQ</string>
+ <!-- Action string for starting an IM chat with the Google Talk protocol -->
+ <string name="chat_gtalk">Chat using Google Talk</string>
+ <!-- Action string for starting an IM chat with the ICQ protocol -->
+ <string name="chat_icq">Chat using ICQ</string>
+ <!-- Action string for starting an IM chat with the Jabber protocol -->
+ <string name="chat_jabber">Chat using Jabber</string>
+
+ <!-- Generic action string for starting an IM chat -->
+ <string name="chat">Chat</string>
+
+ <!-- String describing the Contact Editor Minus button
+
+ Used by AccessibilityService to announce the purpose of the button.
+
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_minus_button">delete</string>
+
+ <!-- Content description for the expand or collapse name fields button.
+ Clicking this button causes the name editor to toggle between showing
+ a single field where the entire name is edited at once, or multiple
+ fields corresponding to each part of the name (Name Prefix, First Name,
+ Middle Name, Last Name, Name Suffix).
+ [CHAR LIMIT=NONE] -->
+ <string name="expand_collapse_name_fields_description">Expand or collapse name fields</string>
+
+ <!-- Content description for the expand or collapse phonetic name fields button. [CHAR LIMIT=100] -->
+ <string name="expand_collapse_phonetic_name_fields_description">Expand or collapse phonetic
+ name fields</string>
+
+ <!-- Contact list filter label indicating that the list is showing all available accounts [CHAR LIMIT=64] -->
+ <string name="list_filter_all_accounts">All contacts</string>
+
+ <!-- Menu item to indicate you are done editing a contact and want to save the changes you've made -->
+ <string name="menu_done">Done</string>
+
+ <!-- Menu item to indicate you want to cancel the current editing process and NOT save the changes you've made [CHAR LIMIT=12] -->
+ <string name="menu_doNotSave">Cancel</string>
+
+ <!-- Displayed at the top of the contacts showing the account filter selected [CHAR LIMIT=64] -->
+ <string name="listAllContactsInAccount">Contacts in <xliff:g example="abc@gmail.com" id="name">%s</xliff:g></string>
+
+ <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=64] -->
+ <string name="listCustomView">Contacts in custom view</string>
+
+ <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=64] -->
+ <string name="listSingleContact">Single contact</string>
+
+ <!-- Message asking user to select an account to save contacts imported from vcard or SIM card [CHAR LIMIT=64] -->
+ <string name="dialog_new_contact_account">Save imported contacts to:</string>
+
+ <!-- Action string for selecting SIM for importing contacts -->
+ <string name="import_from_sim">Import from SIM card</string>
+
+ <!-- Action string for selecting a SIM subscription for importing contacts -->
+ <string name="import_from_sim_summary">Import from SIM <xliff:g id="sim_name">^1</xliff:g> - <xliff:g id="sim_number">^2</xliff:g></string>
+
+ <!-- Action string for selecting a SIM subscription for importing contacts, without a phone number -->
+ <string name="import_from_sim_summary_no_number">Import from SIM <xliff:g id="sim_name">%1$s</xliff:g></string>
+
+ <!-- Action string for selecting a .vcf file to import contacts from [CHAR LIMIT=30] -->
+ <string name="import_from_vcf_file" product="default">Import from .vcf file</string>
+
+ <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard import.
+ The argument is file name for the vCard import the user wants to cancel.
+ [CHAR LIMIT=128] -->
+ <string name="cancel_import_confirmation_message">Cancel import of <xliff:g example="import.vcf" id="filename">%s</xliff:g>?</string>
+
+ <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard export.
+ The argument is file name for the vCard export the user wants to cancel.
+ [CHAR LIMIT=128] -->
+ <string name="cancel_export_confirmation_message">Cancel export of <xliff:g example="export.vcf" id="filename">%s</xliff:g>?</string>
+
+ <!-- Title shown in a Dialog telling users cancel vCard import/export operation is failed. [CHAR LIMIT=40] -->
+ <string name="cancel_vcard_import_or_export_failed">Couldn\'t cancel vCard import/export</string>
+
+ <!-- The failed reason which should not be shown but it may in some buggy condition. [CHAR LIMIT=40] -->
+ <string name="fail_reason_unknown">Unknown error.</string>
+
+ <!-- The failed reason shown when vCard importer/exporter could not open the file
+ specified by a user. The file name should be in the message. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_could_not_open_file">Couldn\'t open \"<xliff:g id="file_name">%s</xliff:g>\": <xliff:g id="exact_reason">%s</xliff:g>.</string>
+
+ <!-- The failed reason shown when contacts exporter fails to be initialized.
+ Some exact reason must follow this. [CHAR LIMIT=NONE]-->
+ <string name="fail_reason_could_not_initialize_exporter">Couldn\'t start the exporter: \"<xliff:g id="exact_reason">%s</xliff:g>\".</string>
+
+ <!-- The failed reason shown when there's no contact which is allowed to be exported.
+ Note that user may have contacts data but all of them are probably not allowed to be
+ exported because of security/permission reasons. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_no_exportable_contact">There is no exportable contact.</string>
+
+ <!-- The user doesn't have all permissions required to use the current screen. So
+ close the current screen and show the user this message. -->
+ <string name="missing_required_permission">You have disabled a required permission.</string>
+
+ <!-- The failed reason shown when some error happend during contacts export.
+ Some exact reason must follow this. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_error_occurred_during_export">An error occurred during export: \"<xliff:g id="exact_reason">%s</xliff:g>\".</string>
+
+ <!-- The failed reason shown when the given file name is too long for the system.
+ The length limit of each file is different in each Android device, so we don't need to
+ mention it here. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\").</string>
+
+ <!-- The failed reason shown when Contacts app (especially vCard importer/exporter)
+ emitted some I/O error. Exact reason will be appended by the system. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_io_error">I/O error</string>
+
+ <!-- Failure reason show when Contacts app (especially vCard importer) encountered
+ low memory problem and could not proceed its import procedure. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_low_memory_during_import">Not enough memory. The file may be too large.</string>
+
+ <!-- The failed reason shown when vCard parser was not able to be parsed by the current vCard
+ implementation. This might happen even when the input vCard is completely valid, though
+ we believe it is rather rare in the actual world. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_vcard_parse_error">Couldn\'t parse vCard for an unexpected reason.</string>
+
+ <!-- The failed reason shown when vCard importer doesn't support the format.
+ This may be shown when the vCard is corrupted [CHAR LIMIT=40] -->
+ <string name="fail_reason_not_supported">The format isn\'t supported.</string>
+
+ <!-- Fail reason shown when vCard importer failed to look over meta information stored in vCard file(s). -->
+ <string name="fail_reason_failed_to_collect_vcard_meta_info">Couldn\'t collect meta information of given vCard file(s).</string>
+
+ <!-- The failed reason shown when the import of some of vCard files failed during multiple vCard
+ files import. It includes the case where all files were failed to be imported. -->
+ <string name="fail_reason_failed_to_read_files">One or more files couldn\'t be imported (%s).</string>
+
+ <!-- The title shown when exporting vCard is successfuly finished [CHAR LIMIT=40] -->
+ <string name="exporting_vcard_finished_title">Finished exporting <xliff:g example="export.vcf" id="filename">%s</xliff:g>.</string>
+
+ <!-- The title shown when exporting vCard has finished successfully but the destination filename could not be resolved. [CHAR LIMIT=NONE] -->
+ <string name="exporting_vcard_finished_title_fallback">Finished exporting contacts.</string>
+
+ <!-- The toast message shown when exporting vCard has finished and vCards are ready to be shared [CHAR LIMIT=150]-->
+ <string name="exporting_vcard_finished_toast">Finished exporting contacts, click the notification to share contacts.</string>
+
+ <!-- The message on notification shown when exporting vCard has finished and vCards are ready to be shared [CHAR LIMIT=60]-->
+ <string name="touch_to_share_contacts">Tap to share contacts.</string>
+
+ <!-- The title shown when exporting vCard is canceled (probably by a user)
+ The argument is file name the user canceled importing.
+ [CHAR LIMIT=40] -->
+ <string name="exporting_vcard_canceled_title">Exporting <xliff:g example="export.vcf" id="filename">%s</xliff:g> canceled.</string>
+
+ <!-- Dialog title shown when the application is exporting contact data outside. [CHAR LIMIT=NONE] -->
+ <string name="exporting_contact_list_title">Exporting contact data</string>
+
+ <!-- Message shown when the application is exporting contact data outside -->
+ <string name="exporting_contact_list_message">Contact data is being exported.</string>
+
+ <!-- The error reason the vCard composer "may" emit when database is corrupted or
+ something is going wrong. Usually users should not see this text. [CHAR LIMIT=NONE] -->
+ <string name="composer_failed_to_get_database_infomation">Couldn\'t get database information.</string>
+
+ <!-- This error message shown when the user actually have no contact
+ (e.g. just after data-wiping), or, data providers of the contact list prohibits their
+ contacts from being exported to outside world via vcard exporter, etc. [CHAR LIMIT=NONE] -->
+ <string name="composer_has_no_exportable_contact">There are no exportable contacts. If you do have contacts on your device, some data providers may not allow the contacts to be exported from the device.</string>
+
+ <!-- The error reason the vCard composer may emit when vCard composer is not initialized
+ even when needed.
+ Users should not usually see this error message. [CHAR LIMIT=NONE] -->
+ <string name="composer_not_initialized">The vCard composer didn\'t start properly.</string>
+
+ <!-- Dialog title shown when exporting Contact data failed. [CHAR LIMIT=20] -->
+ <string name="exporting_contact_failed_title">Couldn\'t export</string>
+
+ <!-- Dialog message shown when exporting Contact data failed. [CHAR LIMIT=NONE] -->
+ <string name="exporting_contact_failed_message">The contact data wasn\'t exported.\nReason: \"<xliff:g id="fail_reason">%s</xliff:g>\"</string>
+
+ <!-- Description shown when importing vCard data.
+ The argument is the name of a contact which is being read.
+ [CHAR LIMIT=20] -->
+ <string name="importing_vcard_description">Importing <xliff:g example="Joe Due" id="name">%s</xliff:g></string>
+
+ <!-- Dialog title shown when reading vCard data failed [CHAR LIMIT=40] -->
+ <string name="reading_vcard_failed_title">Couldn\'t read vCard data</string>
+
+ <!-- The title shown when reading vCard is canceled (probably by a user)
+ [CHAR LIMIT=40] -->
+ <string name="reading_vcard_canceled_title">Reading vCard data canceled</string>
+
+ <!-- The title shown when reading vCard finished
+ The argument is file name the user imported.
+ [CHAR LIMIT=40] -->
+ <string name="importing_vcard_finished_title">Finished importing vCard <xliff:g example="import.vcf" id="filename">%s</xliff:g></string>
+
+ <!-- The title shown when importing vCard is canceled (probably by a user)
+ The argument is file name the user canceled importing.
+ [CHAR LIMIT=40] -->
+ <string name="importing_vcard_canceled_title">Importing <xliff:g example="import.vcf" id="filename">%s</xliff:g> canceled</string>
+
+ <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later
+ when there are already other import/export requests.
+ The argument is file name the user imported.
+ [CHAR LIMIT=40] -->
+ <string name="vcard_import_will_start_message"><xliff:g example="import.vcf" id="filename">%s</xliff:g> will be imported shortly.</string>
+ <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later when there are already other import/export requests.
+ "The file" is what a user selected for importing.
+ [CHAR LIMIT=40] -->
+ <string name="vcard_import_will_start_message_with_default_name">The file will be imported shortly.</string>
+ <!-- The message shown when a given vCard import request is rejected by the system. [CHAR LIMIT=NONE] -->
+ <string name="vcard_import_request_rejected_message">vCard import request was rejected. Try again later.</string>
+ <!-- The message shown when vCard export request is accepted. The system may start that work soon, or do it later
+ when there are already other import/export requests.
+ The argument is file name the user exported.
+ [CHAR LIMIT=40] -->
+ <string name="vcard_export_will_start_message"><xliff:g example="import.vcf" id="filename">%s</xliff:g> will be exported shortly.</string>
+
+ <!-- The message shown when a vCard export request is accepted but the destination filename could not be resolved. [CHAR LIMIT=NONE] -->
+ <string name="vcard_export_will_start_message_fallback">The file will be exported shortly.</string>
+
+ <!-- The message shown when a vCard export request is accepted and contacts will be exported shortly. [CHAR LIMIT=70]-->
+ <string name="contacts_export_will_start_message">Contacts will be exported shortly.</string>
+
+ <!-- The message shown when a given vCard export request is rejected by the system. [CHAR LIMIT=NONE] -->
+ <string name="vcard_export_request_rejected_message">vCard export request was rejected. Try again later.</string>
+ <!-- Used when file name is unknown in vCard processing. It typically happens
+ when the file is given outside the Contacts app. [CHAR LIMIT=30] -->
+ <string name="vcard_unknown_filename">contact</string>
+
+ <!-- The message shown when vCard importer is caching files to be imported into local temporary
+ data storage. [CHAR LIMIT=NONE] -->
+ <string name="caching_vcard_message">Caching vCard(s) to local temporary storage. The actual import will start soon.</string>
+
+ <!-- Message used when vCard import has failed. [CHAR LIMIT=40] -->
+ <string name="vcard_import_failed">Couldn\'t import vCard.</string>
+
+ <!-- The "file name" displayed for vCards received directly via NFC [CHAR LIMIT=16] -->
+ <string name="nfc_vcard_file_name">Contact received over NFC</string>
+
+ <!-- Dialog title shown when a user confirms whether he/she export Contact data. [CHAR LIMIT=32] -->
+ <string name="confirm_export_title">Export contacts?</string>
+
+ <!-- The title shown when vCard importer is caching files to be imported into local temporary
+ data storage. [CHAR LIMIT=40] -->
+ <string name="caching_vcard_title">Caching</string>
+
+ <!-- The message shown while importing vCard(s).
+ First argument is current index of contacts to be imported.
+ Second argument is the total number of contacts.
+ Third argument is the name of a contact which is being read.
+ [CHAR LIMIT=20] -->
+ <string name="progress_notifier_message">Importing <xliff:g id="current_number">%s</xliff:g>/<xliff:g id="total_number">%s</xliff:g>: <xliff:g example="Joe Due" id="name">%s</xliff:g></string>
+
+ <!-- Action that exports all contacts to a user selected destination. [CHAR LIMIT=25] -->
+ <string name="export_to_vcf_file" product="default">Export to .vcf file</string>
+
+ <!-- Contact preferences related strings -->
+
+ <!-- Label of the "sort by" display option -->
+ <string name="display_options_sort_list_by">Sort by</string>
+
+ <!-- An allowable value for the "sort list by" contact display option -->
+ <string name="display_options_sort_by_given_name">First name</string>
+
+ <!-- An allowable value for the "sort list by" contact display option -->
+ <string name="display_options_sort_by_family_name">Last name</string>
+
+ <!-- Label of the "name format" display option [CHAR LIMIT=64]-->
+ <string name="display_options_view_names_as">Name format</string>
+
+ <!-- An allowable value for the "view names as" contact display option -->
+ <string name="display_options_view_given_name_first">First name first</string>
+
+ <!-- An allowable value for the "view names as" contact display option -->
+ <string name="display_options_view_family_name_first">Last name first</string>
+
+ <!--Label of the "default account" setting option to set default editor account. [CHAR LIMIT=80]-->
+ <string name="default_editor_account">Default account for new contacts</string>
+
+ <!--Label of the "Sync contact metadata" setting option to set sync account for Lychee. [CHAR LIMIT=80]-->
+ <string name="sync_contact_metadata_title">Sync contact metadata [DOGFOOD]</string>
+
+ <!--Label of the "Sync contact metadata" setting dialog to set sync account for Lychee. [CHAR LIMIT=80]-->
+ <string name="sync_contact_metadata_dialog_title">Sync contact metadata</string>
+
+ <!-- Label of the "About" setting -->
+ <string name="setting_about">About Contacts</string>
+
+ <!-- Title of the settings activity [CHAR LIMIT=64] -->
+ <string name="activity_title_settings">Settings</string>
+
+ <!-- Action that shares visible contacts -->
+ <string name="share_visible_contacts">Share visible contacts</string>
+
+ <!-- A framework exception (ie, transaction too large) can be thrown while attempting to share all visible contacts. If so, show this toast. -->
+ <string name="share_visible_contacts_failure">Failed to share visible contacts.</string>
+
+ <!-- Action that shares favorite contacts [CHAR LIMIT=40]-->
+ <string name="share_favorite_contacts">Share favorite contacts</string>
+
+ <!-- Action that shares contacts [CHAR LIMIT=30]-->
+ <string name="share_contacts">Share all contacts</string>
+
+ <!-- A framework exception can be thrown while attempting to share all contacts. If so, show this toast. [CHAR LIMIT=40]-->
+ <string name="share_contacts_failure">Failed to share contacts.</string>
+
+ <!-- Dialog title when selecting the bulk operation to perform from a list. [CHAR LIMIT=36] -->
+ <string name="dialog_import_export">Import/export contacts</string>
+
+ <!-- Dialog title when importing contacts from an external source. [CHAR LIMIT=36] -->
+ <string name="dialog_import">Import contacts</string>
+
+ <!-- Toast indicating that sharing a contact has failed. [CHAR LIMIT=NONE] -->
+ <string name="share_error">This contact can\'t be shared.</string>
+
+ <!-- Toast indicating that no visible contact to share [CHAR LIMIT=NONE] -->
+ <string name="no_contact_to_share">There are no contacts to share.</string>
+
+ <!-- Menu item to search contacts -->
+ <string name="menu_search">Search</string>
+
+ <!-- Query hint displayed inside the search field [CHAR LIMIT=64] -->
+ <string name="hint_findContacts">Find contacts</string>
+
+ <!-- The description text for the favorites tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE] -->
+ <string name="contactsFavoritesLabel">Favorites</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when "All contacts" is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZero">No contacts.</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when "Custom" is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZeroCustom">No visible contacts.</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when starred contact list is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZeroStarred">No favorites</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when a group or account is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZeroGroup">No contacts in <xliff:g example="Friends" id="name">%s</xliff:g></string>
+
+ <!-- The menu item to clear frequents [CHAR LIMIT=30] -->
+ <string name="menu_clear_frequents">Clear frequents</string>
+
+ <!-- Menu item to select SIM card -->
+ <string name="menu_select_sim">Select SIM card</string>
+
+ <!-- The menu item to open the list of accounts. [CHAR LIMIT=60]-->
+ <string name="menu_accounts">Manage accounts</string>
+
+ <!-- The menu item to bulk import or bulk export contacts from SIM card or SD card. [CHAR LIMIT=30]-->
+ <string name="menu_import_export">Import/export</string>
+
+ <!-- The font-family to use for tab text. -->
+ <string name="tab_font_family" translatable="false">sans-serif</string>
+
+ <!-- Attribution of a contact status update, when the time of update is unknown -->
+ <string name="contact_status_update_attribution">via <xliff:g example="Google Talk" id="source">%1$s</xliff:g></string>
+
+ <!-- Attribution of a contact status update, when the time of update is known -->
+ <string name="contact_status_update_attribution_with_date"><xliff:g example="3 hours ago" id="date">%1$s</xliff:g> via <xliff:g example="Google Talk" id="source">%2$s</xliff:g></string>
+
+ <!-- Font family used when drawing letters for letter tile avatars. -->
+ <string name="letter_tile_letter_font_family" translatable="false">sans-serif-medium</string>
+
+ <!-- Content description for the fake action menu up button as used
+ inside search. [CHAR LIMIT=NONE] -->
+ <string name="action_menu_back_from_search">stop searching</string>
+
+ <!-- String describing the icon used to clear the search field -->
+ <string name="description_clear_search">Clear search</string>
+
+ <!-- The font-family to use for the text inside the searchbox. -->
+ <string name="search_font_family" translatable="false">sans-serif</string>
+
+ <!-- The title of the preference section that allows users to configure how they want their
+ contacts to be displayed. [CHAR LIMIT=128] -->
+ <string name="settings_contact_display_options_title">Contact display options</string>
+
+ <!-- Title for Select Account Dialog [CHAR LIMIT=30] -->
+ <string name="select_account_dialog_title">Account</string>
+
+ <!-- Label for the check box to toggle default sim card setting [CHAR LIMIT=35]-->
+ <string name="set_default_account">Always use this for calls</string>
+
+ <!-- Title for dialog to select Phone Account for outgoing call. [CHAR LIMIT=40] -->
+ <string name="select_phone_account_for_calls">Call with</string>
+
+ <!-- String used for actions in the dialer call log and the quick contact card to initiate
+ a call to an individual. The user is prompted to enter a note which is sent along with
+ the call (e.g. a call subject). [CHAR LIMIT=40] -->
+ <string name="call_with_a_note">Call with a note</string>
+
+ <!-- Hint text shown in the call subject dialog. [CHAR LIMIT=255] -->
+ <string name="call_subject_hint">Type a note to send with call ...</string>
+
+ <!-- Button used to start a new call with the user entered subject. [CHAR LIMIT=32] -->
+ <string name="send_and_call_button">SEND &amp; CALL</string>
+
+ <!-- String used to represent the total number of characters entered for a call subject,
+ compared to the character limit. Example: 2 / 64 -->
+ <string name="call_subject_limit"><xliff:g example="4" id="count">%1$s</xliff:g> / <xliff:g example="64" id="limit">%2$s</xliff:g></string>
+
+ <!-- String used to build a phone number bype and phone number string.
+ Example: Mobile • 650-555-1212 -->
+ <string name="call_subject_type_and_number"><xliff:g example="Mobile" id="type">%1$s</xliff:g> • <xliff:g example="(650) 555-1212" id="number">%2$s</xliff:g></string>
+
+ <!-- String format to describe a tab e.g.call history tab. -->
+ <string name="tab_title"><xliff:g id="title">%1$s</xliff:g> tab.</string>
+
+ <!-- String format to describe the number of unread items in a tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <plurals name="tab_title_with_unread_items">
+ <item quantity="one">
+ <xliff:g id="title">%1$s</xliff:g> tab. <xliff:g id="count">%2$d</xliff:g> unread item.
+ </item>
+ <item quantity="other">
+ <xliff:g id="title">%1$s</xliff:g> tab. <xliff:g id="count">%2$d</xliff:g> unread items.
+ </item>
+ </plurals>
+
+ <!-- Build version title in About preference. [CHAR LIMIT=40]-->
+ <string name="about_build_version">Build version</string>
+
+ <!-- Open source licenses title in About preference. [CHAR LIMIT=60] -->
+ <string name="about_open_source_licenses">Open source licenses</string>
+
+ <!-- Open source licenses summary in About preference. [CHAR LIMIT=NONE] -->
+ <string name="about_open_source_licenses_summary">License details for open source software</string>
+
+ <!-- Privacy policy title in About preference. [CHAR LIMIT=40]-->
+ <string name="about_privacy_policy">Privacy policy</string>
+
+ <!-- Terms of service title in about preference. [CHAR LIMIT=60]-->
+ <string name="about_terms_of_service">Terms of service</string>
+
+ <!-- Title for the activity that displays licenses for open source libraries. [CHAR LIMIT=100]-->
+ <string name="activity_title_licenses">Open source licenses</string>
+
+ <!-- Toast message showing when failed to open the url. [CHAR LIMIT=100]-->
+ <string name="url_open_error_toast">Failed to open the url.</string>
+
+ <!-- Description string for an action button to initiate a video call from search results.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE]-->
+ <string name="description_search_video_call">Place video call</string>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/styles.xml b/java/com/android/contacts/common/res/values/styles.xml
new file mode 100644
index 000000000..07d4a0225
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/styles.xml
@@ -0,0 +1,97 @@
+<!--
+ ~ Copyright (C) 2012 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
+ -->
+
+<resources>
+
+ <style name="DirectoryHeader">
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <style name="SectionHeaderStyle" parent="@android:style/TextAppearance.Large">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textColor">@color/section_header_text_color</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="DirectoryHeaderStyle" parent="@android:style/TextAppearance.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">@color/dialer_secondary_text_color</item>
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <!-- TextView style used for headers.
+
+This is similar to ?android:attr/listSeparatorTextView but uses different
+background and text color. See also android:style/Widget.Holo.TextView.ListSeparator
+(which is private, so we cannot specify it as a parent style). -->
+ <style name="ContactListSeparatorTextViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <!-- See comments for @dimen/list_section_divider_min_height -->
+ <item name="android:minHeight">@dimen/list_section_divider_min_height</item>
+ <item name="android:background">@drawable/list_section_divider_holo_custom</item>
+ <item name="android:textAppearance">@style/DirectoryHeaderStyle</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingLeft">8dip</item>
+ <item name="android:paddingStart">8dip</item>
+ <item name="android:paddingTop">4dip</item>
+ <item name="android:paddingBottom">4dip</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="TextAppearanceMedium" parent="@android:style/TextAppearance.Medium">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#000000</item>
+ </style>
+
+ <style name="TextAppearanceSmall" parent="@android:style/TextAppearance.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#737373</item>
+ </style>
+
+ <style name="ListViewStyle" parent="@android:style/Widget.Material.Light.ListView">
+ <item name="android:overScrollMode">always</item>
+ </style>
+
+ <style name="BackgroundOnlyTheme" parent="@android:style/Theme.Material.Light">
+ <item name="android:windowBackground">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <!-- Activities that use this theme are background activities without obvious displays.
+ However, some also have dialogs. Therefore, it doesn't make sense to set this true.-->
+ <item name="android:windowNoDisplay">false</item>
+ <item name="android:windowIsFloating">true</item>
+ </style>
+
+ <style name="Theme.CallSubjectDialogTheme" parent="@android:style/Theme.Material.Light.Dialog">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+
+ <!-- No backgrounds, titles or window float -->
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowFullscreen">false</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowElevation">0dp</item>
+ </style>
+</resources>
diff --git a/java/com/android/contacts/common/testing/InjectedServices.java b/java/com/android/contacts/common/testing/InjectedServices.java
new file mode 100644
index 000000000..5ab5e5feb
--- /dev/null
+++ b/java/com/android/contacts/common/testing/InjectedServices.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.testing;
+
+import android.content.ContentResolver;
+import android.content.SharedPreferences;
+import android.util.ArrayMap;
+import java.util.Map;
+
+/**
+ * A mechanism for providing alternative (mock) services to the application while running tests.
+ * Activities, Services and the Application should check with this class to see if a particular
+ * service has been overridden.
+ */
+public class InjectedServices {
+
+ private ContentResolver mContentResolver;
+ private SharedPreferences mSharedPreferences;
+ private Map<String, Object> mSystemServices;
+
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ public void setContentResolver(ContentResolver contentResolver) {
+ this.mContentResolver = contentResolver;
+ }
+
+ public SharedPreferences getSharedPreferences() {
+ return mSharedPreferences;
+ }
+
+ public void setSharedPreferences(SharedPreferences sharedPreferences) {
+ this.mSharedPreferences = sharedPreferences;
+ }
+
+ public void setSystemService(String name, Object service) {
+ if (mSystemServices == null) {
+ mSystemServices = new ArrayMap<>();
+ }
+
+ mSystemServices.put(name, service);
+ }
+
+ public Object getSystemService(String name) {
+ if (mSystemServices != null) {
+ return mSystemServices.get(name);
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/contacts/common/util/AccountFilterUtil.java b/java/com/android/contacts/common/util/AccountFilterUtil.java
new file mode 100644
index 000000000..18743c65e
--- /dev/null
+++ b/java/com/android/contacts/common/util/AccountFilterUtil.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.ContactListFilterController;
+
+/** Utility class for account filter manipulation. */
+public class AccountFilterUtil {
+
+ public static final String EXTRA_CONTACT_LIST_FILTER = "contactListFilter";
+ private static final String TAG = AccountFilterUtil.class.getSimpleName();
+
+ /**
+ * Find TextView with the id "account_filter_header" and set correct text for the account filter
+ * header.
+ *
+ * @param filterContainer View containing TextView with id "account_filter_header"
+ * @return true when header text is set in the call. You may use this for conditionally showing or
+ * hiding this entire view.
+ */
+ public static boolean updateAccountFilterTitleForPeople(
+ View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) {
+ return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, false);
+ }
+
+ /**
+ * Similar to {@link #updateAccountFilterTitleForPeople(View, ContactListFilter, boolean,
+ * boolean)}, but for Phone UI.
+ */
+ public static boolean updateAccountFilterTitleForPhone(
+ View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) {
+ return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, true);
+ }
+
+ private static boolean updateAccountFilterTitle(
+ View filterContainer,
+ ContactListFilter filter,
+ boolean showTitleForAllAccounts,
+ boolean forPhone) {
+ final Context context = filterContainer.getContext();
+ final TextView headerTextView =
+ (TextView) filterContainer.findViewById(R.id.account_filter_header);
+
+ boolean textWasSet = false;
+ if (filter != null) {
+ if (forPhone) {
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+ if (showTitleForAllAccounts) {
+ headerTextView.setText(R.string.list_filter_phones);
+ textWasSet = true;
+ }
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ headerTextView.setText(
+ context.getString(R.string.listAllContactsInAccount, filter.accountName));
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ headerTextView.setText(R.string.listCustomView);
+ textWasSet = true;
+ } else {
+ Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected.");
+ }
+ } else {
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+ if (showTitleForAllAccounts) {
+ headerTextView.setText(R.string.list_filter_all_accounts);
+ textWasSet = true;
+ }
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ headerTextView.setText(
+ context.getString(R.string.listAllContactsInAccount, filter.accountName));
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ headerTextView.setText(R.string.listCustomView);
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+ headerTextView.setText(R.string.listSingleContact);
+ textWasSet = true;
+ } else {
+ Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected.");
+ }
+ }
+ } else {
+ Log.w(TAG, "Filter is null.");
+ }
+ return textWasSet;
+ }
+
+ /** This will update filter via a given ContactListFilterController. */
+ public static void handleAccountFilterResult(
+ ContactListFilterController filterController, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ final ContactListFilter filter = data.getParcelableExtra(EXTRA_CONTACT_LIST_FILTER);
+ if (filter == null) {
+ return;
+ }
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ filterController.selectCustomFilter();
+ } else {
+ filterController.setContactListFilter(filter, true);
+ }
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/BitmapUtil.java b/java/com/android/contacts/common/util/BitmapUtil.java
new file mode 100644
index 000000000..20f916a3f
--- /dev/null
+++ b/java/com/android/contacts/common/util/BitmapUtil.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+/** Provides static functions to decode bitmaps at the optimal size */
+public class BitmapUtil {
+
+ private BitmapUtil() {}
+
+ /**
+ * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually
+ * decode the picture, so it is pretty efficient to run.
+ */
+ public static int getSmallerExtentFromBytes(byte[] bytes) {
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+
+ // don't actually decode the picture, just return its bounds
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+
+ // test what the best sample size is
+ return Math.min(options.outWidth, options.outHeight);
+ }
+
+ /**
+ * Finds the optimal sampleSize for loading the picture
+ *
+ * @param originalSmallerExtent Width or height of the picture, whichever is smaller
+ * @param targetExtent Width or height of the target view, whichever is bigger.
+ * <p>If either one of the parameters is 0 or smaller, no sampling is applied
+ */
+ public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) {
+ // If we don't know sizes, we can't do sampling.
+ if (targetExtent < 1) {
+ return 1;
+ }
+ if (originalSmallerExtent < 1) {
+ return 1;
+ }
+
+ // Test what the best sample size is. To do that, we find the sample size that gives us
+ // the best trade-off between resulting image size and memory requirement. We allow
+ // the down-sampled image to be 20% smaller than the target size. That way we can get around
+ // unfortunate cases where e.g. a 720 picture is requested for 362 and not down-sampled at
+ // all. Why 20%? Why not. Prove me wrong.
+ int extent = originalSmallerExtent;
+ int sampleSize = 1;
+ while ((extent >> 1) >= targetExtent * 0.8f) {
+ sampleSize <<= 1;
+ extent >>= 1;
+ }
+
+ return sampleSize;
+ }
+
+ /** Decodes the bitmap with the given sample size */
+ public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
+ final BitmapFactory.Options options;
+ if (sampleSize <= 1) {
+ options = null;
+ } else {
+ options = new BitmapFactory.Options();
+ options.inSampleSize = sampleSize;
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ }
+
+ /**
+ * Retrieves a copy of the specified drawable resource, rotated by a specified angle.
+ *
+ * @param resources The current resources.
+ * @param resourceId The resource ID of the drawable to rotate.
+ * @param angle The angle of rotation.
+ * @return Rotated drawable.
+ */
+ public static Drawable getRotatedDrawable(
+ android.content.res.Resources resources, int resourceId, float angle) {
+
+ // Get the original drawable and make a copy which will be rotated.
+ Bitmap original = BitmapFactory.decodeResource(resources, resourceId);
+ Bitmap rotated =
+ Bitmap.createBitmap(original.getWidth(), original.getHeight(), Bitmap.Config.ARGB_8888);
+
+ // Perform the rotation.
+ Canvas tempCanvas = new Canvas(rotated);
+ tempCanvas.rotate(angle, original.getWidth() / 2, original.getHeight() / 2);
+ tempCanvas.drawBitmap(original, 0, 0, null);
+
+ return new BitmapDrawable(resources, rotated);
+ }
+
+ /**
+ * Given an input bitmap, scales it to the given width/height and makes it round.
+ *
+ * @param input {@link Bitmap} to scale and crop
+ * @param targetWidth desired output width
+ * @param targetHeight desired output height
+ * @return output bitmap scaled to the target width/height and cropped to an oval. The cropping
+ * algorithm will try to fit as much of the input into the output as possible, while
+ * preserving the target width/height ratio.
+ */
+ public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) {
+ if (input == null) {
+ return null;
+ }
+ final Bitmap.Config inputConfig = input.getConfig();
+ final Bitmap result =
+ Bitmap.createBitmap(
+ targetWidth, targetHeight, inputConfig != null ? inputConfig : Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(result);
+ final Paint paint = new Paint();
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setAntiAlias(true);
+ final RectF dst = new RectF(0, 0, targetWidth, targetHeight);
+ canvas.drawOval(dst, paint);
+
+ // Specifies that only pixels present in the destination (i.e. the drawn oval) should
+ // be overwritten with pixels from the input bitmap.
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+
+ final int inputWidth = input.getWidth();
+ final int inputHeight = input.getHeight();
+
+ // Choose the largest scale factor that will fit inside the dimensions of the
+ // input bitmap.
+ final float scaleBy =
+ Math.min((float) inputWidth / targetWidth, (float) inputHeight / targetHeight);
+
+ final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2);
+ final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2);
+
+ final Rect src =
+ new Rect(
+ inputWidth / 2 - xCropAmountHalved,
+ inputHeight / 2 - yCropAmountHalved,
+ inputWidth / 2 + xCropAmountHalved,
+ inputHeight / 2 + yCropAmountHalved);
+
+ canvas.drawBitmap(input, src, dst, paint);
+ return result;
+ }
+}
diff --git a/java/com/android/contacts/common/util/CommonDateUtils.java b/java/com/android/contacts/common/util/CommonDateUtils.java
new file mode 100644
index 000000000..312e691f8
--- /dev/null
+++ b/java/com/android/contacts/common/util/CommonDateUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/** Common date utilities. */
+public class CommonDateUtils {
+
+ // All the SimpleDateFormats in this class use the UTC timezone
+ public static final SimpleDateFormat NO_YEAR_DATE_FORMAT =
+ new SimpleDateFormat("--MM-dd", Locale.US);
+ public static final SimpleDateFormat FULL_DATE_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd", Locale.US);
+ public static final SimpleDateFormat DATE_AND_TIME_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
+ new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+
+ /** Exchange requires 8:00 for birthdays */
+ public static final int DEFAULT_HOUR = 8;
+}
diff --git a/java/com/android/contacts/common/util/Constants.java b/java/com/android/contacts/common/util/Constants.java
new file mode 100644
index 000000000..172e8c348
--- /dev/null
+++ b/java/com/android/contacts/common/util/Constants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2009 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.
+ */
+
+package com.android.contacts.common.util;
+
+public class Constants {
+
+ /**
+ * Log tag for performance measurement. To enable: adb shell setprop log.tag.ContactsPerf VERBOSE
+ */
+ public static final String PERFORMANCE_TAG = "ContactsPerf";
+
+ // Used for lookup URI that contains an encoded JSON string.
+ public static final String LOOKUP_URI_ENCODED = "encoded";
+}
diff --git a/java/com/android/contacts/common/util/ContactDisplayUtils.java b/java/com/android/contacts/common/util/ContactDisplayUtils.java
new file mode 100644
index 000000000..1586784db
--- /dev/null
+++ b/java/com/android/contacts/common/util/ContactDisplayUtils.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2012 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
+ */
+
+package com.android.contacts.common.util;
+
+import static android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import android.util.Log;
+import android.util.Patterns;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import java.util.Objects;
+
+/** Methods for handling various contact data labels. */
+public class ContactDisplayUtils {
+
+ public static final int INTERACTION_CALL = 1;
+ public static final int INTERACTION_SMS = 2;
+ private static final String TAG = ContactDisplayUtils.class.getSimpleName();
+
+ /**
+ * Checks if the given data type is a custom type.
+ *
+ * @param type Phone data type.
+ * @return {@literal true} if the type is custom. {@literal false} if not.
+ */
+ public static boolean isCustomPhoneType(Integer type) {
+ return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT;
+ }
+
+ /**
+ * Gets a display label for a given phone type.
+ *
+ * @param type The type of number.
+ * @param customLabel A custom label to use if the phone is determined to be of custom type
+ * determined by {@link #isCustomPhoneType(Integer))}
+ * @param interactionType whether this is a call or sms. Either {@link #INTERACTION_CALL} or
+ * {@link #INTERACTION_SMS}.
+ * @param context The application context.
+ * @return An appropriate string label
+ */
+ public static CharSequence getLabelForCallOrSms(
+ Integer type, CharSequence customLabel, int interactionType, @NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ if (isCustomPhoneType(type)) {
+ return (customLabel == null) ? "" : customLabel;
+ } else {
+ int resId;
+ if (interactionType == INTERACTION_SMS) {
+ resId = getSmsLabelResourceId(type);
+ } else {
+ resId = getPhoneLabelResourceId(type);
+ if (interactionType != INTERACTION_CALL) {
+ Log.e(
+ TAG,
+ "Un-recognized interaction type: "
+ + interactionType
+ + ". Defaulting to ContactDisplayUtils.INTERACTION_CALL.");
+ }
+ }
+
+ return context.getResources().getText(resId);
+ }
+ }
+
+ /**
+ * Find a label for calling.
+ *
+ * @param type The type of number.
+ * @return An appropriate string label.
+ */
+ public static int getPhoneLabelResourceId(Integer type) {
+ if (type == null) {
+ return R.string.call_other;
+ }
+ switch (type) {
+ case Phone.TYPE_HOME:
+ return R.string.call_home;
+ case Phone.TYPE_MOBILE:
+ return R.string.call_mobile;
+ case Phone.TYPE_WORK:
+ return R.string.call_work;
+ case Phone.TYPE_FAX_WORK:
+ return R.string.call_fax_work;
+ case Phone.TYPE_FAX_HOME:
+ return R.string.call_fax_home;
+ case Phone.TYPE_PAGER:
+ return R.string.call_pager;
+ case Phone.TYPE_OTHER:
+ return R.string.call_other;
+ case Phone.TYPE_CALLBACK:
+ return R.string.call_callback;
+ case Phone.TYPE_CAR:
+ return R.string.call_car;
+ case Phone.TYPE_COMPANY_MAIN:
+ return R.string.call_company_main;
+ case Phone.TYPE_ISDN:
+ return R.string.call_isdn;
+ case Phone.TYPE_MAIN:
+ return R.string.call_main;
+ case Phone.TYPE_OTHER_FAX:
+ return R.string.call_other_fax;
+ case Phone.TYPE_RADIO:
+ return R.string.call_radio;
+ case Phone.TYPE_TELEX:
+ return R.string.call_telex;
+ case Phone.TYPE_TTY_TDD:
+ return R.string.call_tty_tdd;
+ case Phone.TYPE_WORK_MOBILE:
+ return R.string.call_work_mobile;
+ case Phone.TYPE_WORK_PAGER:
+ return R.string.call_work_pager;
+ case Phone.TYPE_ASSISTANT:
+ return R.string.call_assistant;
+ case Phone.TYPE_MMS:
+ return R.string.call_mms;
+ default:
+ return R.string.call_custom;
+ }
+ }
+
+ /**
+ * Find a label for sending an sms.
+ *
+ * @param type The type of number.
+ * @return An appropriate string label.
+ */
+ public static int getSmsLabelResourceId(Integer type) {
+ if (type == null) {
+ return R.string.sms_other;
+ }
+ switch (type) {
+ case Phone.TYPE_HOME:
+ return R.string.sms_home;
+ case Phone.TYPE_MOBILE:
+ return R.string.sms_mobile;
+ case Phone.TYPE_WORK:
+ return R.string.sms_work;
+ case Phone.TYPE_FAX_WORK:
+ return R.string.sms_fax_work;
+ case Phone.TYPE_FAX_HOME:
+ return R.string.sms_fax_home;
+ case Phone.TYPE_PAGER:
+ return R.string.sms_pager;
+ case Phone.TYPE_OTHER:
+ return R.string.sms_other;
+ case Phone.TYPE_CALLBACK:
+ return R.string.sms_callback;
+ case Phone.TYPE_CAR:
+ return R.string.sms_car;
+ case Phone.TYPE_COMPANY_MAIN:
+ return R.string.sms_company_main;
+ case Phone.TYPE_ISDN:
+ return R.string.sms_isdn;
+ case Phone.TYPE_MAIN:
+ return R.string.sms_main;
+ case Phone.TYPE_OTHER_FAX:
+ return R.string.sms_other_fax;
+ case Phone.TYPE_RADIO:
+ return R.string.sms_radio;
+ case Phone.TYPE_TELEX:
+ return R.string.sms_telex;
+ case Phone.TYPE_TTY_TDD:
+ return R.string.sms_tty_tdd;
+ case Phone.TYPE_WORK_MOBILE:
+ return R.string.sms_work_mobile;
+ case Phone.TYPE_WORK_PAGER:
+ return R.string.sms_work_pager;
+ case Phone.TYPE_ASSISTANT:
+ return R.string.sms_assistant;
+ case Phone.TYPE_MMS:
+ return R.string.sms_mms;
+ default:
+ return R.string.sms_custom;
+ }
+ }
+
+ /**
+ * Whether the given text could be a phone number.
+ *
+ * <p>Note this will miss many things that are legitimate phone numbers, for example, phone
+ * numbers with letters.
+ */
+ public static boolean isPossiblePhoneNumber(CharSequence text) {
+ return text != null && Patterns.PHONE.matcher(text.toString()).matches();
+ }
+
+ /**
+ * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for the given
+ * phone number text wherever it is found within the message.
+ */
+ public static Spannable getTelephoneTtsSpannable(
+ @Nullable String message, @Nullable String phoneNumber) {
+ if (message == null) {
+ return null;
+ }
+ final Spannable spannable = new SpannableString(message);
+ int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber);
+ while (start >= 0) {
+ final int end = start + phoneNumber.length();
+ final TtsSpan ttsSpan = PhoneNumberUtilsCompat.createTtsSpan(phoneNumber);
+ spannable.setSpan(
+ ttsSpan,
+ start,
+ end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // this is consistenly done in a misleading way..
+ start = message.indexOf(phoneNumber, end);
+ }
+ return spannable;
+ }
+
+ /**
+ * Retrieves a string from a string template that takes 1 phone number as argument, span the
+ * number with a telephone {@link TtsSpan}, and return the spanned string.
+ *
+ * @param resources to retrieve the string from
+ * @param stringId ID of the string
+ * @param number to pass in the template
+ * @return CharSequence with the phone number wrapped in a TtsSpan
+ */
+ public static CharSequence getTtsSpannedPhoneNumber(
+ Resources resources, int stringId, String number) {
+ String msg = resources.getString(stringId, number);
+ return ContactDisplayUtils.getTelephoneTtsSpannable(msg, number);
+ }
+
+ /**
+ * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}.
+ * Defaults to the name that is non-null.
+ *
+ * @param namePrimary the primary name.
+ * @param nameAlternative the alternative name.
+ * @param contactsPreferences the ContactsPreferences used to determine the preferred display
+ * name.
+ * @return namePrimary or nameAlternative depending on the value of displayOrderPreference.
+ */
+ public static String getPreferredDisplayName(
+ String namePrimary,
+ String nameAlternative,
+ @Nullable ContactsPreferences contactsPreferences) {
+ if (contactsPreferences == null) {
+ return namePrimary != null ? namePrimary : nameAlternative;
+ }
+ if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ return namePrimary;
+ }
+
+ if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE
+ && !TextUtils.isEmpty(nameAlternative)) {
+ return nameAlternative;
+ }
+
+ return namePrimary;
+ }
+
+ /**
+ * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}.
+ * Defaults to the name that is non-null.
+ *
+ * @param namePrimary the primary name.
+ * @param nameAlternative the alternative name.
+ * @param contactsPreferences the ContactsPreferences used to determine the preferred sort order.
+ * @return namePrimary or nameAlternative depending on the value of displayOrderPreference.
+ */
+ public static String getPreferredSortName(
+ String namePrimary,
+ String nameAlternative,
+ @Nullable ContactsPreferences contactsPreferences) {
+ if (contactsPreferences == null) {
+ return namePrimary != null ? namePrimary : nameAlternative;
+ }
+
+ if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+ return namePrimary;
+ }
+
+ if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE
+ && !TextUtils.isEmpty(nameAlternative)) {
+ return nameAlternative;
+ }
+
+ return namePrimary;
+ }
+}
diff --git a/java/com/android/contacts/common/util/ContactListViewUtils.java b/java/com/android/contacts/common/util/ContactListViewUtils.java
new file mode 100644
index 000000000..278c27d5c
--- /dev/null
+++ b/java/com/android/contacts/common/util/ContactListViewUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.view.View;
+import android.widget.ListView;
+import com.android.contacts.common.R;
+import com.android.dialer.util.ViewUtil;
+
+/** Utilities for configuring ListViews with a card background. */
+public class ContactListViewUtils {
+
+ // These two constants will help add more padding for the text inside the card.
+ private static final double TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO = 1.1;
+
+ private static void addPaddingToView(
+ ListView listView, int parentWidth, int listSpaceWeight, int listViewWeight) {
+ if (listSpaceWeight > 0 && listViewWeight > 0) {
+ double paddingPercent =
+ (double) listSpaceWeight / (double) (listSpaceWeight * 2 + listViewWeight);
+ listView.setPadding(
+ (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO),
+ listView.getPaddingTop(),
+ (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO),
+ listView.getPaddingBottom());
+ // The EdgeEffect and ScrollBar need to span to the edge of the ListView's padding.
+ listView.setClipToPadding(false);
+ listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+ }
+ }
+
+ /**
+ * Add padding to {@param listView} if this configuration has set both space weight and view
+ * weight on the layout. Use this util method instead of defining the padding in the layout file
+ * so that the {@param listView}'s padding can be set proportional to the card padding.
+ *
+ * @param listView ListView that we add padding to
+ * @param rootLayout layout that contains ListView and R.id.list_card
+ */
+ public static void applyCardPaddingToView(
+ Resources resources, final ListView listView, final View rootLayout) {
+ // Set a padding on the list view so it appears in the center of the card
+ // in the layout if required.
+ final int listSpaceWeight = resources.getInteger(R.integer.contact_list_space_layout_weight);
+ final int listViewWeight = resources.getInteger(R.integer.contact_list_card_layout_weight);
+ if (listSpaceWeight > 0 && listViewWeight > 0) {
+ rootLayout.setBackgroundResource(0);
+ // Set the card view visible
+ View mCardView = rootLayout.findViewById(R.id.list_card);
+ if (mCardView == null) {
+ throw new RuntimeException(
+ "Your content must have a list card view who can be turned visible "
+ + "whenever it is necessary.");
+ }
+ mCardView.setVisibility(View.VISIBLE);
+
+ // Add extra padding to the list view to make them appear in the center of the card.
+ // In order to avoid jumping, we skip drawing the next frame of the ListView.
+ ViewUtil.doOnPreDraw(
+ listView,
+ false,
+ new Runnable() {
+ @Override
+ public void run() {
+ // Use the rootLayout.getWidth() instead of listView.getWidth() since
+ // we sometimes hide the listView until we finish loading data. This would
+ // result in incorrect padding.
+ ContactListViewUtils.addPaddingToView(
+ listView, rootLayout.getWidth(), listSpaceWeight, listViewWeight);
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/ContactLoaderUtils.java b/java/com/android/contacts/common/util/ContactLoaderUtils.java
new file mode 100644
index 000000000..e30971721
--- /dev/null
+++ b/java/com/android/contacts/common/util/ContactLoaderUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+
+/** Utility methods for the {@link ContactLoader}. */
+public final class ContactLoaderUtils {
+
+ /** Static helper, not instantiable. */
+ private ContactLoaderUtils() {}
+
+ /**
+ * Transforms the given Uri and returns a Lookup-Uri that represents the contact. For legacy
+ * contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException} can be thrown
+ * if the URI is null or the authority is not recognized.
+ *
+ * <p>Do not call from the UI thread.
+ */
+ @SuppressWarnings("deprecation")
+ public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri)
+ throws IllegalArgumentException {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri must not be null");
+ }
+
+ final String authority = uri.getAuthority();
+
+ // Current Style Uri?
+ if (ContactsContract.AUTHORITY.equals(authority)) {
+ final String type = resolver.getType(uri);
+ // Contact-Uri? Good, return it
+ if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ return uri;
+ }
+
+ // RawContact-Uri? Transform it to ContactUri
+ if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(
+ resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ // Anything else? We don't know what this is
+ throw new IllegalArgumentException("uri format is unknown");
+ }
+
+ // Legacy Style? Convert to RawContact
+ final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY;
+ if (OBSOLETE_AUTHORITY.equals(authority)) {
+ // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(
+ resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ throw new IllegalArgumentException("uri authority is unknown");
+ }
+}
diff --git a/java/com/android/contacts/common/util/DateUtils.java b/java/com/android/contacts/common/util/DateUtils.java
new file mode 100644
index 000000000..1935d727a
--- /dev/null
+++ b/java/com/android/contacts/common/util/DateUtils.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2010 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.Time;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/** Utility methods for processing dates. */
+public class DateUtils {
+
+ public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+
+ /**
+ * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year. Let's
+ * add a one-off hack for that day of the year
+ */
+ public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
+
+ // Variations of ISO 8601 date format. Do not change the order - it does affect the
+ // result in ambiguous cases.
+ private static final SimpleDateFormat[] DATE_FORMATS = {
+ CommonDateUtils.FULL_DATE_FORMAT,
+ CommonDateUtils.DATE_AND_TIME_FORMAT,
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
+ };
+
+ static {
+ for (SimpleDateFormat format : DATE_FORMATS) {
+ format.setLenient(true);
+ format.setTimeZone(UTC_TIMEZONE);
+ }
+ CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
+ }
+
+ /**
+ * Parses the supplied string to see if it looks like a date.
+ *
+ * @param string The string representation of the provided date
+ * @param mustContainYear If true, the string is parsed as a date containing a year. If false, the
+ * string is parsed into a valid date even if the year field is missing.
+ * @return A Calendar object corresponding to the date if the string is successfully parsed. If
+ * not, null is returned.
+ */
+ public static Calendar parseDate(String string, boolean mustContainYear) {
+ ParsePosition parsePosition = new ParsePosition(0);
+ Date date;
+ if (!mustContainYear) {
+ final boolean noYearParsed;
+ // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
+ if (NO_YEAR_DATE_FEB29TH.equals(string)) {
+ return getUtcDate(0, Calendar.FEBRUARY, 29);
+ } else {
+ synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
+ date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
+ }
+ noYearParsed = parsePosition.getIndex() == string.length();
+ }
+
+ if (noYearParsed) {
+ return getUtcDate(date, true);
+ }
+ }
+ for (int i = 0; i < DATE_FORMATS.length; i++) {
+ SimpleDateFormat f = DATE_FORMATS[i];
+ synchronized (f) {
+ parsePosition.setIndex(0);
+ date = f.parse(string, parsePosition);
+ if (parsePosition.getIndex() == string.length()) {
+ return getUtcDate(date, false);
+ }
+ }
+ }
+ return null;
+ }
+
+ private static final Calendar getUtcDate(Date date, boolean noYear) {
+ final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+ calendar.setTime(date);
+ if (noYear) {
+ calendar.set(Calendar.YEAR, 0);
+ }
+ return calendar;
+ }
+
+ private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
+ final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+ calendar.clear();
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month);
+ calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+ return calendar;
+ }
+
+ public static boolean isYearSet(Calendar cal) {
+ // use the Calendar.YEAR field to track whether or not the year is set instead of
+ // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
+ // true irregardless of what the previous value was
+ return cal.get(Calendar.YEAR) > 1;
+ }
+
+ /**
+ * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with longForm
+ * set to {@code true} by default.
+ *
+ * @param context Valid context
+ * @param string String representation of a date to parse
+ * @return Returns the same date in a cleaned up format. If the supplied string does not look like
+ * a date, return it unchanged.
+ */
+ public static String formatDate(Context context, String string) {
+ return formatDate(context, string, true);
+ }
+
+ /**
+ * Parses the supplied string to see if it looks like a date.
+ *
+ * @param context Valid context
+ * @param string String representation of a date to parse
+ * @param longForm If true, return the date formatted into its long string representation. If
+ * false, return the date formatted using its short form representation (i.e. 12/11/2012)
+ * @return Returns the same date in a cleaned up format. If the supplied string does not look like
+ * a date, return it unchanged.
+ */
+ public static String formatDate(Context context, String string, boolean longForm) {
+ if (string == null) {
+ return null;
+ }
+
+ string = string.trim();
+ if (string.length() == 0) {
+ return string;
+ }
+ final Calendar cal = parseDate(string, false);
+
+ // we weren't able to parse the string successfully so just return it unchanged
+ if (cal == null) {
+ return string;
+ }
+
+ final boolean isYearSet = isYearSet(cal);
+ final java.text.DateFormat outFormat;
+ if (!isYearSet) {
+ outFormat = getLocalizedDateFormatWithoutYear(context);
+ } else {
+ outFormat =
+ longForm ? DateFormat.getLongDateFormat(context) : DateFormat.getDateFormat(context);
+ }
+ synchronized (outFormat) {
+ outFormat.setTimeZone(UTC_TIMEZONE);
+ return outFormat.format(cal.getTime());
+ }
+ }
+
+ public static boolean isMonthBeforeDay(Context context) {
+ char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
+ for (int i = 0; i < dateFormatOrder.length; i++) {
+ if (dateFormatOrder[i] == 'd') {
+ return false;
+ }
+ if (dateFormatOrder[i] == 'M') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a SimpleDateFormat object without the year fields by using a regular expression to
+ * eliminate the year in the string pattern. In the rare occurence that the resulting pattern
+ * cannot be reconverted into a SimpleDateFormat, it uses the provided context to determine
+ * whether the month field should be displayed before the day field, and returns either "MMMM dd"
+ * or "dd MMMM" converted into a SimpleDateFormat.
+ */
+ public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
+ final String pattern =
+ ((SimpleDateFormat) SimpleDateFormat.getDateInstance(java.text.DateFormat.LONG))
+ .toPattern();
+ // Determine the correct regex pattern for year.
+ // Special case handling for Spanish locale by checking for "de"
+ final String yearPattern =
+ pattern.contains("de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
+ try {
+ // Eliminate the substring in pattern that matches the format for that of year
+ return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
+ } catch (IllegalArgumentException e) {
+ return new SimpleDateFormat(DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
+ }
+ }
+
+ /**
+ * Given a calendar (possibly containing only a day of the year), returns the earliest possible
+ * anniversary of the date that is equal to or after the current point in time if the date does
+ * not contain a year, or the date converted to the local time zone (if the date contains a year.
+ *
+ * @param target The date we wish to convert(in the UTC time zone).
+ * @return If date does not contain a year (year < 1900), returns the next earliest anniversary
+ * that is after the current point in time (in the local time zone). Otherwise, returns the
+ * adjusted Date in the local time zone.
+ */
+ public static Date getNextAnnualDate(Calendar target) {
+ final Calendar today = Calendar.getInstance();
+ today.setTime(new Date());
+
+ // Round the current time to the exact start of today so that when we compare
+ // today against the target date, both dates are set to exactly 0000H.
+ today.set(Calendar.HOUR_OF_DAY, 0);
+ today.set(Calendar.MINUTE, 0);
+ today.set(Calendar.SECOND, 0);
+ today.set(Calendar.MILLISECOND, 0);
+
+ final boolean isYearSet = isYearSet(target);
+ final int targetYear = target.get(Calendar.YEAR);
+ final int targetMonth = target.get(Calendar.MONTH);
+ final int targetDay = target.get(Calendar.DAY_OF_MONTH);
+ final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
+ final GregorianCalendar anniversary = new GregorianCalendar();
+ // Convert from the UTC date to the local date. Set the year to today's year if the
+ // there is no provided year (targetYear < 1900)
+ anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear, targetMonth, targetDay);
+ // If the anniversary's date is before the start of today and there is no year set,
+ // increment the year by 1 so that the returned date is always equal to or greater than
+ // today. If the day is a leap year, keep going until we get the next leap year anniversary
+ // Otherwise if there is already a year set, simply return the exact date.
+ if (!isYearSet) {
+ int anniversaryYear = today.get(Calendar.YEAR);
+ if (anniversary.before(today) || (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
+ // If the target date is not Feb 29, then set the anniversary to the next year.
+ // Otherwise, keep going until we find the next leap year (this is not guaranteed
+ // to be in 4 years time).
+ do {
+ anniversaryYear += 1;
+ } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
+ anniversary.set(anniversaryYear, targetMonth, targetDay);
+ }
+ }
+ return anniversary.getTime();
+ }
+
+ /**
+ * Determine the difference, in days between two dates. Uses similar logic as the {@link
+ * android.text.format.DateUtils.getRelativeTimeSpanString} method.
+ *
+ * @param time Instance of time object to use for calculations.
+ * @param date1 First date to check.
+ * @param date2 Second date to check.
+ * @return The absolute difference in days between the two dates.
+ */
+ public static int getDayDifference(Time time, long date1, long date2) {
+ time.set(date1);
+ int startDay = Time.getJulianDay(date1, time.gmtoff);
+
+ time.set(date2);
+ int currentDay = Time.getJulianDay(date2, time.gmtoff);
+
+ return Math.abs(currentDay - startDay);
+ }
+}
diff --git a/java/com/android/contacts/common/util/FabUtil.java b/java/com/android/contacts/common/util/FabUtil.java
new file mode 100644
index 000000000..b1bb2e653
--- /dev/null
+++ b/java/com/android/contacts/common/util/FabUtil.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.ListView;
+import com.android.contacts.common.R;
+import com.android.dialer.compat.CompatUtils;
+
+/** Provides static functions to work with views */
+public class FabUtil {
+
+ private static final ViewOutlineProvider OVAL_OUTLINE_PROVIDER =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setOval(0, 0, view.getWidth(), view.getHeight());
+ }
+ };
+
+ private FabUtil() {}
+
+ /**
+ * Configures the floating action button, clipping it to a circle and setting its translation z
+ *
+ * @param fabView the float action button's view
+ * @param res the resources file
+ */
+ public static void setupFloatingActionButton(View fabView, Resources res) {
+ if (CompatUtils.isLollipopCompatible()) {
+ fabView.setOutlineProvider(OVAL_OUTLINE_PROVIDER);
+ fabView.setTranslationZ(
+ res.getDimensionPixelSize(R.dimen.floating_action_button_translation_z));
+ }
+ }
+
+ /**
+ * Adds padding to the bottom of the given {@link ListView} so that the floating action button
+ * does not obscure any content.
+ *
+ * @param listView to add the padding to
+ * @param res valid resources object
+ */
+ public static void addBottomPaddingToListViewForFab(ListView listView, Resources res) {
+ final int fabPadding =
+ res.getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding);
+ listView.setPaddingRelative(
+ listView.getPaddingStart(),
+ listView.getPaddingTop(),
+ listView.getPaddingEnd(),
+ listView.getPaddingBottom() + fabPadding);
+ listView.setClipToPadding(false);
+ }
+}
diff --git a/java/com/android/contacts/common/util/MaterialColorMapUtils.java b/java/com/android/contacts/common/util/MaterialColorMapUtils.java
new file mode 100644
index 000000000..a2d9847ec
--- /dev/null
+++ b/java/com/android/contacts/common/util/MaterialColorMapUtils.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Trace;
+import com.android.contacts.common.R;
+
+public class MaterialColorMapUtils {
+
+ private final TypedArray sPrimaryColors;
+ private final TypedArray sSecondaryColors;
+
+ public MaterialColorMapUtils(Resources resources) {
+ sPrimaryColors =
+ resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors);
+ sSecondaryColors =
+ resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors_dark);
+ }
+
+ public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) {
+ final int primaryColor = resources.getColor(R.color.quickcontact_default_photo_tint_color);
+ final int secondaryColor =
+ resources.getColor(R.color.quickcontact_default_photo_tint_color_dark);
+ return new MaterialPalette(primaryColor, secondaryColor);
+ }
+
+ /**
+ * Returns the hue component of a color int.
+ *
+ * @return A value between 0.0f and 1.0f
+ */
+ public static float hue(int color) {
+ int r = (color >> 16) & 0xFF;
+ int g = (color >> 8) & 0xFF;
+ int b = color & 0xFF;
+
+ int V = Math.max(b, Math.max(r, g));
+ int temp = Math.min(b, Math.min(r, g));
+
+ float H;
+
+ if (V == temp) {
+ H = 0;
+ } else {
+ final float vtemp = V - temp;
+ final float cr = (V - r) / vtemp;
+ final float cg = (V - g) / vtemp;
+ final float cb = (V - b) / vtemp;
+
+ if (r == V) {
+ H = cb - cg;
+ } else if (g == V) {
+ H = 2 + cr - cb;
+ } else {
+ H = 4 + cg - cr;
+ }
+
+ H /= 6.f;
+ if (H < 0) {
+ H++;
+ }
+ }
+
+ return H;
+ }
+
+ /**
+ * Return primary and secondary colors from the Material color palette that are similar to {@param
+ * color}.
+ */
+ public MaterialPalette calculatePrimaryAndSecondaryColor(int color) {
+ Trace.beginSection("calculatePrimaryAndSecondaryColor");
+
+ final float colorHue = hue(color);
+ float minimumDistance = Float.MAX_VALUE;
+ int indexBestMatch = 0;
+ for (int i = 0; i < sPrimaryColors.length(); i++) {
+ final int primaryColor = sPrimaryColors.getColor(i, 0);
+ final float comparedHue = hue(primaryColor);
+ // No need to be perceptually accurate when calculating color distances since
+ // we are only mapping to 15 colors. Being slightly inaccurate isn't going to change
+ // the mapping very often.
+ final float distance = Math.abs(comparedHue - colorHue);
+ if (distance < minimumDistance) {
+ minimumDistance = distance;
+ indexBestMatch = i;
+ }
+ }
+
+ Trace.endSection();
+ return new MaterialPalette(
+ sPrimaryColors.getColor(indexBestMatch, 0), sSecondaryColors.getColor(indexBestMatch, 0));
+ }
+
+ public static class MaterialPalette implements Parcelable {
+
+ public static final Creator<MaterialPalette> CREATOR =
+ new Creator<MaterialPalette>() {
+ @Override
+ public MaterialPalette createFromParcel(Parcel in) {
+ return new MaterialPalette(in);
+ }
+
+ @Override
+ public MaterialPalette[] newArray(int size) {
+ return new MaterialPalette[size];
+ }
+ };
+ public final int mPrimaryColor;
+ public final int mSecondaryColor;
+
+ public MaterialPalette(int primaryColor, int secondaryColor) {
+ mPrimaryColor = primaryColor;
+ mSecondaryColor = secondaryColor;
+ }
+
+ private MaterialPalette(Parcel in) {
+ mPrimaryColor = in.readInt();
+ mSecondaryColor = in.readInt();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ MaterialPalette other = (MaterialPalette) obj;
+ if (mPrimaryColor != other.mPrimaryColor) {
+ return false;
+ }
+ if (mSecondaryColor != other.mSecondaryColor) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mPrimaryColor;
+ result = prime * result + mSecondaryColor;
+ return result;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPrimaryColor);
+ dest.writeInt(mSecondaryColor);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/NameConverter.java b/java/com/android/contacts/common/util/NameConverter.java
new file mode 100644
index 000000000..ae3275d14
--- /dev/null
+++ b/java/com/android/contacts/common/util/NameConverter.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+package com.android.contacts.common.util;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Utility class for converting between a display name and structured name (and vice-versa), via
+ * calls to the contact provider.
+ */
+public class NameConverter {
+
+ /** The array of fields that comprise a structured name. */
+ public static final String[] STRUCTURED_NAME_FIELDS =
+ new String[] {
+ StructuredName.PREFIX,
+ StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX
+ };
+
+ /**
+ * Converts the given structured name (provided as a map from {@link StructuredName} fields to
+ * corresponding values) into a display name string.
+ *
+ * <p>Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread. See ContactsProvider2.completeName()
+ * for the underlying method call.
+ *
+ * @param context Activity context.
+ * @param structuredName The structured name map to convert.
+ * @return The display name computed from the structured name map.
+ */
+ public static String structuredNameToDisplayName(
+ Context context, Map<String, String> structuredName) {
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+ for (String key : STRUCTURED_NAME_FIELDS) {
+ if (structuredName.containsKey(key)) {
+ appendQueryParameter(builder, key, structuredName.get(key));
+ }
+ }
+ return fetchDisplayName(context, builder.build());
+ }
+
+ /**
+ * Converts the given structured name (provided as ContentValues) into a display name string.
+ *
+ * @param context Activity context.
+ * @param values The content values containing values comprising the structured name.
+ */
+ public static String structuredNameToDisplayName(Context context, ContentValues values) {
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+ for (String key : STRUCTURED_NAME_FIELDS) {
+ if (values.containsKey(key)) {
+ appendQueryParameter(builder, key, values.getAsString(key));
+ }
+ }
+ return fetchDisplayName(context, builder.build());
+ }
+
+ /** Helper method for fetching the display name via the given URI. */
+ private static String fetchDisplayName(Context context, Uri uri) {
+ String displayName = null;
+ Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ uri,
+ new String[] {
+ StructuredName.DISPLAY_NAME,
+ },
+ null,
+ null,
+ null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ displayName = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return displayName;
+ }
+
+ /**
+ * Converts the given display name string into a structured name (as a map from {@link
+ * StructuredName} fields to corresponding values).
+ *
+ * <p>Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread.
+ *
+ * @param context Activity context.
+ * @param displayName The display name to convert.
+ * @return The structured name map computed from the display name.
+ */
+ public static Map<String, String> displayNameToStructuredName(
+ Context context, String displayName) {
+ Map<String, String> structuredName = new TreeMap<String, String>();
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+
+ appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName);
+ Cursor cursor =
+ context
+ .getContentResolver()
+ .query(builder.build(), STRUCTURED_NAME_FIELDS, null, null, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) {
+ structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return structuredName;
+ }
+
+ /**
+ * Converts the given display name string into a structured name (inserting the structured values
+ * into a new or existing ContentValues object).
+ *
+ * <p>Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread.
+ *
+ * @param context Activity context.
+ * @param displayName The display name to convert.
+ * @param contentValues The content values object to place the structured name values into. If
+ * null, a new one will be created and returned.
+ * @return The ContentValues object containing the structured name fields derived from the display
+ * name.
+ */
+ public static ContentValues displayNameToStructuredName(
+ Context context, String displayName, ContentValues contentValues) {
+ if (contentValues == null) {
+ contentValues = new ContentValues();
+ }
+ Map<String, String> mapValues = displayNameToStructuredName(context, displayName);
+ for (String key : mapValues.keySet()) {
+ contentValues.put(key, mapValues.get(key));
+ }
+ return contentValues;
+ }
+
+ private static void appendQueryParameter(Builder builder, String field, String value) {
+ if (!TextUtils.isEmpty(value)) {
+ builder.appendQueryParameter(field, value);
+ }
+ }
+
+ /**
+ * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues. Parsed
+ * data should be {@link StructuredName#PHONETIC_FAMILY_NAME}, {@link
+ * StructuredName#PHONETIC_MIDDLE_NAME}, and {@link StructuredName#PHONETIC_GIVEN_NAME}. If this
+ * method cannot parse given phoneticName, null values will be stored.
+ *
+ * @param phoneticName Phonetic name to be parsed
+ * @param values ContentValues to be used for storing data. If null, new instance will be created.
+ * @return ContentValues with parsed data. Those data can be null.
+ */
+ public static StructuredNameDataItem parsePhoneticName(
+ String phoneticName, StructuredNameDataItem item) {
+ String family = null;
+ String middle = null;
+ String given = null;
+
+ if (!TextUtils.isEmpty(phoneticName)) {
+ String[] strings = phoneticName.split(" ", 3);
+ switch (strings.length) {
+ case 1:
+ family = strings[0];
+ break;
+ case 2:
+ family = strings[0];
+ given = strings[1];
+ break;
+ case 3:
+ family = strings[0];
+ middle = strings[1];
+ given = strings[2];
+ break;
+ }
+ }
+
+ if (item == null) {
+ item = new StructuredNameDataItem();
+ }
+ item.setPhoneticFamilyName(family);
+ item.setPhoneticMiddleName(middle);
+ item.setPhoneticGivenName(given);
+ return item;
+ }
+
+ /** Constructs and returns a phonetic full name from given parts. */
+ public static String buildPhoneticName(String family, String middle, String given) {
+ if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle) || !TextUtils.isEmpty(given)) {
+ StringBuilder sb = new StringBuilder();
+ if (!TextUtils.isEmpty(family)) {
+ sb.append(family.trim()).append(' ');
+ }
+ if (!TextUtils.isEmpty(middle)) {
+ sb.append(middle.trim()).append(' ');
+ }
+ if (!TextUtils.isEmpty(given)) {
+ sb.append(given.trim()).append(' ');
+ }
+ sb.setLength(sb.length() - 1); // Yank the last space
+ return sb.toString();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/SearchUtil.java b/java/com/android/contacts/common/util/SearchUtil.java
new file mode 100644
index 000000000..314d565b2
--- /dev/null
+++ b/java/com/android/contacts/common/util/SearchUtil.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.support.annotation.VisibleForTesting;
+
+/** Methods related to search. */
+public class SearchUtil {
+
+ /**
+ * Given a string with lines delimited with '\n', finds the matching line to the given substring.
+ *
+ * @param contents The string to search.
+ * @param substring The substring to search for.
+ * @return A MatchedLine object containing the matching line and the startIndex of the substring
+ * match within that line.
+ */
+ public static MatchedLine findMatchingLine(String contents, String substring) {
+ final MatchedLine matched = new MatchedLine();
+
+ // Snippet may contain multiple lines separated by "\n".
+ // Locate the lines of the content that contain the substring.
+ final int index = SearchUtil.contains(contents, substring);
+ if (index != -1) {
+ // Match found. Find the corresponding line.
+ int start = index - 1;
+ while (start > -1) {
+ if (contents.charAt(start) == '\n') {
+ break;
+ }
+ start--;
+ }
+ int end = index + 1;
+ while (end < contents.length()) {
+ if (contents.charAt(end) == '\n') {
+ break;
+ }
+ end++;
+ }
+ matched.line = contents.substring(start + 1, end);
+ matched.startIndex = index - (start + 1);
+ }
+ return matched;
+ }
+
+ /**
+ * Similar to String.contains() with two main differences:
+ *
+ * <p>1) Only searches token prefixes. A token is defined as any combination of letters or
+ * numbers.
+ *
+ * <p>2) Returns the starting index where the substring is found.
+ *
+ * @param value The string to search.
+ * @param substring The substring to look for.
+ * @return The starting index where the substring is found. {@literal -1} if substring is not
+ * found in value.
+ */
+ @VisibleForTesting
+ static int contains(String value, String substring) {
+ if (value.length() < substring.length()) {
+ return -1;
+ }
+
+ // i18n support
+ // Generate the code points for the substring once.
+ // There will be a maximum of substring.length code points. But may be fewer.
+ // Since the array length is not an accurate size, we need to keep a separate variable.
+ final int[] substringCodePoints = new int[substring.length()];
+ int substringLength = 0; // may not equal substring.length()!!
+ for (int i = 0; i < substring.length(); ) {
+ final int codePoint = Character.codePointAt(substring, i);
+ substringCodePoints[substringLength] = codePoint;
+ substringLength++;
+ i += Character.charCount(codePoint);
+ }
+
+ for (int i = 0; i < value.length(); i = findNextTokenStart(value, i)) {
+ int numMatch = 0;
+ for (int j = i; j < value.length() && numMatch < substringLength; ++numMatch) {
+ int valueCp = Character.toLowerCase(value.codePointAt(j));
+ int substringCp = substringCodePoints[numMatch];
+ if (valueCp != substringCp) {
+ break;
+ }
+ j += Character.charCount(valueCp);
+ }
+ if (numMatch == substringLength) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Find the start of the next token. A token is composed of letters and numbers. Any other
+ * character are considered delimiters.
+ *
+ * @param line The string to search for the next token.
+ * @param startIndex The index to start searching. 0 based indexing.
+ * @return The index for the start of the next token. line.length() if next token not found.
+ */
+ @VisibleForTesting
+ static int findNextTokenStart(String line, int startIndex) {
+ int index = startIndex;
+
+ // If already in token, eat remainder of token.
+ while (index <= line.length()) {
+ if (index == line.length()) {
+ // No more tokens.
+ return index;
+ }
+ final int codePoint = line.codePointAt(index);
+ if (!Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ index += Character.charCount(codePoint);
+ }
+
+ // Out of token, eat all consecutive delimiters.
+ while (index <= line.length()) {
+ if (index == line.length()) {
+ return index;
+ }
+ final int codePoint = line.codePointAt(index);
+ if (Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ index += Character.charCount(codePoint);
+ }
+
+ return index;
+ }
+
+ /**
+ * Anything other than letter and numbers are considered delimiters. Remove start and end
+ * delimiters since they are not relevant to search.
+ *
+ * @param query The query string to clean.
+ * @return The cleaned query. Empty string if all characters are cleaned out.
+ */
+ public static String cleanStartAndEndOfSearchQuery(String query) {
+ int start = 0;
+ while (start < query.length()) {
+ int codePoint = query.codePointAt(start);
+ if (Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ start += Character.charCount(codePoint);
+ }
+
+ if (start == query.length()) {
+ // All characters are delimiters.
+ return "";
+ }
+
+ int end = query.length() - 1;
+ while (end > -1) {
+ if (Character.isLowSurrogate(query.charAt(end))) {
+ // Assume valid i18n string. There should be a matching high surrogate before it.
+ end--;
+ }
+ int codePoint = query.codePointAt(end);
+ if (Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ end--;
+ }
+
+ // end is a letter or digit.
+ return query.substring(start, end + 1);
+ }
+
+ public static class MatchedLine {
+
+ public int startIndex = -1;
+ public String line;
+
+ @Override
+ public String toString() {
+ return "MatchedLine{" + "line='" + line + '\'' + ", startIndex=" + startIndex + '}';
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/StopWatch.java b/java/com/android/contacts/common/util/StopWatch.java
new file mode 100644
index 000000000..b944b9867
--- /dev/null
+++ b/java/com/android/contacts/common/util/StopWatch.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.util.Log;
+import java.util.ArrayList;
+
+/** A {@link StopWatch} records start, laps and stop, and print them to logcat. */
+public class StopWatch {
+
+ private final String mLabel;
+
+ private final ArrayList<Long> mTimes = new ArrayList<>();
+ private final ArrayList<String> mLapLabels = new ArrayList<>();
+
+ private StopWatch(String label) {
+ mLabel = label;
+ lap("");
+ }
+
+ /** Create a new instance and start it. */
+ public static StopWatch start(String label) {
+ return new StopWatch(label);
+ }
+
+ /** Return a dummy instance that does no operations. */
+ public static StopWatch getNullStopWatch() {
+ return NullStopWatch.INSTANCE;
+ }
+
+ /** Record a lap. */
+ public void lap(String lapLabel) {
+ mTimes.add(System.currentTimeMillis());
+ mLapLabels.add(lapLabel);
+ }
+
+ /** Stop it and log the result, if the total time >= {@code timeThresholdToLog}. */
+ public void stopAndLog(String TAG, int timeThresholdToLog) {
+
+ lap("");
+
+ final long start = mTimes.get(0);
+ final long stop = mTimes.get(mTimes.size() - 1);
+
+ final long total = stop - start;
+ if (total < timeThresholdToLog) {
+ return;
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(mLabel);
+ sb.append(",");
+ sb.append(total);
+ sb.append(": ");
+
+ long last = start;
+ for (int i = 1; i < mTimes.size(); i++) {
+ final long current = mTimes.get(i);
+ sb.append(mLapLabels.get(i));
+ sb.append(",");
+ sb.append((current - last));
+ sb.append(" ");
+ last = current;
+ }
+ Log.v(TAG, sb.toString());
+ }
+
+ private static class NullStopWatch extends StopWatch {
+
+ public static final NullStopWatch INSTANCE = new NullStopWatch();
+
+ public NullStopWatch() {
+ super(null);
+ }
+
+ @Override
+ public void lap(String lapLabel) {
+ // noop
+ }
+
+ @Override
+ public void stopAndLog(String TAG, int timeThresholdToLog) {
+ // noop
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/TelephonyManagerUtils.java b/java/com/android/contacts/common/util/TelephonyManagerUtils.java
new file mode 100644
index 000000000..b664268ca
--- /dev/null
+++ b/java/com/android/contacts/common/util/TelephonyManagerUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 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.
+ */
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.telephony.TelephonyManager;
+
+/** This class provides several TelephonyManager util functions. */
+public class TelephonyManagerUtils {
+
+ /**
+ * Gets the voicemail tag from Telephony Manager.
+ *
+ * @param context Current application context
+ * @return Voicemail tag, the alphabetic identifier associated with the voice mail number.
+ */
+ public static String getVoiceMailAlphaTag(Context context) {
+ final TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ final String voiceMailLabel = telephonyManager.getVoiceMailAlphaTag();
+ return voiceMailLabel;
+ }
+
+ /**
+ * @param context Current application context.
+ * @return True if there is a subscription which supports video calls. False otherwise.
+ */
+ public static boolean hasVideoCallSubscription(Context context) {
+ // TODO: Check the telephony manager's subscriptions to see if any support video calls.
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/util/TrafficStatsTags.java b/java/com/android/contacts/common/util/TrafficStatsTags.java
new file mode 100644
index 000000000..b0e7fb583
--- /dev/null
+++ b/java/com/android/contacts/common/util/TrafficStatsTags.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2015 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.
+ */
+package com.android.contacts.common.util;
+
+public class TrafficStatsTags {
+
+ public static final int CONTACT_PHOTO_DOWNLOAD_TAG = 0x0001;
+ public static final int TAG_MAX = 0x9999;
+}
diff --git a/java/com/android/contacts/common/util/UriUtils.java b/java/com/android/contacts/common/util/UriUtils.java
new file mode 100644
index 000000000..4690942ba
--- /dev/null
+++ b/java/com/android/contacts/common/util/UriUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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.
+ */
+
+package com.android.contacts.common.util;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+import java.util.List;
+
+/** Utility methods for dealing with URIs. */
+public class UriUtils {
+
+ /** Static helper, not instantiable. */
+ private UriUtils() {}
+
+ /** Checks whether two URI are equal, taking care of the case where either is null. */
+ public static boolean areEqual(Uri uri1, Uri uri2) {
+ if (uri1 == null && uri2 == null) {
+ return true;
+ }
+ if (uri1 == null || uri2 == null) {
+ return false;
+ }
+ return uri1.equals(uri2);
+ }
+
+ /** Parses a string into a URI and returns null if the given string is null. */
+ public static Uri parseUriOrNull(String uriString) {
+ if (uriString == null) {
+ return null;
+ }
+ return Uri.parse(uriString);
+ }
+
+ /** Converts a URI into a string, returns null if the given URI is null. */
+ public static String uriToString(Uri uri) {
+ return uri == null ? null : uri.toString();
+ }
+
+ public static boolean isEncodedContactUri(Uri uri) {
+ if (uri == null) {
+ return false;
+ }
+ final String lastPathSegment = uri.getLastPathSegment();
+ if (lastPathSegment == null) {
+ return false;
+ }
+ return lastPathSegment.equals(Constants.LOOKUP_URI_ENCODED);
+ }
+
+ /**
+ * @return {@code uri} as-is if the authority is of contacts provider. Otherwise or {@code uri} is
+ * null, return null otherwise
+ */
+ public static Uri nullForNonContactsUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ return ContactsContract.AUTHORITY.equals(uri.getAuthority()) ? uri : null;
+ }
+
+ /** Parses the given URI to determine the original lookup key of the contact. */
+ public static String getLookupKeyFromUri(Uri lookupUri) {
+ // Would be nice to be able to persist the lookup key somehow to avoid having to parse
+ // the uri entirely just to retrieve the lookup key, but every uri is already parsed
+ // once anyway to check if it is an encoded JSON uri, so this has negligible effect
+ // on performance.
+ if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) {
+ final List<String> segments = lookupUri.getPathSegments();
+ // This returns the third path segment of the uri, where the lookup key is located.
+ // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ return (segments.size() < 3) ? null : Uri.encode(segments.get(2));
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java
new file mode 100644
index 000000000..2988a5a58
--- /dev/null
+++ b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+import com.android.dialer.util.TouchPointManager;
+
+/**
+ * Linear layout for an activity that listens to all touch events on the screen and saves the touch
+ * point. Typically touch events are handled by child views--this class intercepts those touch
+ * events before passing them on to the child.
+ */
+public class ActivityTouchLinearLayout extends LinearLayout {
+
+ public ActivityTouchLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/contacts/common/widget/FloatingActionButtonController.java b/java/com/android/contacts/common/widget/FloatingActionButtonController.java
new file mode 100644
index 000000000..f03129779
--- /dev/null
+++ b/java/com/android/contacts/common/widget/FloatingActionButtonController.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+package com.android.contacts.common.widget;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ImageButton;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.FabUtil;
+import com.android.dialer.animation.AnimUtils;
+
+/** Controls the movement and appearance of the FAB (Floating Action Button). */
+public class FloatingActionButtonController {
+
+ public static final int ALIGN_MIDDLE = 0;
+ public static final int ALIGN_QUARTER_END = 1;
+ public static final int ALIGN_END = 2;
+
+ private static final int FAB_SCALE_IN_DURATION = 266;
+ private static final int FAB_SCALE_IN_FADE_IN_DELAY = 100;
+ private static final int FAB_ICON_FADE_OUT_DURATION = 66;
+
+ private final int mAnimationDuration;
+ private final int mFloatingActionButtonWidth;
+ private final int mFloatingActionButtonMarginRight;
+ private final View mFloatingActionButtonContainer;
+ private final ImageButton mFloatingActionButton;
+ private final Interpolator mFabInterpolator;
+ private int mScreenWidth;
+
+ public FloatingActionButtonController(Activity activity, View container, ImageButton button) {
+ Resources resources = activity.getResources();
+ mFabInterpolator =
+ AnimationUtils.loadInterpolator(activity, android.R.interpolator.fast_out_slow_in);
+ mFloatingActionButtonWidth =
+ resources.getDimensionPixelSize(R.dimen.floating_action_button_width);
+ mFloatingActionButtonMarginRight =
+ resources.getDimensionPixelOffset(R.dimen.floating_action_button_margin_right);
+ mAnimationDuration = resources.getInteger(R.integer.floating_action_button_animation_duration);
+ mFloatingActionButtonContainer = container;
+ mFloatingActionButton = button;
+ FabUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources);
+ }
+
+ /**
+ * Passes the screen width into the class. Necessary for translation calculations. Should be
+ * called as soon as parent View width is available.
+ *
+ * @param screenWidth The width of the screen in pixels.
+ */
+ public void setScreenWidth(int screenWidth) {
+ mScreenWidth = screenWidth;
+ }
+
+ public boolean isVisible() {
+ return mFloatingActionButtonContainer.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * Sets FAB as View.VISIBLE or View.GONE.
+ *
+ * @param visible Whether or not to make the container visible.
+ */
+ public void setVisible(boolean visible) {
+ mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ public void changeIcon(Drawable icon, String description) {
+ if (mFloatingActionButton.getDrawable() != icon
+ || !mFloatingActionButton.getContentDescription().equals(description)) {
+ mFloatingActionButton.setImageDrawable(icon);
+ mFloatingActionButton.setContentDescription(description);
+ }
+ }
+
+ /**
+ * Updates the FAB location (middle to right position) as the PageView scrolls.
+ *
+ * @param positionOffset A fraction used to calculate position of the FAB during page scroll.
+ */
+ public void onPageScrolled(float positionOffset) {
+ // As the page is scrolling, if we're on the first tab, update the FAB position so it
+ // moves along with it.
+ mFloatingActionButtonContainer.setTranslationX(
+ (int) (positionOffset * getTranslationXForAlignment(ALIGN_END)));
+ }
+
+ /**
+ * Aligns the FAB to the described location
+ *
+ * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+ * @param animate Whether or not to animate the transition.
+ */
+ public void align(int align, boolean animate) {
+ align(align, 0 /*offsetX */, 0 /* offsetY */, animate);
+ }
+
+ /**
+ * Aligns the FAB to the described location plus specified additional offsets.
+ *
+ * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+ * @param offsetX Additional offsetX to translate by.
+ * @param offsetY Additional offsetY to translate by.
+ * @param animate Whether or not to animate the transition.
+ */
+ public void align(int align, int offsetX, int offsetY, boolean animate) {
+ if (mScreenWidth == 0) {
+ return;
+ }
+
+ int translationX = getTranslationXForAlignment(align);
+
+ // Skip animation if container is not shown; animation causes container to show again.
+ if (animate && mFloatingActionButtonContainer.isShown()) {
+ mFloatingActionButtonContainer
+ .animate()
+ .translationX(translationX + offsetX)
+ .translationY(offsetY)
+ .setInterpolator(mFabInterpolator)
+ .setDuration(mAnimationDuration)
+ .start();
+ } else {
+ mFloatingActionButtonContainer.setTranslationX(translationX + offsetX);
+ mFloatingActionButtonContainer.setTranslationY(offsetY);
+ }
+ }
+
+ /**
+ * Resizes width and height of the floating action bar container.
+ *
+ * @param dimension The new dimensions for the width and height.
+ * @param animate Whether to animate this change.
+ */
+ public void resize(int dimension, boolean animate) {
+ if (animate) {
+ AnimUtils.changeDimensions(mFloatingActionButtonContainer, dimension, dimension);
+ } else {
+ mFloatingActionButtonContainer.getLayoutParams().width = dimension;
+ mFloatingActionButtonContainer.getLayoutParams().height = dimension;
+ mFloatingActionButtonContainer.requestLayout();
+ }
+ }
+
+ /**
+ * Scales the floating action button from no height and width to its actual dimensions. This is an
+ * animation for showing the floating action button.
+ *
+ * @param delayMs The delay for the effect, in milliseconds.
+ */
+ public void scaleIn(int delayMs) {
+ setVisible(true);
+ AnimUtils.scaleIn(mFloatingActionButtonContainer, FAB_SCALE_IN_DURATION, delayMs);
+ AnimUtils.fadeIn(
+ mFloatingActionButton, FAB_SCALE_IN_DURATION, delayMs + FAB_SCALE_IN_FADE_IN_DELAY, null);
+ }
+
+ /** Immediately remove the affects of the last call to {@link #scaleOut}. */
+ public void resetIn() {
+ mFloatingActionButton.setAlpha(1f);
+ mFloatingActionButton.setVisibility(View.VISIBLE);
+ mFloatingActionButtonContainer.setScaleX(1);
+ mFloatingActionButtonContainer.setScaleY(1);
+ }
+
+ /**
+ * Scales the floating action button from its actual dimensions to no height and width. This is an
+ * animation for hiding the floating action button.
+ */
+ public void scaleOut() {
+ AnimUtils.scaleOut(mFloatingActionButtonContainer, mAnimationDuration);
+ // Fade out the icon faster than the scale out animation, so that the icon scaling is less
+ // obvious. We don't want it to scale, but the resizing the container is not as performant.
+ AnimUtils.fadeOut(mFloatingActionButton, FAB_ICON_FADE_OUT_DURATION, null);
+ }
+
+ /**
+ * Calculates the X offset of the FAB to the given alignment, adjusted for whether or not the view
+ * is in RTL mode.
+ *
+ * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+ * @return The translationX for the given alignment.
+ */
+ public int getTranslationXForAlignment(int align) {
+ int result = 0;
+ switch (align) {
+ case ALIGN_MIDDLE:
+ // Moves the FAB to exactly center screen.
+ return 0;
+ case ALIGN_QUARTER_END:
+ // Moves the FAB a quarter of the screen width.
+ result = mScreenWidth / 4;
+ break;
+ case ALIGN_END:
+ // Moves the FAB half the screen width. Same as aligning right with a marginRight.
+ result =
+ mScreenWidth / 2 - mFloatingActionButtonWidth / 2 - mFloatingActionButtonMarginRight;
+ break;
+ }
+ if (isLayoutRtl()) {
+ result *= -1;
+ }
+ return result;
+ }
+
+ private boolean isLayoutRtl() {
+ return mFloatingActionButtonContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+}
diff --git a/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java
new file mode 100644
index 000000000..d84d8f757
--- /dev/null
+++ b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 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.
+ */
+
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Custom {@link ImageView} that improves layouting performance.
+ *
+ * <p>This improves the performance by not passing requestLayout() to its parent, taking advantage
+ * of knowing that image size won't change once set.
+ */
+public class LayoutSuppressingImageView extends ImageView {
+
+ public LayoutSuppressingImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void requestLayout() {
+ forceLayout();
+ }
+}
diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
new file mode 100644
index 000000000..63f8ca580
--- /dev/null
+++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2014 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
+ */
+
+package com.android.contacts.common.widget;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneAccountCompat;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dialog that allows the user to select a phone accounts for a given action. Optionally provides
+ * the choice to set the phone account as default.
+ */
+public class SelectPhoneAccountDialogFragment extends DialogFragment {
+
+ private static final String ARG_TITLE_RES_ID = "title_res_id";
+ private static final String ARG_CAN_SET_DEFAULT = "can_set_default";
+ private static final String ARG_ACCOUNT_HANDLES = "account_handles";
+ private static final String ARG_IS_DEFAULT_CHECKED = "is_default_checked";
+ private static final String ARG_LISTENER = "listener";
+ private static final String ARG_CALL_ID = "call_id";
+
+ private int mTitleResId;
+ private boolean mCanSetDefault;
+ private List<PhoneAccountHandle> mAccountHandles;
+ private boolean mIsSelected;
+ private boolean mIsDefaultChecked;
+ private SelectPhoneAccountListener mListener;
+
+ public SelectPhoneAccountDialogFragment() {}
+
+ /**
+ * Create new fragment instance with default title and no option to set as default.
+ *
+ * @param accountHandles The {@code PhoneAccountHandle}s available to select from.
+ * @param listener The listener for the results of the account selection.
+ */
+ public static SelectPhoneAccountDialogFragment newInstance(
+ List<PhoneAccountHandle> accountHandles,
+ SelectPhoneAccountListener listener,
+ @Nullable String callId) {
+ return newInstance(
+ R.string.select_account_dialog_title, false, accountHandles, listener, callId);
+ }
+
+ /**
+ * Create new fragment instance. This method also allows specifying a custom title and "set
+ * default" checkbox.
+ *
+ * @param titleResId The resource ID for the string to use in the title of the dialog.
+ * @param canSetDefault {@code true} if the dialog should include an option to set the selection
+ * as the default. False otherwise.
+ * @param accountHandles The {@code PhoneAccountHandle}s available to select from.
+ * @param listener The listener for the results of the account selection.
+ */
+ public static SelectPhoneAccountDialogFragment newInstance(
+ int titleResId,
+ boolean canSetDefault,
+ List<PhoneAccountHandle> accountHandles,
+ SelectPhoneAccountListener listener,
+ @Nullable String callId) {
+ ArrayList<PhoneAccountHandle> accountHandlesCopy = new ArrayList<>();
+ if (accountHandles != null) {
+ accountHandlesCopy.addAll(accountHandles);
+ }
+ SelectPhoneAccountDialogFragment fragment = new SelectPhoneAccountDialogFragment();
+ final Bundle args = new Bundle();
+ args.putInt(ARG_TITLE_RES_ID, titleResId);
+ args.putBoolean(ARG_CAN_SET_DEFAULT, canSetDefault);
+ args.putParcelableArrayList(ARG_ACCOUNT_HANDLES, accountHandlesCopy);
+ args.putParcelable(ARG_LISTENER, listener);
+ args.putString(ARG_CALL_ID, callId);
+ fragment.setArguments(args);
+ fragment.setListener(listener);
+ return fragment;
+ }
+
+ public void setListener(SelectPhoneAccountListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(ARG_IS_DEFAULT_CHECKED, mIsDefaultChecked);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+ mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
+ mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES);
+ mListener = getArguments().getParcelable(ARG_LISTENER);
+ if (savedInstanceState != null) {
+ mIsDefaultChecked = savedInstanceState.getBoolean(ARG_IS_DEFAULT_CHECKED);
+ }
+ mIsSelected = false;
+
+ final DialogInterface.OnClickListener selectionListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mIsSelected = true;
+ PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which);
+ Bundle result = new Bundle();
+ result.putParcelable(
+ SelectPhoneAccountListener.EXTRA_SELECTED_ACCOUNT_HANDLE, selectedAccountHandle);
+ result.putBoolean(SelectPhoneAccountListener.EXTRA_SET_DEFAULT, mIsDefaultChecked);
+ result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId());
+ if (mListener != null) {
+ mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_SELECTED, result);
+ }
+ }
+ };
+
+ final CompoundButton.OnCheckedChangeListener checkListener =
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton check, boolean isChecked) {
+ mIsDefaultChecked = isChecked;
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ ListAdapter selectAccountListAdapter =
+ new SelectAccountListAdapter(
+ builder.getContext(), R.layout.select_account_list_item, mAccountHandles);
+
+ AlertDialog dialog =
+ builder
+ .setTitle(mTitleResId)
+ .setAdapter(selectAccountListAdapter, selectionListener)
+ .create();
+
+ if (mCanSetDefault) {
+ // Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog)
+ @SuppressLint("InflateParams")
+ LinearLayout checkboxLayout =
+ (LinearLayout)
+ LayoutInflater.from(builder.getContext())
+ .inflate(R.layout.default_account_checkbox, null);
+
+ CheckBox cb = (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view);
+ cb.setOnCheckedChangeListener(checkListener);
+ cb.setChecked(mIsDefaultChecked);
+
+ dialog.getListView().addFooterView(checkboxLayout);
+ }
+
+ return dialog;
+ }
+
+ @Override
+ public void onStop() {
+ if (!mIsSelected && mListener != null) {
+ Bundle result = new Bundle();
+ result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId());
+ mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result);
+ }
+ super.onStop();
+ }
+
+ @Nullable
+ private String getCallId() {
+ return getArguments().getString(ARG_CALL_ID);
+ }
+
+ public static class SelectPhoneAccountListener extends ResultReceiver {
+
+ static final int RESULT_SELECTED = 1;
+ static final int RESULT_DISMISSED = 2;
+
+ static final String EXTRA_SELECTED_ACCOUNT_HANDLE = "extra_selected_account_handle";
+ static final String EXTRA_SET_DEFAULT = "extra_set_default";
+ static final String EXTRA_CALL_ID = "extra_call_id";
+
+ public SelectPhoneAccountListener() {
+ super(new Handler());
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == RESULT_SELECTED) {
+ onPhoneAccountSelected(
+ resultData.getParcelable(EXTRA_SELECTED_ACCOUNT_HANDLE),
+ resultData.getBoolean(EXTRA_SET_DEFAULT),
+ resultData.getString(EXTRA_CALL_ID));
+ } else if (resultCode == RESULT_DISMISSED) {
+ onDialogDismissed(resultData.getString(EXTRA_CALL_ID));
+ }
+ }
+
+ public void onPhoneAccountSelected(
+ PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {}
+
+ public void onDialogDismissed(@Nullable String callId) {}
+ }
+
+ private static class SelectAccountListAdapter extends ArrayAdapter<PhoneAccountHandle> {
+
+ private int mResId;
+
+ public SelectAccountListAdapter(
+ Context context, int resource, List<PhoneAccountHandle> accountHandles) {
+ super(context, resource, accountHandles);
+ mResId = resource;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ View rowView;
+ final ViewHolder holder;
+
+ if (convertView == null) {
+ // Cache views for faster scrolling
+ rowView = inflater.inflate(mResId, null);
+ holder = new ViewHolder();
+ holder.labelTextView = (TextView) rowView.findViewById(R.id.label);
+ holder.numberTextView = (TextView) rowView.findViewById(R.id.number);
+ holder.imageView = (ImageView) rowView.findViewById(R.id.icon);
+ rowView.setTag(holder);
+ } else {
+ rowView = convertView;
+ holder = (ViewHolder) rowView.getTag();
+ }
+
+ PhoneAccountHandle accountHandle = getItem(position);
+ PhoneAccount account =
+ getContext().getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ if (account == null) {
+ return rowView;
+ }
+ holder.labelTextView.setText(account.getLabel());
+ if (account.getAddress() == null
+ || TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) {
+ holder.numberTextView.setVisibility(View.GONE);
+ } else {
+ holder.numberTextView.setVisibility(View.VISIBLE);
+ holder.numberTextView.setText(
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ account.getAddress().getSchemeSpecificPart()));
+ }
+ holder.imageView.setImageDrawable(
+ PhoneAccountCompat.createIconDrawable(account, getContext()));
+ return rowView;
+ }
+
+ private static final class ViewHolder {
+
+ TextView labelTextView;
+ TextView numberTextView;
+ ImageView imageView;
+ }
+ }
+}