From ccca31529c07970e89419fb85a9e8153a5396838 Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Wed, 22 Feb 2017 16:32:36 -0800 Subject: Update dialer sources. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958 --- .../android/contacts/common/AndroidManifest.xml | 39 + java/com/android/contacts/common/Bindings.java | 52 + .../android/contacts/common/ClipboardUtils.java | 55 + java/com/android/contacts/common/Collapser.java | 95 + .../contacts/common/ContactPhotoManager.java | 487 +++++ .../contacts/common/ContactPhotoManagerImpl.java | 1262 +++++++++++++ .../contacts/common/ContactPresenceIconUtil.java | 46 + .../android/contacts/common/ContactStatusUtil.java | 44 + .../contacts/common/ContactTileLoaderFactory.java | 64 + .../com/android/contacts/common/ContactsUtils.java | 265 +++ java/com/android/contacts/common/GeoUtil.java | 55 + .../com/android/contacts/common/GroupMetaData.java | 76 + .../android/contacts/common/MoreContactUtils.java | 251 +++ .../common/bindings/ContactsCommonBindings.java | 25 + .../bindings/ContactsCommonBindingsFactory.java | 24 + .../bindings/ContactsCommonBindingsStub.java | 27 + .../android/contacts/common/compat/CallCompat.java | 45 + .../contacts/common/compat/CallableCompat.java | 36 + .../contacts/common/compat/ContactsCompat.java | 57 + .../contacts/common/compat/DirectoryCompat.java | 51 + .../contacts/common/compat/PhoneAccountCompat.java | 104 ++ .../contacts/common/compat/PhoneCompat.java | 36 + .../common/compat/PhoneNumberUtilsCompat.java | 174 ++ .../common/compat/TelephonyManagerCompat.java | 213 +++ .../compat/telecom/TelecomManagerCompat.java | 302 ++++ .../common/database/ContactUpdateUtils.java | 49 + .../contacts/common/database/EmptyCursor.java | 84 + .../database/NoNullCursorAsyncQueryHandler.java | 73 + .../contacts/common/dialog/CallSubjectDialog.java | 607 +++++++ .../common/dialog/ClearFrequentsDialog.java | 88 + .../common/extensions/PhoneDirectoryExtender.java | 28 + .../extensions/PhoneDirectoryExtenderAccessor.java | 45 + .../extensions/PhoneDirectoryExtenderFactory.java | 27 + .../extensions/PhoneDirectoryExtenderStub.java | 29 + .../contacts/common/format/FormatUtils.java | 181 ++ .../contacts/common/format/TextHighlighter.java | 93 + .../common/format/testing/SpannedTestUtils.java | 85 + .../common/lettertiles/LetterTileDrawable.java | 382 ++++ .../contacts/common/list/AutoScrollListView.java | 125 ++ .../android/contacts/common/list/ContactEntry.java | 57 + .../common/list/ContactEntryListAdapter.java | 742 ++++++++ .../common/list/ContactEntryListFragment.java | 862 +++++++++ .../contacts/common/list/ContactListAdapter.java | 232 +++ .../contacts/common/list/ContactListFilter.java | 297 +++ .../common/list/ContactListFilterController.java | 170 ++ .../contacts/common/list/ContactListItemView.java | 1513 ++++++++++++++++ .../common/list/ContactListPinnedHeaderView.java | 70 + .../contacts/common/list/ContactTileView.java | 171 ++ .../common/list/ContactsSectionIndexer.java | 119 ++ .../common/list/DefaultContactListAdapter.java | 216 +++ .../contacts/common/list/DirectoryListLoader.java | 201 +++ .../contacts/common/list/DirectoryPartition.java | 179 ++ .../contacts/common/list/IndexerListAdapter.java | 214 +++ .../list/OnPhoneNumberPickerActionListener.java | 39 + .../common/list/PhoneNumberListAdapter.java | 583 ++++++ .../common/list/PhoneNumberPickerFragment.java | 402 +++++ .../common/list/PinnedHeaderListAdapter.java | 159 ++ .../contacts/common/list/PinnedHeaderListView.java | 563 ++++++ .../contacts/common/list/ViewPagerTabStrip.java | 109 ++ .../contacts/common/list/ViewPagerTabs.java | 317 ++++ .../contacts/common/location/CountryDetector.java | 221 +++ .../common/location/UpdateCountryService.java | 104 ++ .../contacts/common/model/AccountTypeManager.java | 813 +++++++++ .../contacts/common/model/BuilderWrapper.java | 53 + .../android/contacts/common/model/CPOWrapper.java | 50 + .../com/android/contacts/common/model/Contact.java | 384 ++++ .../contacts/common/model/ContactLoader.java | 998 +++++++++++ .../android/contacts/common/model/RawContact.java | 351 ++++ .../contacts/common/model/account/AccountType.java | 501 ++++++ .../model/account/AccountTypeWithDataSet.java | 103 ++ .../common/model/account/AccountWithDataSet.java | 229 +++ .../common/model/account/BaseAccountType.java | 1890 ++++++++++++++++++++ .../common/model/account/ExchangeAccountType.java | 365 ++++ .../common/model/account/ExternalAccountType.java | 443 +++++ .../common/model/account/FallbackAccountType.java | 77 + .../common/model/account/GoogleAccountType.java | 206 +++ .../common/model/account/SamsungAccountType.java | 235 +++ .../contacts/common/model/dataitem/DataItem.java | 258 +++ .../contacts/common/model/dataitem/DataKind.java | 132 ++ .../common/model/dataitem/EmailDataItem.java | 47 + .../common/model/dataitem/EventDataItem.java | 62 + .../model/dataitem/GroupMembershipDataItem.java | 40 + .../common/model/dataitem/IdentityDataItem.java | 39 + .../contacts/common/model/dataitem/ImDataItem.java | 109 ++ .../common/model/dataitem/NicknameDataItem.java | 39 + .../common/model/dataitem/NoteDataItem.java | 35 + .../model/dataitem/OrganizationDataItem.java | 64 + .../common/model/dataitem/PhoneDataItem.java | 76 + .../common/model/dataitem/PhotoDataItem.java | 39 + .../common/model/dataitem/RelationDataItem.java | 62 + .../common/model/dataitem/SipAddressDataItem.java | 40 + .../model/dataitem/StructuredNameDataItem.java | 100 ++ .../model/dataitem/StructuredPostalDataItem.java | 68 + .../common/model/dataitem/WebsiteDataItem.java | 39 + .../common/preference/ContactsPreferences.java | 269 +++ .../common/preference/DisplayOrderPreference.java | 89 + .../common/preference/SortOrderPreference.java | 89 + .../contacts/common/res/color/popup_menu_color.xml | 20 + .../contacts/common/res/color/tab_text_color.xml | 21 + .../common/res/drawable-hdpi/ic_ab_search.png | Bin 0 -> 1115 bytes .../res/drawable-hdpi/ic_arrow_back_24dp.png | Bin 0 -> 612 bytes .../res/drawable-hdpi/ic_business_white_120dp.png | Bin 0 -> 2477 bytes .../common/res/drawable-hdpi/ic_call_24dp.png | Bin 0 -> 340 bytes .../res/drawable-hdpi/ic_call_note_white_24dp.png | Bin 0 -> 373 bytes .../common/res/drawable-hdpi/ic_close_dk.png | Bin 0 -> 609 bytes .../common/res/drawable-hdpi/ic_create_24dp.png | Bin 0 -> 370 bytes .../res/drawable-hdpi/ic_group_white_24dp.png | Bin 0 -> 389 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 525 bytes .../res/drawable-hdpi/ic_info_outline_24dp.png | Bin 0 -> 485 bytes .../common/res/drawable-hdpi/ic_menu_back.png | Bin 0 -> 799 bytes .../common/res/drawable-hdpi/ic_menu_group_dk.png | Bin 0 -> 1954 bytes .../common/res/drawable-hdpi/ic_menu_group_lt.png | Bin 0 -> 1922 bytes .../res/drawable-hdpi/ic_menu_overflow_lt.png | Bin 0 -> 220 bytes .../common/res/drawable-hdpi/ic_menu_person_dk.png | Bin 0 -> 1439 bytes .../common/res/drawable-hdpi/ic_menu_person_lt.png | Bin 0 -> 1416 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 515 bytes .../common/res/drawable-hdpi/ic_menu_star_dk.png | Bin 0 -> 1438 bytes .../res/drawable-hdpi/ic_menu_star_holo_light.png | Bin 0 -> 1211 bytes .../common/res/drawable-hdpi/ic_menu_star_lt.png | Bin 0 -> 1414 bytes .../common/res/drawable-hdpi/ic_message_24dp.png | Bin 0 -> 167 bytes .../common/res/drawable-hdpi/ic_person_24dp.png | Bin 0 -> 273 bytes .../res/drawable-hdpi/ic_person_add_24dp.png | Bin 0 -> 289 bytes .../common/res/drawable-hdpi/ic_phone_attach.png | Bin 0 -> 828 bytes .../common/res/drawable-hdpi/ic_rx_videocam.png | Bin 0 -> 413 bytes .../common/res/drawable-hdpi/ic_scroll_handle.png | Bin 0 -> 544 bytes .../common/res/drawable-hdpi/ic_tx_videocam.png | Bin 0 -> 370 bytes .../common/res/drawable-hdpi/ic_videocam.png | Bin 0 -> 269 bytes .../res/drawable-hdpi/ic_voicemail_avatar.png | Bin 0 -> 3607 bytes .../res/drawable-hdpi/list_activated_holo.9.png | Bin 0 -> 154 bytes .../res/drawable-hdpi/list_background_holo.9.png | Bin 0 -> 224 bytes .../res/drawable-hdpi/list_focused_holo.9.png | Bin 0 -> 235 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 158 bytes .../drawable-hdpi/list_pressed_holo_light.9.png | Bin 0 -> 159 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 205 bytes .../common/res/drawable-hdpi/list_title_holo.9.png | Bin 0 -> 267 bytes .../drawable-ldrtl-hdpi/list_background_holo.9.png | Bin 0 -> 219 bytes .../drawable-ldrtl-hdpi/list_focused_holo.9.png | Bin 0 -> 234 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 191 bytes .../res/drawable-ldrtl-hdpi/list_title_holo.9.png | Bin 0 -> 258 bytes .../drawable-ldrtl-mdpi/list_background_holo.9.png | Bin 0 -> 178 bytes .../drawable-ldrtl-mdpi/list_focused_holo.9.png | Bin 0 -> 234 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 180 bytes .../res/drawable-ldrtl-mdpi/list_title_holo.9.png | Bin 0 -> 186 bytes .../list_activated_holo.9.png | Bin 0 -> 1666 bytes .../list_activated_holo.9.png | Bin 0 -> 1034 bytes .../list_activated_holo.9.png | Bin 0 -> 2486 bytes .../list_background_holo.9.png | Bin 0 -> 243 bytes .../drawable-ldrtl-xhdpi/list_focused_holo.9.png | Bin 0 -> 234 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 196 bytes .../res/drawable-ldrtl-xhdpi/list_title_holo.9.png | Bin 0 -> 255 bytes .../common/res/drawable-mdpi/ic_ab_search.png | Bin 0 -> 781 bytes .../res/drawable-mdpi/ic_arrow_back_24dp.png | Bin 0 -> 578 bytes .../res/drawable-mdpi/ic_business_white_120dp.png | Bin 0 -> 2040 bytes .../common/res/drawable-mdpi/ic_call_24dp.png | Bin 0 -> 246 bytes .../res/drawable-mdpi/ic_call_note_white_24dp.png | Bin 0 -> 266 bytes .../common/res/drawable-mdpi/ic_close_dk.png | Bin 0 -> 572 bytes .../common/res/drawable-mdpi/ic_create_24dp.png | Bin 0 -> 290 bytes .../res/drawable-mdpi/ic_group_white_24dp.png | Bin 0 -> 297 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 340 bytes .../res/drawable-mdpi/ic_info_outline_24dp.png | Bin 0 -> 320 bytes .../common/res/drawable-mdpi/ic_menu_back.png | Bin 0 -> 607 bytes .../common/res/drawable-mdpi/ic_menu_group_dk.png | Bin 0 -> 1266 bytes .../common/res/drawable-mdpi/ic_menu_group_lt.png | Bin 0 -> 1270 bytes .../res/drawable-mdpi/ic_menu_overflow_lt.png | Bin 0 -> 171 bytes .../common/res/drawable-mdpi/ic_menu_person_dk.png | Bin 0 -> 1052 bytes .../common/res/drawable-mdpi/ic_menu_person_lt.png | Bin 0 -> 1021 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 424 bytes .../common/res/drawable-mdpi/ic_menu_star_dk.png | Bin 0 -> 1034 bytes .../common/res/drawable-mdpi/ic_menu_star_lt.png | Bin 0 -> 1018 bytes .../common/res/drawable-mdpi/ic_message_24dp.png | Bin 0 -> 130 bytes .../common/res/drawable-mdpi/ic_person_24dp.png | Bin 0 -> 188 bytes .../res/drawable-mdpi/ic_person_add_24dp.png | Bin 0 -> 204 bytes .../common/res/drawable-mdpi/ic_phone_attach.png | Bin 0 -> 476 bytes .../common/res/drawable-mdpi/ic_rx_videocam.png | Bin 0 -> 299 bytes .../common/res/drawable-mdpi/ic_scroll_handle.png | Bin 0 -> 504 bytes .../common/res/drawable-mdpi/ic_tx_videocam.png | Bin 0 -> 265 bytes .../common/res/drawable-mdpi/ic_videocam.png | Bin 0 -> 216 bytes .../res/drawable-mdpi/ic_voicemail_avatar.png | Bin 0 -> 2120 bytes .../res/drawable-mdpi/list_activated_holo.9.png | Bin 0 -> 151 bytes .../res/drawable-mdpi/list_background_holo.9.png | Bin 0 -> 188 bytes .../res/drawable-mdpi/list_focused_holo.9.png | Bin 0 -> 235 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 155 bytes .../drawable-mdpi/list_pressed_holo_light.9.png | Bin 0 -> 158 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 198 bytes .../common/res/drawable-mdpi/list_title_holo.9.png | Bin 0 -> 199 bytes .../list_activated_holo.9.png | Bin 0 -> 1659 bytes .../list_activated_holo.9.png | Bin 0 -> 1005 bytes .../list_activated_holo.9.png | Bin 0 -> 2478 bytes .../common/res/drawable-xhdpi/ic_ab_search.png | Bin 0 -> 1451 bytes .../res/drawable-xhdpi/ic_arrow_back_24dp.png | Bin 0 -> 765 bytes .../res/drawable-xhdpi/ic_business_white_120dp.png | Bin 0 -> 2916 bytes .../common/res/drawable-xhdpi/ic_call_24dp.png | Bin 0 -> 420 bytes .../res/drawable-xhdpi/ic_call_note_white_24dp.png | Bin 0 -> 449 bytes .../common/res/drawable-xhdpi/ic_close_dk.png | Bin 0 -> 814 bytes .../common/res/drawable-xhdpi/ic_create_24dp.png | Bin 0 -> 426 bytes .../res/drawable-xhdpi/ic_group_white_24dp.png | Bin 0 -> 461 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 659 bytes .../res/drawable-xhdpi/ic_info_outline_24dp.png | Bin 0 -> 655 bytes .../common/res/drawable-xhdpi/ic_menu_back.png | Bin 0 -> 1034 bytes .../common/res/drawable-xhdpi/ic_menu_group_dk.png | Bin 0 -> 2650 bytes .../common/res/drawable-xhdpi/ic_menu_group_lt.png | Bin 0 -> 2632 bytes .../res/drawable-xhdpi/ic_menu_overflow_lt.png | Bin 0 -> 287 bytes .../res/drawable-xhdpi/ic_menu_person_dk.png | Bin 0 -> 1844 bytes .../res/drawable-xhdpi/ic_menu_person_lt.png | Bin 0 -> 1815 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 593 bytes .../common/res/drawable-xhdpi/ic_menu_star_dk.png | Bin 0 -> 1830 bytes .../res/drawable-xhdpi/ic_menu_star_holo_light.png | Bin 0 -> 1607 bytes .../common/res/drawable-xhdpi/ic_menu_star_lt.png | Bin 0 -> 1827 bytes .../common/res/drawable-xhdpi/ic_message_24dp.png | Bin 0 -> 204 bytes .../common/res/drawable-xhdpi/ic_person_24dp.png | Bin 0 -> 312 bytes .../res/drawable-xhdpi/ic_person_add_24dp.png | Bin 0 -> 329 bytes .../common/res/drawable-xhdpi/ic_phone_attach.png | Bin 0 -> 1009 bytes .../common/res/drawable-xhdpi/ic_rx_videocam.png | Bin 0 -> 439 bytes .../common/res/drawable-xhdpi/ic_scroll_handle.png | Bin 0 -> 620 bytes .../common/res/drawable-xhdpi/ic_tx_videocam.png | Bin 0 -> 405 bytes .../common/res/drawable-xhdpi/ic_videocam.png | Bin 0 -> 301 bytes .../res/drawable-xhdpi/ic_voicemail_avatar.png | Bin 0 -> 4894 bytes .../res/drawable-xhdpi/list_activated_holo.9.png | Bin 0 -> 158 bytes .../res/drawable-xhdpi/list_background_holo.9.png | Bin 0 -> 245 bytes .../res/drawable-xhdpi/list_focused_holo.9.png | Bin 0 -> 235 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 162 bytes .../drawable-xhdpi/list_pressed_holo_light.9.png | Bin 0 -> 163 bytes .../list_section_divider_holo_custom.9.png | Bin 0 -> 210 bytes .../res/drawable-xhdpi/list_title_holo.9.png | Bin 0 -> 267 bytes .../common/res/drawable-xxhdpi/ic_ab_search.png | Bin 0 -> 2100 bytes .../res/drawable-xxhdpi/ic_arrow_back_24dp.png | Bin 0 -> 1376 bytes .../drawable-xxhdpi/ic_business_white_120dp.png | Bin 0 -> 2541 bytes .../common/res/drawable-xxhdpi/ic_call_24dp.png | Bin 0 -> 597 bytes .../drawable-xxhdpi/ic_call_note_white_24dp.png | Bin 0 -> 647 bytes .../common/res/drawable-xxhdpi/ic_close_dk.png | Bin 0 -> 1465 bytes .../common/res/drawable-xxhdpi/ic_create_24dp.png | Bin 0 -> 668 bytes .../res/drawable-xxhdpi/ic_group_white_24dp.png | Bin 0 -> 604 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 971 bytes .../res/drawable-xxhdpi/ic_info_outline_24dp.png | Bin 0 -> 953 bytes .../common/res/drawable-xxhdpi/ic_menu_back.png | Bin 0 -> 1546 bytes .../res/drawable-xxhdpi/ic_menu_group_dk.png | Bin 0 -> 3338 bytes .../res/drawable-xxhdpi/ic_menu_group_lt.png | Bin 0 -> 3381 bytes .../res/drawable-xxhdpi/ic_menu_overflow_lt.png | Bin 0 -> 414 bytes .../res/drawable-xxhdpi/ic_menu_person_dk.png | Bin 0 -> 2357 bytes .../res/drawable-xxhdpi/ic_menu_person_lt.png | Bin 0 -> 2363 bytes .../ic_menu_remove_field_holo_light.png | Bin 0 -> 1381 bytes .../common/res/drawable-xxhdpi/ic_menu_star_dk.png | Bin 0 -> 2111 bytes .../drawable-xxhdpi/ic_menu_star_holo_light.png | Bin 0 -> 2119 bytes .../common/res/drawable-xxhdpi/ic_menu_star_lt.png | Bin 0 -> 2117 bytes .../common/res/drawable-xxhdpi/ic_message_24dp.png | Bin 0 -> 269 bytes .../common/res/drawable-xxhdpi/ic_person_24dp.png | Bin 0 -> 440 bytes .../res/drawable-xxhdpi/ic_person_add_24dp.png | Bin 0 -> 464 bytes .../common/res/drawable-xxhdpi/ic_phone_attach.png | Bin 0 -> 1517 bytes .../common/res/drawable-xxhdpi/ic_rx_videocam.png | Bin 0 -> 603 bytes .../res/drawable-xxhdpi/ic_scroll_handle.png | Bin 0 -> 837 bytes .../common/res/drawable-xxhdpi/ic_tx_videocam.png | Bin 0 -> 551 bytes .../common/res/drawable-xxhdpi/ic_videocam.png | Bin 0 -> 398 bytes .../res/drawable-xxhdpi/ic_voicemail_avatar.png | Bin 0 -> 7976 bytes .../res/drawable-xxhdpi/list_activated_holo.9.png | Bin 0 -> 1140 bytes .../res/drawable-xxhdpi/list_focused_holo.9.png | Bin 0 -> 1147 bytes .../list_longpressed_holo_light.9.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/list_pressed_holo_light.9.png | Bin 0 -> 1051 bytes .../res/drawable-xxhdpi/list_title_holo.9.png | Bin 0 -> 465 bytes .../common/res/drawable-xxxhdpi/ic_ab_search.png | Bin 0 -> 2571 bytes .../res/drawable-xxxhdpi/ic_arrow_back_24dp.png | Bin 0 -> 1512 bytes .../drawable-xxxhdpi/ic_business_white_120dp.png | Bin 0 -> 2915 bytes .../common/res/drawable-xxxhdpi/ic_call_24dp.png | Bin 0 -> 778 bytes .../drawable-xxxhdpi/ic_call_note_white_24dp.png | Bin 0 -> 853 bytes .../common/res/drawable-xxxhdpi/ic_close_dk.png | Bin 0 -> 1688 bytes .../common/res/drawable-xxxhdpi/ic_create_24dp.png | Bin 0 -> 612 bytes .../ic_history_white_drawable_24dp.png | Bin 0 -> 1311 bytes .../res/drawable-xxxhdpi/ic_info_outline_24dp.png | Bin 0 -> 1279 bytes .../res/drawable-xxxhdpi/ic_message_24dp.png | Bin 0 -> 342 bytes .../common/res/drawable-xxxhdpi/ic_person_24dp.png | Bin 0 -> 577 bytes .../res/drawable-xxxhdpi/ic_person_add_24dp.png | Bin 0 -> 610 bytes .../res/drawable-xxxhdpi/ic_phone_attach.png | Bin 0 -> 2135 bytes .../res/drawable-xxxhdpi/ic_scroll_handle.png | Bin 0 -> 1579 bytes .../common/res/drawable-xxxhdpi/ic_videocam.png | Bin 0 -> 481 bytes .../res/drawable-xxxhdpi/ic_voicemail_avatar.png | Bin 0 -> 11277 bytes .../res/drawable/dialog_background_material.xml | 23 + .../common/res/drawable/fastscroll_thumb.xml | 19 + .../contacts/common/res/drawable/ic_back_arrow.xml | 20 + .../contacts/common/res/drawable/ic_call.xml | 19 + .../common/res/drawable/ic_message_24dp.xml | 19 + .../contacts/common/res/drawable/ic_more_vert.xml | 9 + .../res/drawable/ic_person_add_tinted_24dp.xml | 20 + .../res/drawable/ic_scroll_handle_default.xml | 20 + .../res/drawable/ic_scroll_handle_pressed.xml | 20 + .../common/res/drawable/ic_search_add_contact.xml | 20 + .../common/res/drawable/ic_search_video_call.xml | 21 + .../contacts/common/res/drawable/ic_tab_all.xml | 21 + .../contacts/common/res/drawable/ic_tab_groups.xml | 21 + .../common/res/drawable/ic_tab_starred.xml | 21 + .../common/res/drawable/ic_work_profile.xml | 16 + .../item_background_material_borderless_dark.xml | 19 + .../res/drawable/item_background_material_dark.xml | 23 + .../drawable/item_background_material_light.xml | 23 + .../drawable/list_item_activated_background.xml | 20 + ...t_selector_background_transition_holo_light.xml | 20 + .../res/drawable/searchedittext_custom_cursor.xml | 7 + .../res/drawable/unread_count_background.xml | 21 + .../res/drawable/view_pager_tab_background.xml | 22 + .../common/res/layout-ldrtl/unread_count_tab.xml | 48 + .../common/res/layout/account_filter_header.xml | 44 + .../res/layout/account_selector_list_item.xml | 57 + .../account_selector_list_item_condensed.xml | 56 + .../common/res/layout/call_subject_history.xml | 33 + .../res/layout/call_subject_history_list_item.xml | 29 + .../res/layout/contact_detail_list_padding.xml | 27 + .../common/res/layout/contact_list_card.xml | 39 + .../common/res/layout/contact_list_content.xml | 61 + .../common/res/layout/default_account_checkbox.xml | 36 + .../common/res/layout/dialog_call_subject.xml | 159 ++ .../common/res/layout/directory_header.xml | 55 + .../contacts/common/res/layout/list_separator.xml | 27 + .../common/res/layout/search_bar_expanded.xml | 62 + .../common/res/layout/select_account_list_item.xml | 56 + .../common/res/layout/unread_count_tab.xml | 43 + .../res/mipmap-hdpi/ic_contacts_launcher.png | Bin 0 -> 3169 bytes .../res/mipmap-mdpi/ic_contacts_launcher.png | Bin 0 -> 2062 bytes .../res/mipmap-xhdpi/ic_contacts_launcher.png | Bin 0 -> 4430 bytes .../res/mipmap-xxhdpi/ic_contacts_launcher.png | Bin 0 -> 7228 bytes .../res/mipmap-xxxhdpi/ic_contacts_launcher.png | Bin 0 -> 10065 bytes .../common/res/values-ja/donottranslate_config.xml | 20 + .../common/res/values-ko/donottranslate_config.xml | 17 + .../contacts/common/res/values-land/integers.xml | 22 + .../common/res/values-sw600dp-land/integers.xml | 22 + .../contacts/common/res/values-sw600dp/dimens.xml | 29 + .../common/res/values-sw600dp/integers.xml | 24 + .../common/res/values-sw720dp-land/integers.xml | 22 + .../common/res/values-sw720dp/integers.xml | 22 + .../res/values-zh-rCN/donottranslate_config.xml | 17 + .../res/values-zh-rTW/donottranslate_config.xml | 17 + .../common/res/values/animation_constants.xml | 19 + .../android/contacts/common/res/values/attrs.xml | 83 + .../android/contacts/common/res/values/colors.xml | 158 ++ .../android/contacts/common/res/values/dimens.xml | 161 ++ .../common/res/values/donottranslate_config.xml | 95 + .../com/android/contacts/common/res/values/ids.xml | 30 + .../contacts/common/res/values/integers.xml | 39 + .../android/contacts/common/res/values/strings.xml | 798 +++++++++ .../android/contacts/common/res/values/styles.xml | 97 + .../contacts/common/testing/InjectedServices.java | 65 + .../contacts/common/util/AccountFilterUtil.java | 125 ++ .../android/contacts/common/util/BitmapUtil.java | 167 ++ .../contacts/common/util/CommonDateUtils.java | 37 + .../android/contacts/common/util/Constants.java | 28 + .../contacts/common/util/ContactDisplayUtils.java | 307 ++++ .../contacts/common/util/ContactListViewUtils.java | 89 + .../contacts/common/util/ContactLoaderUtils.java | 78 + .../android/contacts/common/util/DateUtils.java | 283 +++ java/com/android/contacts/common/util/FabUtil.java | 71 + .../common/util/MaterialColorMapUtils.java | 181 ++ .../contacts/common/util/NameConverter.java | 242 +++ .../android/contacts/common/util/SearchUtil.java | 198 ++ .../android/contacts/common/util/StopWatch.java | 100 ++ .../common/util/TelephonyManagerUtils.java | 45 + .../contacts/common/util/TrafficStatsTags.java | 22 + .../com/android/contacts/common/util/UriUtils.java | 90 + .../common/widget/ActivityTouchLinearLayout.java | 43 + .../widget/FloatingActionButtonController.java | 226 +++ .../common/widget/LayoutSuppressingImageView.java | 39 + .../widget/SelectPhoneAccountDialogFragment.java | 297 +++ java/com/android/dialer/animation/AnimUtils.java | 247 +++ .../dialer/animation/AnimationListenerAdapter.java | 39 + java/com/android/dialer/app/AndroidManifest.xml | 116 ++ java/com/android/dialer/app/Bindings.java | 77 + .../com/android/dialer/app/CallDetailActivity.java | 480 +++++ java/com/android/dialer/app/DialerApplication.java | 77 + java/com/android/dialer/app/DialtactsActivity.java | 1484 +++++++++++++++ .../dialer/app/FloatingActionButtonBehavior.java | 50 + java/com/android/dialer/app/PhoneCallDetails.java | 207 +++ .../android/dialer/app/SpecialCharSequenceMgr.java | 493 +++++ .../com/android/dialer/app/alert/AlertManager.java | 30 + .../dialer/app/bindings/DialerBindings.java | 25 + .../dialer/app/bindings/DialerBindingsFactory.java | 26 + .../dialer/app/bindings/DialerBindingsStub.java | 48 + .../app/calllog/BlockReportSpamListener.java | 212 +++ .../app/calllog/CallDetailHistoryAdapter.java | 214 +++ .../android/dialer/app/calllog/CallLogAdapter.java | 915 ++++++++++ .../dialer/app/calllog/CallLogAlertManager.java | 90 + .../android/dialer/app/calllog/CallLogAsync.java | 96 + .../dialer/app/calllog/CallLogAsyncTaskUtil.java | 376 ++++ .../dialer/app/calllog/CallLogFragment.java | 528 ++++++ .../dialer/app/calllog/CallLogGroupBuilder.java | 274 +++ .../dialer/app/calllog/CallLogListItemHelper.java | 277 +++ .../app/calllog/CallLogListItemViewHolder.java | 966 ++++++++++ .../app/calllog/CallLogModalAlertManager.java | 74 + .../app/calllog/CallLogNotificationsHelper.java | 299 ++++ .../app/calllog/CallLogNotificationsService.java | 203 +++ .../dialer/app/calllog/CallLogReceiver.java | 77 + .../android/dialer/app/calllog/CallTypeHelper.java | 136 ++ .../dialer/app/calllog/CallTypeIconsView.java | 221 +++ .../dialer/app/calllog/ClearCallLogDialog.java | 98 + .../app/calllog/DefaultVoicemailNotifier.java | 273 +++ .../dialer/app/calllog/GroupingListAdapter.java | 153 ++ .../android/dialer/app/calllog/IntentProvider.java | 198 ++ .../calllog/MissedCallNotificationReceiver.java | 50 + .../dialer/app/calllog/MissedCallNotifier.java | 330 ++++ .../dialer/app/calllog/PhoneAccountUtils.java | 104 ++ .../dialer/app/calllog/PhoneCallDetailsHelper.java | 352 ++++ .../dialer/app/calllog/PhoneCallDetailsViews.java | 75 + .../dialer/app/calllog/PhoneNumberDisplayUtil.java | 85 + .../calllog/VisualVoicemailCallLogFragment.java | 132 ++ .../dialer/app/calllog/VoicemailQueryHandler.java | 74 + .../app/calllog/calllogcache/CallLogCache.java | 105 ++ .../calllog/calllogcache/CallLogCacheLollipop.java | 74 + .../calllogcache/CallLogCacheLollipopMr1.java | 116 ++ .../dialer/app/contactinfo/ContactInfoCache.java | 357 ++++ .../dialer/app/contactinfo/ContactInfoRequest.java | 122 ++ .../dialer/app/contactinfo/ContactPhotoLoader.java | 129 ++ .../ExpirableCacheHeadlessFragment.java | 67 + .../app/contactinfo/NumberWithCountryIso.java | 57 + .../dialer/app/dialpad/DialpadFragment.java | 1689 +++++++++++++++++ .../app/dialpad/PseudoEmergencyAnimator.java | 161 ++ .../dialer/app/dialpad/SmartDialCursorLoader.java | 202 +++ .../app/dialpad/UnicodeDialerKeyListener.java | 56 + .../app/filterednumber/BlockedNumbersAdapter.java | 97 + .../app/filterednumber/BlockedNumbersFragment.java | 271 +++ .../BlockedNumbersSettingsActivity.java | 146 ++ .../dialer/app/filterednumber/NumbersAdapter.java | 138 ++ .../filterednumber/ViewNumbersToImportAdapter.java | 56 + .../ViewNumbersToImportFragment.java | 130 ++ .../app/legacybindings/DialerLegacyBindings.java | 47 + .../DialerLegacyBindingsFactory.java | 26 + .../legacybindings/DialerLegacyBindingsStub.java | 53 + .../dialer/app/list/AllContactsFragment.java | 209 +++ .../dialer/app/list/BlockedListSearchAdapter.java | 84 + .../dialer/app/list/BlockedListSearchFragment.java | 245 +++ .../dialer/app/list/ContentChangedFilter.java | 56 + .../app/list/DialerPhoneNumberListAdapter.java | 228 +++ .../dialer/app/list/DragDropController.java | 106 ++ .../com/android/dialer/app/list/ListsFragment.java | 587 ++++++ .../dialer/app/list/OnDragDropListener.java | 58 + .../app/list/OnListFragmentScrolledListener.java | 27 + .../dialer/app/list/PhoneFavoriteListView.java | 315 ++++ .../app/list/PhoneFavoriteSquareTileView.java | 119 ++ .../dialer/app/list/PhoneFavoriteTileView.java | 155 ++ .../dialer/app/list/PhoneFavoritesTileAdapter.java | 627 +++++++ .../dialer/app/list/RegularSearchFragment.java | 146 ++ .../dialer/app/list/RegularSearchListAdapter.java | 126 ++ java/com/android/dialer/app/list/RemoveView.java | 105 ++ .../android/dialer/app/list/SearchFragment.java | 425 +++++ .../app/list/SmartDialNumberListAdapter.java | 117 ++ .../dialer/app/list/SmartDialSearchFragment.java | 120 ++ .../android/dialer/app/list/SpeedDialFragment.java | 512 ++++++ .../app/manifests/activities/AndroidManifest.xml | 129 ++ .../app/res/color/settings_text_color_primary.xml | 23 + .../res/color/settings_text_color_secondary.xml | 23 + .../app/res/drawable-hdpi/empty_call_log.png | Bin 0 -> 3538 bytes .../app/res/drawable-hdpi/empty_contacts.png | Bin 0 -> 2461 bytes .../app/res/drawable-hdpi/empty_speed_dial.png | Bin 0 -> 6041 bytes .../dialer/app/res/drawable-hdpi/fab_ic_dial.png | Bin 0 -> 1028 bytes .../res/drawable-hdpi/ic_archive_white_24dp.png | Bin 0 -> 247 bytes .../dialer/app/res/drawable-hdpi/ic_call_arrow.png | Bin 0 -> 538 bytes .../app/res/drawable-hdpi/ic_content_copy_24dp.png | Bin 0 -> 203 bytes .../app/res/drawable-hdpi/ic_delete_24dp.png | Bin 0 -> 242 bytes .../res/drawable-hdpi/ic_dialer_fork_add_call.png | Bin 0 -> 1649 bytes .../drawable-hdpi/ic_dialer_fork_current_call.png | Bin 0 -> 2305 bytes .../res/drawable-hdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 2419 bytes .../dialer/app/res/drawable-hdpi/ic_grade_24dp.png | Bin 0 -> 370 bytes .../dialer/app/res/drawable-hdpi/ic_handle.png | Bin 0 -> 543 bytes .../app/res/drawable-hdpi/ic_menu_history_lt.png | Bin 0 -> 1565 bytes .../app/res/drawable-hdpi/ic_mic_grey600.png | Bin 0 -> 377 bytes .../app/res/drawable-hdpi/ic_more_vert_24dp.png | Bin 0 -> 134 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 565 bytes .../dialer/app/res/drawable-hdpi/ic_not_spam.png | Bin 0 -> 858 bytes .../dialer/app/res/drawable-hdpi/ic_pause_24dp.png | Bin 0 -> 105 bytes .../app/res/drawable-hdpi/ic_people_24dp.png | Bin 0 -> 299 bytes .../dialer/app/res/drawable-hdpi/ic_phone_24dp.png | Bin 0 -> 347 bytes .../app/res/drawable-hdpi/ic_play_arrow_24dp.png | Bin 0 -> 195 bytes .../dialer/app/res/drawable-hdpi/ic_remove.png | Bin 0 -> 884 bytes .../app/res/drawable-hdpi/ic_results_phone.png | Bin 0 -> 1084 bytes .../app/res/drawable-hdpi/ic_schedule_24dp.png | Bin 0 -> 575 bytes .../app/res/drawable-hdpi/ic_share_white_24dp.png | Bin 0 -> 397 bytes .../dialer/app/res/drawable-hdpi/ic_star.png | Bin 0 -> 732 bytes .../dialer/app/res/drawable-hdpi/ic_unblock.png | Bin 0 -> 1049 bytes .../app/res/drawable-hdpi/ic_vm_sound_off_dis.png | Bin 0 -> 1339 bytes .../app/res/drawable-hdpi/ic_vm_sound_off_dk.png | Bin 0 -> 1337 bytes .../app/res/drawable-hdpi/ic_vm_sound_on_dis.png | Bin 0 -> 1755 bytes .../app/res/drawable-hdpi/ic_vm_sound_on_dk.png | Bin 0 -> 1750 bytes .../app/res/drawable-hdpi/ic_voicemail_24dp.png | Bin 0 -> 478 bytes .../app/res/drawable-hdpi/ic_volume_down_24dp.png | Bin 0 -> 186 bytes .../app/res/drawable-hdpi/ic_volume_up_24dp.png | Bin 0 -> 365 bytes .../app/res/drawable-hdpi/search_shadow.9.png | Bin 0 -> 183 bytes .../app/res/drawable-hdpi/shadow_contact_photo.png | Bin 0 -> 960 bytes .../app/res/drawable-mdpi/empty_call_log.png | Bin 0 -> 2463 bytes .../app/res/drawable-mdpi/empty_contacts.png | Bin 0 -> 1778 bytes .../app/res/drawable-mdpi/empty_speed_dial.png | Bin 0 -> 4119 bytes .../dialer/app/res/drawable-mdpi/fab_ic_dial.png | Bin 0 -> 905 bytes .../res/drawable-mdpi/ic_archive_white_24dp.png | Bin 0 -> 181 bytes .../dialer/app/res/drawable-mdpi/ic_call_arrow.png | Bin 0 -> 455 bytes .../app/res/drawable-mdpi/ic_content_copy_24dp.png | Bin 0 -> 134 bytes .../app/res/drawable-mdpi/ic_delete_24dp.png | Bin 0 -> 195 bytes .../res/drawable-mdpi/ic_dialer_fork_add_call.png | Bin 0 -> 1309 bytes .../drawable-mdpi/ic_dialer_fork_current_call.png | Bin 0 -> 1581 bytes .../res/drawable-mdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 1586 bytes .../dialer/app/res/drawable-mdpi/ic_grade_24dp.png | Bin 0 -> 271 bytes .../dialer/app/res/drawable-mdpi/ic_handle.png | Bin 0 -> 454 bytes .../app/res/drawable-mdpi/ic_menu_history_lt.png | Bin 0 -> 1086 bytes .../app/res/drawable-mdpi/ic_mic_grey600.png | Bin 0 -> 252 bytes .../app/res/drawable-mdpi/ic_more_vert_24dp.png | Bin 0 -> 112 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 377 bytes .../dialer/app/res/drawable-mdpi/ic_not_spam.png | Bin 0 -> 627 bytes .../dialer/app/res/drawable-mdpi/ic_pause_24dp.png | Bin 0 -> 83 bytes .../app/res/drawable-mdpi/ic_people_24dp.png | Bin 0 -> 210 bytes .../dialer/app/res/drawable-mdpi/ic_phone_24dp.png | Bin 0 -> 262 bytes .../app/res/drawable-mdpi/ic_play_arrow_24dp.png | Bin 0 -> 157 bytes .../dialer/app/res/drawable-mdpi/ic_remove.png | Bin 0 -> 728 bytes .../app/res/drawable-mdpi/ic_results_phone.png | Bin 0 -> 801 bytes .../app/res/drawable-mdpi/ic_schedule_24dp.png | Bin 0 -> 377 bytes .../app/res/drawable-mdpi/ic_share_white_24dp.png | Bin 0 -> 268 bytes .../dialer/app/res/drawable-mdpi/ic_star.png | Bin 0 -> 531 bytes .../dialer/app/res/drawable-mdpi/ic_unblock.png | Bin 0 -> 746 bytes .../app/res/drawable-mdpi/ic_vm_sound_off_dis.png | Bin 0 -> 948 bytes .../app/res/drawable-mdpi/ic_vm_sound_off_dk.png | Bin 0 -> 945 bytes .../app/res/drawable-mdpi/ic_vm_sound_on_dis.png | Bin 0 -> 1166 bytes .../app/res/drawable-mdpi/ic_vm_sound_on_dk.png | Bin 0 -> 1192 bytes .../app/res/drawable-mdpi/ic_voicemail_24dp.png | Bin 0 -> 221 bytes .../app/res/drawable-mdpi/ic_volume_down_24dp.png | Bin 0 -> 139 bytes .../app/res/drawable-mdpi/ic_volume_up_24dp.png | Bin 0 -> 251 bytes .../app/res/drawable-mdpi/search_shadow.9.png | Bin 0 -> 159 bytes .../app/res/drawable-mdpi/shadow_contact_photo.png | Bin 0 -> 948 bytes .../app/res/drawable-xhdpi/empty_call_log.png | Bin 0 -> 4860 bytes .../app/res/drawable-xhdpi/empty_contacts.png | Bin 0 -> 3352 bytes .../app/res/drawable-xhdpi/empty_speed_dial.png | Bin 0 -> 8689 bytes .../dialer/app/res/drawable-xhdpi/fab_ic_dial.png | Bin 0 -> 1699 bytes .../res/drawable-xhdpi/ic_archive_white_24dp.png | Bin 0 -> 267 bytes .../app/res/drawable-xhdpi/ic_call_arrow.png | Bin 0 -> 627 bytes .../res/drawable-xhdpi/ic_content_copy_24dp.png | Bin 0 -> 188 bytes .../app/res/drawable-xhdpi/ic_delete_24dp.png | Bin 0 -> 271 bytes .../res/drawable-xhdpi/ic_dialer_fork_add_call.png | Bin 0 -> 2150 bytes .../drawable-xhdpi/ic_dialer_fork_current_call.png | Bin 0 -> 3154 bytes .../drawable-xhdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 3298 bytes .../app/res/drawable-xhdpi/ic_grade_24dp.png | Bin 0 -> 479 bytes .../dialer/app/res/drawable-xhdpi/ic_handle.png | Bin 0 -> 681 bytes .../app/res/drawable-xhdpi/ic_menu_history_lt.png | Bin 0 -> 2237 bytes .../app/res/drawable-xhdpi/ic_mic_grey600.png | Bin 0 -> 454 bytes .../app/res/drawable-xhdpi/ic_more_vert_24dp.png | Bin 0 -> 158 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 755 bytes .../dialer/app/res/drawable-xhdpi/ic_not_spam.png | Bin 0 -> 996 bytes .../app/res/drawable-xhdpi/ic_pause_24dp.png | Bin 0 -> 90 bytes .../app/res/drawable-xhdpi/ic_people_24dp.png | Bin 0 -> 368 bytes .../app/res/drawable-xhdpi/ic_phone_24dp.png | Bin 0 -> 439 bytes .../app/res/drawable-xhdpi/ic_play_arrow_24dp.png | Bin 0 -> 220 bytes .../dialer/app/res/drawable-xhdpi/ic_remove.png | Bin 0 -> 1237 bytes .../app/res/drawable-xhdpi/ic_results_phone.png | Bin 0 -> 1376 bytes .../app/res/drawable-xhdpi/ic_schedule_24dp.png | Bin 0 -> 737 bytes .../app/res/drawable-xhdpi/ic_share_white_24dp.png | Bin 0 -> 496 bytes .../dialer/app/res/drawable-xhdpi/ic_star.png | Bin 0 -> 889 bytes .../dialer/app/res/drawable-xhdpi/ic_unblock.png | Bin 0 -> 1356 bytes .../app/res/drawable-xhdpi/ic_vm_sound_off_dis.png | Bin 0 -> 1794 bytes .../app/res/drawable-xhdpi/ic_vm_sound_off_dk.png | Bin 0 -> 1794 bytes .../app/res/drawable-xhdpi/ic_vm_sound_on_dis.png | Bin 0 -> 2354 bytes .../app/res/drawable-xhdpi/ic_vm_sound_on_dk.png | Bin 0 -> 2339 bytes .../app/res/drawable-xhdpi/ic_voicemail_24dp.png | Bin 0 -> 487 bytes .../app/res/drawable-xhdpi/ic_volume_down_24dp.png | Bin 0 -> 212 bytes .../app/res/drawable-xhdpi/ic_volume_up_24dp.png | Bin 0 -> 455 bytes .../app/res/drawable-xhdpi/search_shadow.9.png | Bin 0 -> 198 bytes .../res/drawable-xhdpi/shadow_contact_photo.png | Bin 0 -> 965 bytes .../app/res/drawable-xxhdpi/empty_call_log.png | Bin 0 -> 6226 bytes .../app/res/drawable-xxhdpi/empty_contacts.png | Bin 0 -> 3686 bytes .../app/res/drawable-xxhdpi/empty_speed_dial.png | Bin 0 -> 11039 bytes .../dialer/app/res/drawable-xxhdpi/fab_ic_dial.png | Bin 0 -> 3042 bytes .../res/drawable-xxhdpi/ic_archive_white_24dp.png | Bin 0 -> 390 bytes .../app/res/drawable-xxhdpi/ic_call_arrow.png | Bin 0 -> 1203 bytes .../res/drawable-xxhdpi/ic_content_copy_24dp.png | Bin 0 -> 266 bytes .../app/res/drawable-xxhdpi/ic_delete_24dp.png | Bin 0 -> 323 bytes .../drawable-xxhdpi/ic_dialer_fork_add_call.png | Bin 0 -> 2583 bytes .../ic_dialer_fork_current_call.png | Bin 0 -> 3622 bytes .../drawable-xxhdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 3229 bytes .../app/res/drawable-xxhdpi/ic_grade_24dp.png | Bin 0 -> 676 bytes .../dialer/app/res/drawable-xxhdpi/ic_handle.png | Bin 0 -> 1431 bytes .../app/res/drawable-xxhdpi/ic_menu_history_lt.png | Bin 0 -> 2945 bytes .../app/res/drawable-xxhdpi/ic_mic_grey600.png | Bin 0 -> 631 bytes .../app/res/drawable-xxhdpi/ic_more_vert_24dp.png | Bin 0 -> 216 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 1112 bytes .../dialer/app/res/drawable-xxhdpi/ic_not_spam.png | Bin 0 -> 1340 bytes .../app/res/drawable-xxhdpi/ic_pause_24dp.png | Bin 0 -> 92 bytes .../app/res/drawable-xxhdpi/ic_people_24dp.png | Bin 0 -> 488 bytes .../app/res/drawable-xxhdpi/ic_phone_24dp.png | Bin 0 -> 619 bytes .../app/res/drawable-xxhdpi/ic_play_arrow_24dp.png | Bin 0 -> 283 bytes .../dialer/app/res/drawable-xxhdpi/ic_remove.png | Bin 0 -> 1942 bytes .../app/res/drawable-xxhdpi/ic_results_phone.png | Bin 0 -> 2090 bytes .../app/res/drawable-xxhdpi/ic_schedule_24dp.png | Bin 0 -> 1107 bytes .../res/drawable-xxhdpi/ic_share_white_24dp.png | Bin 0 -> 698 bytes .../dialer/app/res/drawable-xxhdpi/ic_star.png | Bin 0 -> 1539 bytes .../dialer/app/res/drawable-xxhdpi/ic_unblock.png | Bin 0 -> 1990 bytes .../res/drawable-xxhdpi/ic_vm_sound_off_dis.png | Bin 0 -> 2316 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png | Bin 0 -> 2319 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png | Bin 0 -> 2878 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png | Bin 0 -> 2879 bytes .../app/res/drawable-xxhdpi/ic_voicemail_24dp.png | Bin 0 -> 625 bytes .../res/drawable-xxhdpi/ic_volume_down_24dp.png | Bin 0 -> 291 bytes .../app/res/drawable-xxhdpi/ic_volume_up_24dp.png | Bin 0 -> 654 bytes .../app/res/drawable-xxhdpi/search_shadow.9.png | Bin 0 -> 1148 bytes .../res/drawable-xxhdpi/shadow_contact_photo.png | Bin 0 -> 970 bytes .../app/res/drawable-xxxhdpi/empty_call_log.png | Bin 0 -> 8761 bytes .../app/res/drawable-xxxhdpi/empty_contacts.png | Bin 0 -> 5204 bytes .../app/res/drawable-xxxhdpi/fab_ic_dial.png | Bin 0 -> 3800 bytes .../res/drawable-xxxhdpi/ic_archive_white_24dp.png | Bin 0 -> 489 bytes .../app/res/drawable-xxxhdpi/ic_call_arrow.png | Bin 0 -> 1344 bytes .../res/drawable-xxxhdpi/ic_content_copy_24dp.png | Bin 0 -> 329 bytes .../app/res/drawable-xxxhdpi/ic_delete_24dp.png | Bin 0 -> 1394 bytes .../app/res/drawable-xxxhdpi/ic_grade_24dp.png | Bin 0 -> 887 bytes .../dialer/app/res/drawable-xxxhdpi/ic_handle.png | Bin 0 -> 1687 bytes .../app/res/drawable-xxxhdpi/ic_mic_grey600.png | Bin 0 -> 853 bytes .../app/res/drawable-xxxhdpi/ic_more_vert_24dp.png | Bin 0 -> 305 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 1458 bytes .../app/res/drawable-xxxhdpi/ic_not_spam.png | Bin 0 -> 1752 bytes .../app/res/drawable-xxxhdpi/ic_pause_24dp.png | Bin 0 -> 94 bytes .../app/res/drawable-xxxhdpi/ic_people_24dp.png | Bin 0 -> 636 bytes .../app/res/drawable-xxxhdpi/ic_phone_24dp.png | Bin 0 -> 837 bytes .../res/drawable-xxxhdpi/ic_play_arrow_24dp.png | Bin 0 -> 343 bytes .../app/res/drawable-xxxhdpi/ic_results_phone.png | Bin 0 -> 2281 bytes .../app/res/drawable-xxxhdpi/ic_schedule_24dp.png | Bin 0 -> 1478 bytes .../res/drawable-xxxhdpi/ic_share_white_24dp.png | Bin 0 -> 938 bytes .../dialer/app/res/drawable-xxxhdpi/ic_unblock.png | Bin 0 -> 1389 bytes .../app/res/drawable-xxxhdpi/ic_voicemail_24dp.png | Bin 0 -> 971 bytes .../res/drawable-xxxhdpi/ic_volume_down_24dp.png | Bin 0 -> 356 bytes .../app/res/drawable-xxxhdpi/ic_volume_up_24dp.png | Bin 0 -> 878 bytes .../app/res/drawable/background_dial_holo_dark.xml | 22 + .../app/res/drawable/floating_action_button.xml | 25 + .../res/drawable/ic_call_detail_content_copy.xml | 20 + .../app/res/drawable/ic_call_detail_edit.xml | 20 + .../app/res/drawable/ic_call_detail_report.xml | 20 + .../app/res/drawable/ic_call_detail_unblock.xml | 20 + .../android/dialer/app/res/drawable/ic_pause.xml | 31 + .../dialer/app/res/drawable/ic_play_arrow.xml | 32 + .../dialer/app/res/drawable/ic_search_phone.xml | 20 + .../app/res/drawable/ic_speakerphone_off.xml | 20 + .../dialer/app/res/drawable/ic_speakerphone_on.xml | 20 + .../app/res/drawable/ic_voicemail_seek_handle.xml | 20 + .../drawable/ic_voicemail_seek_handle_disabled.xml | 20 + .../dialer/app/res/drawable/oval_ripple.xml | 26 + .../dialer/app/res/drawable/overflow_menu.xml | 20 + .../dialer/app/res/drawable/rounded_corner.xml | 22 + .../dialer/app/res/drawable/seekbar_drawable.xml | 63 + .../drawable/selectable_primary_flat_button.xml | 31 + .../dialer/app/res/drawable/shadow_fade_left.xml | 24 + .../dialer/app/res/drawable/shadow_fade_up.xml | 24 + .../app/res/layout-land/dialpad_fragment.xml | 90 + .../empty_content_view_dialpad_search.xml | 71 + .../account_filter_header_for_phone_favorite.xml | 47 + .../app/res/layout/all_contacts_activity.xml | 26 + .../app/res/layout/all_contacts_fragment.xml | 54 + .../app/res/layout/blocked_number_footer.xml | 38 + .../app/res/layout/blocked_number_fragment.xml | 30 + .../app/res/layout/blocked_number_header.xml | 220 +++ .../dialer/app/res/layout/blocked_number_item.xml | 72 + .../app/res/layout/blocked_numbers_activity.xml | 22 + .../android/dialer/app/res/layout/call_detail.xml | 32 + .../dialer/app/res/layout/call_detail_footer.xml | 52 + .../dialer/app/res/layout/call_detail_header.xml | 89 + .../app/res/layout/call_detail_history_item.xml | 56 + .../dialer/app/res/layout/call_log_alert_item.xml | 22 + .../dialer/app/res/layout/call_log_fragment.xml | 48 + .../dialer/app/res/layout/call_log_list_item.xml | 176 ++ .../app/res/layout/call_log_list_item_actions.xml | 230 +++ .../app/res/layout/dialpad_chooser_list_item.xml | 38 + .../dialer/app/res/layout/dialpad_fragment.xml | 78 + .../dialer/app/res/layout/dialtacts_activity.xml | 73 + .../dialer/app/res/layout/empty_content_view.xml | 54 + .../layout/empty_content_view_dialpad_search.xml | 56 + .../dialer/app/res/layout/keyguard_preview.xml | 30 + .../dialer/app/res/layout/lists_fragment.xml | 98 + .../app/res/layout/phone_favorite_tile_view.xml | 128 ++ .../dialer/app/res/layout/search_edittext.xml | 71 + .../dialer/app/res/layout/speed_dial_fragment.xml | 51 + .../res/layout/view_numbers_to_import_fragment.xml | 58 + .../app/res/layout/voicemail_playback_layout.xml | 115 ++ .../dialer/app/res/menu/dialpad_options.xml | 30 + .../dialer/app/res/menu/dialtacts_options.xml | 28 + .../app/res/mipmap-hdpi/ic_launcher_phone.png | Bin 0 -> 2780 bytes .../app/res/mipmap-mdpi/ic_launcher_phone.png | Bin 0 -> 1778 bytes .../app/res/mipmap-xhdpi/ic_launcher_phone.png | Bin 0 -> 3939 bytes .../app/res/mipmap-xxhdpi/ic_launcher_phone.png | Bin 0 -> 6251 bytes .../app/res/mipmap-xxxhdpi/ic_launcher_phone.png | Bin 0 -> 8793 bytes .../dialer/app/res/values/animation_constants.xml | 30 + java/com/android/dialer/app/res/values/attrs.xml | 21 + java/com/android/dialer/app/res/values/colors.xml | 115 ++ java/com/android/dialer/app/res/values/dimens.xml | 148 ++ .../app/res/values/donottranslate_config.xml | 37 + java/com/android/dialer/app/res/values/ids.xml | 28 + java/com/android/dialer/app/res/values/strings.xml | 960 ++++++++++ java/com/android/dialer/app/res/values/styles.xml | 279 +++ .../app/res/xml/display_options_settings.xml | 31 + java/com/android/dialer/app/res/xml/file_paths.xml | 24 + java/com/android/dialer/app/res/xml/searchable.xml | 22 + .../android/dialer/app/res/xml/sound_settings.xml | 46 + .../app/settings/AppCompatPreferenceActivity.java | 155 ++ .../app/settings/DefaultRingtonePreference.java | 64 + .../app/settings/DialerSettingsActivity.java | 187 ++ .../settings/DisplayOptionsSettingsFragment.java | 30 + .../dialer/app/settings/SoundSettingsFragment.java | 242 +++ .../app/voicemail/VoicemailAudioManager.java | 252 +++ .../app/voicemail/VoicemailErrorManager.java | 129 ++ .../app/voicemail/VoicemailPlaybackLayout.java | 449 +++++ .../app/voicemail/VoicemailPlaybackPresenter.java | 1050 +++++++++++ .../dialer/app/voicemail/WiredHeadsetManager.java | 88 + .../dialer/app/voicemail/error/AndroidManifest.xml | 5 + .../error/OmtpVoicemailMessageCreator.java | 177 ++ .../app/voicemail/error/VoicemailErrorAlert.java | 165 ++ .../app/voicemail/error/VoicemailErrorMessage.java | 178 ++ .../error/VoicemailErrorMessageCreator.java | 45 + .../app/voicemail/error/VoicemailStatus.java | 260 +++ .../error/VoicemailStatusCorruptionHandler.java | 114 ++ .../app/voicemail/error/VoicemailStatusReader.java | 25 + .../app/voicemail/error/VoicemailTosMessage.java | 25 + .../error/Vvm3VoicemailMessageCreator.java | 428 +++++ .../res/layout/voicemai_error_message_fragment.xml | 114 ++ .../error/res/layout/voicemail_tos_fragment.xml | 72 + .../app/voicemail/error/res/values/dimens.xml | 12 + .../app/voicemail/error/res/values/strings.xml | 176 ++ .../app/voicemail/error/res/values/styles.xml | 26 + .../dialer/app/widget/ActionBarController.java | 247 +++ .../app/widget/DialpadSearchEmptyContentView.java | 43 + .../dialer/app/widget/EmptyContentView.java | 121 ++ .../dialer/app/widget/SearchEditTextLayout.java | 324 ++++ java/com/android/dialer/backup/AndroidManifest.xml | 27 + .../android/dialer/backup/DialerBackupAgent.java | 276 +++ .../android/dialer/backup/DialerBackupUtils.java | 320 ++++ .../android/dialer/backup/proto/VoicemailInfo.java | 377 ++++ .../android/dialer/blocking/AndroidManifest.xml | 13 + .../dialer/blocking/BlockNumberDialogFragment.java | 328 ++++ .../dialer/blocking/BlockReportSpamDialogs.java | 305 ++++ .../blocking/BlockedNumbersAutoMigrator.java | 110 ++ .../dialer/blocking/BlockedNumbersMigrator.java | 159 ++ .../blocking/FilteredNumberAsyncQueryHandler.java | 428 +++++ .../dialer/blocking/FilteredNumberCompat.java | 320 ++++ .../dialer/blocking/FilteredNumberProvider.java | 176 ++ .../dialer/blocking/FilteredNumbersUtil.java | 380 ++++ .../MigrateBlockedNumbersDialogFragment.java | 113 ++ .../blocking/res/drawable-hdpi/ic_block_24dp.png | Bin 0 -> 478 bytes .../blocking/res/drawable-hdpi/ic_report_24dp.png | Bin 0 -> 240 bytes .../res/drawable-hdpi/ic_report_white_36dp.png | Bin 0 -> 312 bytes .../blocking/res/drawable-mdpi/ic_block_24dp.png | Bin 0 -> 335 bytes .../blocking/res/drawable-mdpi/ic_report_24dp.png | Bin 0 -> 174 bytes .../res/drawable-mdpi/ic_report_white_36dp.png | Bin 0 -> 240 bytes .../blocking/res/drawable-xhdpi/ic_block_24dp.png | Bin 0 -> 665 bytes .../blocking/res/drawable-xhdpi/ic_report_24dp.png | Bin 0 -> 272 bytes .../res/drawable-xhdpi/ic_report_white_36dp.png | Bin 0 -> 340 bytes .../blocking/res/drawable-xxhdpi/ic_block_24dp.png | Bin 0 -> 973 bytes .../res/drawable-xxhdpi/ic_report_24dp.png | Bin 0 -> 340 bytes .../res/drawable-xxhdpi/ic_report_white_36dp.png | Bin 0 -> 522 bytes .../res/drawable-xxxhdpi/ic_block_24dp.png | Bin 0 -> 1295 bytes .../res/drawable-xxxhdpi/ic_report_24dp.png | Bin 0 -> 450 bytes .../res/drawable-xxxhdpi/ic_report_white_36dp.png | Bin 0 -> 649 bytes .../blocking/res/drawable/blocked_contact.xml | 36 + .../res/layout/block_report_spam_dialog.xml | 36 + .../android/dialer/blocking/res/values/colors.xml | 24 + .../android/dialer/blocking/res/values/dimens.xml | 18 + .../android/dialer/blocking/res/values/strings.xml | 122 ++ java/com/android/dialer/buildtype/BuildType.java | 62 + .../dialer/buildtype/BuildTypeAccessor.java | 31 + .../buildtype/dogfood/BuildTypeAccessorImpl.java | 30 + .../dialer/callcomposer/AndroidManifest.xml | 28 + .../dialer/callcomposer/CallComposerActivity.java | 728 ++++++++ .../dialer/callcomposer/CallComposerFragment.java | 125 ++ .../callcomposer/CallComposerPagerAdapter.java | 57 + .../callcomposer/CameraComposerFragment.java | 378 ++++ .../callcomposer/GalleryComposerFragment.java | 256 +++ .../dialer/callcomposer/GalleryCursorLoader.java | 54 + .../dialer/callcomposer/GalleryGridAdapter.java | 118 ++ .../dialer/callcomposer/GalleryGridItemData.java | 91 + .../dialer/callcomposer/GalleryGridItemView.java | 126 ++ .../callcomposer/MessageComposerFragment.java | 143 ++ .../dialer/callcomposer/camera/AndroidManifest.xml | 16 + .../dialer/callcomposer/camera/CameraManager.java | 822 +++++++++ .../dialer/callcomposer/camera/CameraPreview.java | 177 ++ .../callcomposer/camera/HardwareCameraPreview.java | 125 ++ .../callcomposer/camera/ImagePersistTask.java | 143 ++ .../callcomposer/camera/SoftwareCameraPreview.java | 120 ++ .../camera/camerafocus/AndroidManifest.xml | 16 + .../camera/camerafocus/FocusIndicator.java | 28 + .../camera/camerafocus/FocusOverlayManager.java | 482 +++++ .../camera/camerafocus/OverlayRenderer.java | 97 + .../callcomposer/camera/camerafocus/PieItem.java | 179 ++ .../camera/camerafocus/PieRenderer.java | 816 +++++++++ .../camera/camerafocus/RenderOverlay.java | 153 ++ .../camera/camerafocus/res/values/dimens.xml | 26 + .../camera/exif/CountedDataInputStream.java | 129 ++ .../dialer/callcomposer/camera/exif/ExifData.java | 89 + .../callcomposer/camera/exif/ExifInterface.java | 374 ++++ .../camera/exif/ExifInvalidFormatException.java | 24 + .../callcomposer/camera/exif/ExifParser.java | 846 +++++++++ .../callcomposer/camera/exif/ExifReader.java | 81 + .../dialer/callcomposer/camera/exif/ExifTag.java | 619 +++++++ .../dialer/callcomposer/camera/exif/IfdData.java | 126 ++ .../dialer/callcomposer/camera/exif/IfdId.java | 28 + .../callcomposer/camera/exif/JpegHeader.java | 38 + .../dialer/callcomposer/camera/exif/Rational.java | 70 + .../callcomposer/cameraui/AndroidManifest.xml | 16 + .../cameraui/CameraMediaChooserView.java | 107 ++ .../cameraui/res/drawable-hdpi/ic_capture.png | Bin 0 -> 2690 bytes .../cameraui/res/drawable-mdpi/ic_capture.png | Bin 0 -> 1851 bytes .../cameraui/res/drawable-xhdpi/ic_capture.png | Bin 0 -> 3636 bytes .../cameraui/res/drawable-xxhdpi/ic_capture.png | Bin 0 -> 5449 bytes .../cameraui/res/drawable-xxxhdpi/ic_capture.png | Bin 0 -> 7354 bytes .../res/drawable/transparent_button_background.xml | 26 + .../cameraui/res/layout/camera_view.xml | 121 ++ .../callcomposer/cameraui/res/values/colors.xml | 4 + .../callcomposer/cameraui/res/values/dimens.xml | 22 + .../callcomposer/cameraui/res/values/strings.xml | 17 + .../callcomposer/nano/CallComposerContact.java | 220 +++ .../res/drawable/call_composer_contact_border.xml | 30 + .../res/drawable/gallery_background.xml | 22 + .../drawable/gallery_grid_checkbox_background.xml | 22 + .../drawable/gallery_grid_item_view_background.xml | 22 + .../drawable/gallery_item_selected_drawable.xml | 37 + .../res/layout/call_composer_activity.xml | 147 ++ .../res/layout/fragment_camera_composer.xml | 33 + .../res/layout/fragment_gallery_composer.xml | 38 + .../res/layout/fragment_message_composer.xml | 79 + .../res/layout/gallery_grid_item_view.xml | 57 + .../callcomposer/res/layout/permission_view.xml | 52 + .../dialer/callcomposer/res/values/colors.xml | 24 + .../dialer/callcomposer/res/values/dimens.xml | 63 + .../dialer/callcomposer/res/values/strings.xml | 42 + .../dialer/callcomposer/res/values/styles.xml | 50 + .../callcomposer/util/CopyAndResizeImageTask.java | 124 ++ .../dialer/callintent/CallIntentBuilder.java | 108 ++ .../dialer/callintent/CallIntentParser.java | 54 + java/com/android/dialer/callintent/Constants.java | 31 + .../dialer/callintent/nano/CallInitiationType.java | 101 ++ .../callintent/nano/CallSpecificAppData.java | 143 ++ java/com/android/dialer/common/AndroidManifest.xml | 3 + java/com/android/dialer/common/Assert.java | 185 ++ .../android/dialer/common/AsyncTaskExecutor.java | 51 + .../android/dialer/common/AsyncTaskExecutors.java | 91 + ...Value_FallibleAsyncTask_FallibleTaskResult.java | 79 + java/com/android/dialer/common/ConfigProvider.java | 27 + .../dialer/common/ConfigProviderBindings.java | 68 + .../dialer/common/ConfigProviderFactory.java | 26 + java/com/android/dialer/common/DpUtil.java | 31 + .../android/dialer/common/FallibleAsyncTask.java | 94 + java/com/android/dialer/common/FragmentUtils.java | 98 + java/com/android/dialer/common/LogUtil.java | 214 +++ java/com/android/dialer/common/MathUtil.java | 57 + java/com/android/dialer/common/NetworkUtil.java | 192 ++ java/com/android/dialer/common/UiUtil.java | 41 + .../android/dialer/common/res/values/strings.xml | 5 + java/com/android/dialer/compat/ActivityCompat.java | 29 + .../android/dialer/compat/AppCompatConstants.java | 33 + java/com/android/dialer/compat/CompatUtils.java | 222 +++ .../dialer/compat/PathInterpolatorCompat.java | 120 ++ .../android/dialer/compat/SdkVersionOverride.java | 43 + java/com/android/dialer/constants/Constants.java | 47 + .../android/dialer/constants/ScheduledJobIds.java | 31 + .../dialer/constants/aospdialer/ConstantsImpl.java | 37 + .../dialer/database/CallLogQueryHandler.java | 369 ++++ java/com/android/dialer/database/Database.java | 49 + .../android/dialer/database/DatabaseBindings.java | 25 + .../dialer/database/DatabaseBindingsFactory.java | 26 + .../dialer/database/DatabaseBindingsStub.java | 35 + .../dialer/database/DialerDatabaseHelper.java | 1242 +++++++++++++ .../dialer/database/FilteredNumberContract.java | 137 ++ .../dialer/database/VoicemailStatusQuery.java | 91 + java/com/android/dialer/debug/AndroidManifest.xml | 3 + .../dialer/debug/bindings/impl/DebugBindings.java | 32 + .../android/dialer/debug/impl/AndroidManifest.xml | 18 + .../android/dialer/debug/impl/DebugConnection.java | 55 + .../dialer/debug/impl/DebugConnectionService.java | 103 ++ .../android/dialer/dialpadview/AndroidManifest.xml | 3 + .../dialer/dialpadview/DialpadKeyButton.java | 231 +++ .../dialer/dialpadview/DialpadTextView.java | 71 + .../android/dialer/dialpadview/DialpadView.java | 464 +++++ .../android/dialer/dialpadview/DigitsEditText.java | 57 + .../res/anim/dialpad_slide_in_bottom.xml | 19 + .../dialpadview/res/anim/dialpad_slide_in_left.xml | 22 + .../res/anim/dialpad_slide_in_right.xml | 20 + .../res/anim/dialpad_slide_out_bottom.xml | 19 + .../res/anim/dialpad_slide_out_left.xml | 22 + .../res/anim/dialpad_slide_out_right.xml | 20 + .../dialpadview/res/drawable-hdpi/dialer_fab.png | Bin 0 -> 3273 bytes .../dialpadview/res/drawable-hdpi/fab_green.png | Bin 0 -> 2798 bytes .../dialpadview/res/drawable-hdpi/fab_ic_call.png | Bin 0 -> 875 bytes .../res/drawable-hdpi/ic_close_black_24dp.png | Bin 0 -> 207 bytes .../res/drawable-hdpi/ic_dialpad_delete.png | Bin 0 -> 805 bytes .../res/drawable-hdpi/ic_dialpad_voicemail.png | Bin 0 -> 623 bytes .../res/drawable-hdpi/ic_overflow_menu.png | Bin 0 -> 503 bytes .../dialpadview/res/drawable-mdpi/dialer_fab.png | Bin 0 -> 1945 bytes .../dialpadview/res/drawable-mdpi/fab_green.png | Bin 0 -> 1845 bytes .../dialpadview/res/drawable-mdpi/fab_ic_call.png | Bin 0 -> 698 bytes .../res/drawable-mdpi/ic_close_black_24dp.png | Bin 0 -> 164 bytes .../res/drawable-mdpi/ic_dialpad_delete.png | Bin 0 -> 669 bytes .../res/drawable-mdpi/ic_dialpad_voicemail.png | Bin 0 -> 504 bytes .../res/drawable-mdpi/ic_overflow_menu.png | Bin 0 -> 424 bytes .../dialpadview/res/drawable-xhdpi/dialer_fab.png | Bin 0 -> 4872 bytes .../dialpadview/res/drawable-xhdpi/fab_green.png | Bin 0 -> 4092 bytes .../dialpadview/res/drawable-xhdpi/fab_ic_call.png | Bin 0 -> 1266 bytes .../res/drawable-xhdpi/ic_close_black_24dp.png | Bin 0 -> 235 bytes .../res/drawable-xhdpi/ic_dialpad_delete.png | Bin 0 -> 1110 bytes .../res/drawable-xhdpi/ic_dialpad_voicemail.png | Bin 0 -> 787 bytes .../res/drawable-xhdpi/ic_overflow_menu.png | Bin 0 -> 550 bytes .../dialpadview/res/drawable-xxhdpi/dialer_fab.png | Bin 0 -> 8621 bytes .../dialpadview/res/drawable-xxhdpi/fab_green.png | Bin 0 -> 7004 bytes .../res/drawable-xxhdpi/fab_ic_call.png | Bin 0 -> 2321 bytes .../res/drawable-xxhdpi/ic_close_black_24dp.png | Bin 0 -> 309 bytes .../res/drawable-xxhdpi/ic_dialpad_delete.png | Bin 0 -> 1745 bytes .../res/drawable-xxhdpi/ic_dialpad_voicemail.png | Bin 0 -> 1578 bytes .../res/drawable-xxhdpi/ic_overflow_menu.png | Bin 0 -> 1384 bytes .../res/drawable-xxxhdpi/dialer_fab.png | Bin 0 -> 12782 bytes .../dialpadview/res/drawable-xxxhdpi/fab_green.png | Bin 0 -> 9900 bytes .../res/drawable-xxxhdpi/fab_ic_call.png | Bin 0 -> 2921 bytes .../res/drawable-xxxhdpi/ic_close_black_24dp.png | Bin 0 -> 377 bytes .../res/drawable-xxxhdpi/ic_dialpad_delete.png | Bin 0 -> 2128 bytes .../res/drawable-xxxhdpi/ic_dialpad_voicemail.png | Bin 0 -> 1829 bytes .../res/drawable-xxxhdpi/ic_overflow_menu.png | Bin 0 -> 1785 bytes .../dialpadview/res/drawable/btn_dialpad_key.xml | 18 + .../dialpadview/res/drawable/dialpad_scrim.xml | 7 + .../dialpadview/res/layout-land/dialpad_key.xml | 44 + .../res/layout-land/dialpad_key_one.xml | 44 + .../res/layout-land/dialpad_key_pound.xml | 33 + .../res/layout-land/dialpad_key_star.xml | 33 + .../res/layout-land/dialpad_key_zero.xml | 44 + .../dialer/dialpadview/res/layout/dialpad.xml | 99 + .../dialer/dialpadview/res/layout/dialpad_key.xml | 35 + .../dialpadview/res/layout/dialpad_key_one.xml | 41 + .../dialpadview/res/layout/dialpad_key_pound.xml | 26 + .../dialpadview/res/layout/dialpad_key_star.xml | 26 + .../dialpadview/res/layout/dialpad_key_zero.xml | 37 + .../dialer/dialpadview/res/layout/dialpad_view.xml | 23 + .../res/layout/dialpad_view_unthemed.xml | 153 ++ .../dialer/dialpadview/res/values-land/dimens.xml | 27 + .../dialer/dialpadview/res/values-land/styles.xml | 37 + .../dialpadview/res/values/animation_constants.xml | 20 + .../dialer/dialpadview/res/values/attrs.xml | 39 + .../dialer/dialpadview/res/values/colors.xml | 27 + .../dialer/dialpadview/res/values/dimens.xml | 48 + .../dialer/dialpadview/res/values/strings.xml | 53 + .../dialer/dialpadview/res/values/styles.xml | 118 ++ java/com/android/dialer/disabled_lint_checks.txt | 1 + .../AutoValue_EnrichedCallCapabilities.java | 76 + .../AutoValue_OutgoingCallComposerData.java | 127 ++ .../enrichedcall/EnrichedCallCapabilities.java | 36 + .../dialer/enrichedcall/EnrichedCallManager.java | 225 +++ .../enrichedcall/EnrichedCallManagerStub.java | 84 + .../enrichedcall/OutgoingCallComposerData.java | 94 + java/com/android/dialer/enrichedcall/Session.java | 63 + .../enrichedcall/StubEnrichedCallModule.java | 32 + .../enrichedcall/extensions/StateExtension.java | 54 + .../android/dialer/inject/ApplicationModule.java | 39 + .../android/dialer/inject/DialerAppComponent.java | 29 + .../dialer/interactions/AndroidManifest.xml | 20 + .../dialer/interactions/ContactUpdateService.java | 48 + .../interactions/PhoneNumberInteraction.java | 557 ++++++ .../interactions/UndemoteOutgoingCallReceiver.java | 107 ++ .../res/layout/phone_disambig_item.xml | 43 + .../res/layout/set_primary_checkbox.xml | 32 + .../dialer/interactions/res/values/strings.xml | 29 + java/com/android/dialer/logging/Logger.java | 49 + .../android/dialer/logging/LoggingBindings.java | 59 + .../dialer/logging/LoggingBindingsFactory.java | 24 + .../dialer/logging/LoggingBindingsStub.java | 36 + .../dialer/logging/nano/ContactLookupResult.java | 91 + .../android/dialer/logging/nano/ContactSource.java | 90 + .../dialer/logging/nano/DialerImpression.java | 178 ++ .../dialer/logging/nano/InteractionEvent.java | 95 + .../dialer/logging/nano/ReportingLocation.java | 87 + .../android/dialer/logging/nano/ScreenEvent.java | 104 ++ .../multimedia/AutoValue_MultimediaData.java | 165 ++ .../android/dialer/multimedia/MultimediaData.java | 100 ++ .../android/dialer/p13n/inference/P13nRanking.java | 75 + .../dialer/p13n/inference/protocol/P13nRanker.java | 75 + .../p13n/inference/protocol/P13nRankerFactory.java | 26 + .../android/dialer/p13n/logging/P13nLogger.java | 35 + .../dialer/p13n/logging/P13nLoggerFactory.java | 29 + .../android/dialer/p13n/logging/P13nLogging.java | 60 + .../CachedNumberLookupService.java | 77 + .../dialer/phonenumbercache/CallLogQuery.java | 107 ++ .../dialer/phonenumbercache/ContactInfo.java | 165 ++ .../dialer/phonenumbercache/ContactInfoHelper.java | 586 ++++++ .../dialer/phonenumbercache/PhoneLookupUtil.java | 40 + .../dialer/phonenumbercache/PhoneNumberCache.java | 50 + .../phonenumbercache/PhoneNumberCacheBindings.java | 26 + .../PhoneNumberCacheBindingsFactory.java | 26 + .../PhoneNumberCacheBindingsStub.java | 29 + .../dialer/phonenumbercache/PhoneQuery.java | 96 + .../dialer/phonenumberutil/AndroidManifest.xml | 3 + .../dialer/phonenumberutil/PhoneNumberHelper.java | 276 +++ .../dialer/phonenumberutil/res/values/strings.xml | 27 + .../android/dialer/proguard/UsedByReflection.java | 34 + java/com/android/dialer/protos/ProtoParsers.java | 167 ++ .../android/dialer/shortcuts/AndroidManifest.xml | 50 + .../dialer/shortcuts/AutoValue_DialerShortcut.java | 161 ++ .../dialer/shortcuts/CallContactActivity.java | 133 ++ .../android/dialer/shortcuts/DialerShortcut.java | 190 ++ .../android/dialer/shortcuts/DynamicShortcuts.java | 243 +++ java/com/android/dialer/shortcuts/IconFactory.java | 112 ++ .../dialer/shortcuts/PeriodicJobService.java | 118 ++ .../android/dialer/shortcuts/PinnedShortcuts.java | 159 ++ .../dialer/shortcuts/RefreshShortcutsTask.java | 71 + .../dialer/shortcuts/ShortcutInfoFactory.java | 100 ++ .../dialer/shortcuts/ShortcutRefresher.java | 86 + .../dialer/shortcuts/ShortcutUsageReporter.java | 132 ++ java/com/android/dialer/shortcuts/Shortcuts.java | 34 + .../dialer/shortcuts/ShortcutsJobScheduler.java | 48 + .../res/drawable/ic_shortcut_add_contact.xml | 39 + .../android/dialer/shortcuts/res/values/colors.xml | 20 + .../android/dialer/shortcuts/res/values/dimens.xml | 19 + .../dialer/shortcuts/res/values/strings.xml | 37 + .../android/dialer/shortcuts/res/values/themes.xml | 39 + .../android/dialer/shortcuts/res/xml/shortcuts.xml | 31 + java/com/android/dialer/simulator/Simulator.java | 27 + .../dialer/simulator/impl/AndroidManifest.xml | 18 + .../impl/AutoValue_SimulatorCallLog_CallEntry.java | 160 ++ .../impl/AutoValue_SimulatorContacts_Contact.java | 231 +++ .../AutoValue_SimulatorVoicemail_Voicemail.java | 184 ++ .../simulator/impl/SimulatorActionProvider.java | 88 + .../dialer/simulator/impl/SimulatorCallLog.java | 139 ++ .../dialer/simulator/impl/SimulatorConnection.java | 56 + .../simulator/impl/SimulatorConnectionService.java | 87 + .../dialer/simulator/impl/SimulatorContacts.java | 319 ++++ .../dialer/simulator/impl/SimulatorModule.java | 34 + .../dialer/simulator/impl/SimulatorVoiceCall.java | 47 + .../dialer/simulator/impl/SimulatorVoicemail.java | 154 ++ .../dialer/smartdial/LatinSmartDialMap.java | 784 ++++++++ .../com/android/dialer/smartdial/SmartDialMap.java | 60 + .../dialer/smartdial/SmartDialMatchPosition.java | 70 + .../dialer/smartdial/SmartDialNameMatcher.java | 434 +++++ .../android/dialer/smartdial/SmartDialPrefix.java | 605 +++++++ java/com/android/dialer/spam/Spam.java | 49 + java/com/android/dialer/spam/SpamBindings.java | 146 ++ .../android/dialer/spam/SpamBindingsFactory.java | 26 + java/com/android/dialer/spam/SpamBindingsStub.java | 92 + java/com/android/dialer/telecom/TelecomUtil.java | 212 +++ java/com/android/dialer/theme/AndroidManifest.xml | 3 + .../anim/front_back_switch_button_animation.xml | 14 + .../res/animator/activated_button_elevation.xml | 21 + .../dialer/theme/res/animator/button_elevation.xml | 21 + .../res/drawable/front_back_switch_button.xml | 75 + .../front_back_switch_button_animation.xml | 8 + .../com/android/dialer/theme/res/values/colors.xml | 64 + .../com/android/dialer/theme/res/values/dimens.xml | 28 + .../android/dialer/theme/res/values/strings.xml | 27 + .../com/android/dialer/theme/res/values/styles.xml | 56 + .../com/android/dialer/theme/res/values/themes.xml | 21 + java/com/android/dialer/util/AndroidManifest.xml | 3 + java/com/android/dialer/util/CallUtil.java | 135 ++ java/com/android/dialer/util/DialerUtils.java | 246 +++ .../com/android/dialer/util/DrawableConverter.java | 97 + java/com/android/dialer/util/ExpirableCache.java | 269 +++ java/com/android/dialer/util/IntentUtil.java | 78 + java/com/android/dialer/util/MoreStrings.java | 64 + java/com/android/dialer/util/OrientationUtil.java | 30 + java/com/android/dialer/util/PermissionsUtil.java | 121 ++ java/com/android/dialer/util/SettingsUtil.java | 95 + .../com/android/dialer/util/TouchPointManager.java | 60 + .../dialer/util/TransactionSafeActivity.java | 64 + java/com/android/dialer/util/ViewUtil.java | 129 ++ .../com/android/dialer/util/res/values/strings.xml | 42 + .../dialer/voicemailstatus/AndroidManifest.xml | 3 + .../VisualVoicemailEnabledChecker.java | 111 ++ .../voicemailstatus/VoicemailStatusHelper.java | 96 + .../voicemailstatus/VoicemailStatusHelperImpl.java | 278 +++ .../dialer/voicemailstatus/res/values/strings.xml | 41 + java/com/android/dialer/widget/AndroidManifest.xml | 3 + .../dialer/widget/ResizingTextEditText.java | 51 + .../dialer/widget/ResizingTextTextView.java | 51 + .../com/android/dialer/widget/res/values/attrs.xml | 23 + .../android/incallui/AccelerometerListener.java | 173 ++ java/com/android/incallui/AndroidManifest.xml | 121 ++ .../android/incallui/AnswerScreenPresenter.java | 110 ++ .../incallui/AnswerScreenPresenterStub.java | 44 + java/com/android/incallui/AudioModeProvider.java | 69 + java/com/android/incallui/Bindings.java | 52 + java/com/android/incallui/CallButtonPresenter.java | 515 ++++++ java/com/android/incallui/CallCardPresenter.java | 1110 ++++++++++++ java/com/android/incallui/CallerInfo.java | 573 ++++++ .../com/android/incallui/CallerInfoAsyncQuery.java | 638 +++++++ java/com/android/incallui/CallerInfoUtils.java | 279 +++ .../incallui/ConferenceManagerFragment.java | 106 ++ .../incallui/ConferenceManagerPresenter.java | 139 ++ .../incallui/ConferenceParticipantListAdapter.java | 523 ++++++ java/com/android/incallui/ContactInfoCache.java | 759 ++++++++ java/com/android/incallui/ContactsAsyncHelper.java | 269 +++ .../incallui/ContactsPreferencesFactory.java | 56 + java/com/android/incallui/DialpadFragment.java | 461 +++++ java/com/android/incallui/DialpadPresenter.java | 91 + .../com/android/incallui/ExternalCallNotifier.java | 465 +++++ java/com/android/incallui/InCallActivity.java | 756 ++++++++ .../com/android/incallui/InCallActivityCommon.java | 820 +++++++++ java/com/android/incallui/InCallCameraManager.java | 173 ++ .../incallui/InCallOrientationEventListener.java | 194 ++ java/com/android/incallui/InCallPresenter.java | 1679 +++++++++++++++++ java/com/android/incallui/InCallServiceImpl.java | 99 + .../incallui/InCallUIMaterialColorMapUtils.java | 67 + java/com/android/incallui/Log.java | 145 ++ .../android/incallui/ManageConferenceActivity.java | 86 + .../incallui/NotificationBroadcastReceiver.java | 165 ++ .../android/incallui/PostCharDialogFragment.java | 96 + java/com/android/incallui/ProximitySensor.java | 292 +++ java/com/android/incallui/StatusBarNotifier.java | 842 +++++++++ java/com/android/incallui/ThemeColorManager.java | 142 ++ .../incallui/TransactionSafeFragmentActivity.java | 64 + java/com/android/incallui/VideoCallPresenter.java | 1289 +++++++++++++ .../com/android/incallui/VideoPauseController.java | 416 +++++ .../incallui/answer/bindings/AnswerBindings.java | 29 + .../answer/impl/AffordanceHolderLayout.java | 178 ++ .../incallui/answer/impl/AndroidManifest.xml | 3 + .../incallui/answer/impl/AnswerFragment.java | 981 ++++++++++ .../answer/impl/AnswerVideoCallScreen.java | 127 ++ .../answer/impl/CreateCustomSmsDialogFragment.java | 137 ++ .../android/incallui/answer/impl/PillDrawable.java | 43 + .../answer/impl/SmsBottomSheetFragment.java | 136 ++ .../answer/impl/affordance/AndroidManifest.xml | 3 + .../answer/impl/affordance/SwipeButtonHelper.java | 642 +++++++ .../answer/impl/affordance/SwipeButtonView.java | 505 ++++++ .../answer/impl/affordance/res/values/dimens.xml | 23 + .../answer/impl/answermethod/AndroidManifest.xml | 3 + .../answer/impl/answermethod/AnswerMethod.java | 45 + .../impl/answermethod/AnswerMethodFactory.java | 52 + .../impl/answermethod/AnswerMethodHolder.java | 47 + .../impl/answermethod/FlingUpDownMethod.java | 1149 ++++++++++++ .../impl/answermethod/FlingUpDownTouchHandler.java | 496 +++++ .../answer/impl/answermethod/TwoButtonMethod.java | 268 +++ .../impl/answermethod/res/drawable/call_answer.xml | 19 + .../res/drawable/circular_background.xml | 6 + .../res/layout/swipe_up_down_method.xml | 115 ++ .../answermethod/res/layout/two_button_method.xml | 97 + .../impl/answermethod/res/values-h240dp/values.xml | 20 + .../impl/answermethod/res/values-h280dp/dimens.xml | 21 + .../impl/answermethod/res/values-h480dp/dimens.xml | 20 + .../answer/impl/answermethod/res/values/dimens.xml | 27 + .../answer/impl/answermethod/res/values/ids.xml | 5 + .../impl/answermethod/res/values/strings.xml | 14 + .../answer/impl/answermethod/res/values/styles.xml | 7 + .../answer/impl/answermethod/res/values/values.xml | 25 + .../impl/classifier/AccelerationClassifier.java | 99 + .../answer/impl/classifier/AnglesClassifier.java | 193 ++ .../impl/classifier/AnglesPercentageEvaluator.java | 33 + .../impl/classifier/AnglesVarianceEvaluator.java | 42 + .../answer/impl/classifier/Classifier.java | 35 + .../answer/impl/classifier/ClassifierData.java | 96 + .../impl/classifier/DirectionClassifier.java | 37 + .../answer/impl/classifier/DirectionEvaluator.java | 23 + .../impl/classifier/DurationCountClassifier.java | 35 + .../impl/classifier/DurationCountEvaluator.java | 39 + .../impl/classifier/EndPointLengthClassifier.java | 36 + .../impl/classifier/EndPointLengthEvaluator.java | 42 + .../impl/classifier/EndPointRatioClassifier.java | 43 + .../impl/classifier/EndPointRatioEvaluator.java | 42 + .../answer/impl/classifier/FalsingManager.java | 140 ++ .../answer/impl/classifier/GestureClassifier.java | 31 + .../answer/impl/classifier/HistoryEvaluator.java | 115 ++ .../classifier/HumanInteractionClassifier.java | 142 ++ .../impl/classifier/LengthCountClassifier.java | 39 + .../impl/classifier/LengthCountEvaluator.java | 45 + .../incallui/answer/impl/classifier/Point.java | 95 + .../impl/classifier/PointerCountClassifier.java | 51 + .../impl/classifier/PointerCountEvaluator.java | 23 + .../impl/classifier/ProximityClassifier.java | 97 + .../answer/impl/classifier/ProximityEvaluator.java | 28 + .../impl/classifier/SpeedAnglesClassifier.java | 147 ++ .../classifier/SpeedAnglesPercentageEvaluator.java | 33 + .../answer/impl/classifier/SpeedClassifier.java | 40 + .../answer/impl/classifier/SpeedEvaluator.java | 36 + .../impl/classifier/SpeedRatioEvaluator.java | 39 + .../impl/classifier/SpeedVarianceEvaluator.java | 36 + .../incallui/answer/impl/classifier/Stroke.java | 72 + .../answer/impl/classifier/StrokeClassifier.java | 28 + .../incallui/answer/impl/hint/AndroidManifest.xml | 13 + .../incallui/answer/impl/hint/AnswerHint.java | 46 + .../answer/impl/hint/AnswerHintFactory.java | 133 ++ .../incallui/answer/impl/hint/DotAnswerHint.java | 283 +++ .../incallui/answer/impl/hint/EmptyAnswerHint.java | 39 + .../incallui/answer/impl/hint/EventAnswerHint.java | 235 +++ .../answer/impl/hint/EventPayloadLoader.java | 30 + .../answer/impl/hint/EventPayloadLoaderImpl.java | 118 ++ .../answer/impl/hint/EventSecretCodeListener.java | 67 + .../impl/hint/res/drawable/answer_hint_large.xml | 4 + .../impl/hint/res/drawable/answer_hint_mid.xml | 4 + .../impl/hint/res/drawable/answer_hint_small.xml | 5 + .../answer/impl/hint/res/layout/dot_hint.xml | 30 + .../answer/impl/hint/res/layout/event_hint.xml | 36 + .../answer/impl/hint/res/values/dimens.xml | 12 + .../answer/impl/hint/res/values/strings.xml | 5 + .../impl/res/anim/incoming_unlocked_icon_entry.xml | 19 + .../impl/res/anim/incoming_unlocked_text_entry.xml | 9 + .../answer/impl/res/layout/fragment_avatar.xml | 26 + .../impl/res/layout/fragment_custom_sms_dialog.xml | 14 + .../impl/res/layout/fragment_incoming_call.xml | 152 ++ .../answer/impl/res/values-h240dp/dimens.xml | 21 + .../answer/impl/res/values-h300dp/dimens.xml | 20 + .../answer/impl/res/values-h480dp/dimens.xml | 22 + .../answer/impl/res/values-h540dp/dimens.xml | 21 + .../incallui/answer/impl/res/values/dimens.xml | 25 + .../incallui/answer/impl/res/values/strings.xml | 26 + .../answer/impl/utils/FlingAnimationUtils.java | 293 +++ .../incallui/answer/impl/utils/Interpolators.java | 30 + .../incallui/answer/protocol/AnswerScreen.java | 38 + .../answer/protocol/AnswerScreenDelegate.java | 44 + .../protocol/AnswerScreenDelegateFactory.java | 23 + .../AnswerProximitySensor.java | 150 ++ .../AnswerProximityWakeLock.java | 37 + .../PseudoProximityWakeLock.java | 85 + .../answerproximitysensor/PseudoScreenState.java | 66 + .../SystemProximityWakeLock.java | 90 + .../android/incallui/async/PausableExecutor.java | 56 + .../incallui/async/PausableExecutorImpl.java | 40 + .../incallui/audioroute/AndroidManifest.xml | 3 + .../AudioRouteSelectorDialogFragment.java | 114 ++ .../drawable-hdpi/ic_phone_audio_grey600_24dp.png | Bin 0 -> 990 bytes .../drawable-mdpi/ic_phone_audio_grey600_24dp.png | Bin 0 -> 632 bytes .../drawable-xhdpi/ic_phone_audio_grey600_24dp.png | Bin 0 -> 1297 bytes .../ic_phone_audio_grey600_24dp.png | Bin 0 -> 1979 bytes .../audioroute/res/layout/audioroute_selector.xml | 37 + .../incallui/audioroute/res/values/strings.xml | 7 + .../incallui/audioroute/res/values/styles.xml | 14 + .../incallui/autoresizetext/AndroidManifest.xml | 25 + .../autoresizetext/AutoResizeTextView.java | 316 ++++ .../incallui/autoresizetext/res/values/attrs.xml | 47 + java/com/android/incallui/baseui/BaseFragment.java | 75 + java/com/android/incallui/baseui/Presenter.java | 54 + java/com/android/incallui/baseui/Ui.java | 20 + .../android/incallui/bindings/ContactUtils.java | 33 + .../android/incallui/bindings/DistanceHelper.java | 36 + .../incallui/bindings/InCallUiBindings.java | 48 + .../incallui/bindings/InCallUiBindingsFactory.java | 26 + .../incallui/bindings/InCallUiBindingsStub.java | 81 + .../incallui/bindings/PhoneNumberService.java | 77 + java/com/android/incallui/call/CallList.java | 763 ++++++++ java/com/android/incallui/call/DialerCall.java | 1401 +++++++++++++++ .../android/incallui/call/DialerCallDelegate.java | 25 + .../android/incallui/call/DialerCallListener.java | 39 + .../android/incallui/call/ExternalCallList.java | 136 ++ .../incallui/call/InCallServiceListener.java | 40 + .../incallui/call/InCallUiLegacyBindings.java | 26 + .../call/InCallUiLegacyBindingsFactory.java | 26 + .../incallui/call/InCallUiLegacyBindingsStub.java | 24 + .../incallui/call/InCallVideoCallCallback.java | 197 ++ .../call/InCallVideoCallCallbackNotifier.java | 279 +++ java/com/android/incallui/call/TelecomAdapter.java | 160 ++ java/com/android/incallui/call/VideoUtils.java | 151 ++ .../incallui/commontheme/AndroidManifest.xml | 3 + .../commontheme/res/animator/button_state.xml | 30 + .../commontheme/res/animator/disabled_alpha.xml | 22 + .../commontheme/res/color/incall_button_ripple.xml | 5 + .../commontheme/res/color/incall_button_white.xml | 5 + .../drawable-hdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 1010 bytes .../drawable-mdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 682 bytes .../drawable-xhdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 1362 bytes .../drawable-xxhdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 2259 bytes .../drawable-xxxhdpi/ic_phone_audio_white_36dp.png | Bin 0 -> 3156 bytes .../res/drawable/answer_answer_background.xml | 10 + .../res/drawable/answer_decline_background.xml | 10 + .../res/drawable/incall_end_call_background.xml | 10 + .../res/values-w260dp-h520dp/dimens.xml | 21 + .../res/values-w520dp-h260dp-land/dimens.xml | 21 + .../incallui/commontheme/res/values/colors.xml | 5 + .../incallui/commontheme/res/values/dimens.xml | 22 + .../incallui/commontheme/res/values/strings.xml | 35 + .../incallui/commontheme/res/values/styles.xml | 58 + .../incallui/contactgrid/AndroidManifest.xml | 3 + .../android/incallui/contactgrid/BottomRow.java | 142 ++ .../incallui/contactgrid/ContactGridManager.java | 315 ++++ java/com/android/incallui/contactgrid/TopRow.java | 168 ++ .../res/layout/incall_contactgrid_bottom_row.xml | 71 + .../res/layout/incall_contactgrid_top_row.xml | 26 + .../incallui/contactgrid/res/values/ids.xml | 31 + .../incallui/contactgrid/res/values/strings.xml | 69 + java/com/android/incallui/hold/AndroidManifest.xml | 3 + java/com/android/incallui/hold/OnHoldFragment.java | 102 ++ .../hold/res/layout/incall_on_hold_banner.xml | 46 + .../android/incallui/hold/res/values/strings.xml | 6 + .../incallui/incall/bindings/InCallBindings.java | 28 + .../incallui/incall/impl/AndroidManifest.xml | 3 + .../AutoValue_MappedButtonConfig_MappingInfo.java | 135 ++ .../incallui/incall/impl/ButtonChooser.java | 114 ++ .../incallui/incall/impl/ButtonChooserFactory.java | 100 ++ .../incallui/incall/impl/ButtonController.java | 584 ++++++ .../incall/impl/CheckableLabeledButton.java | 286 +++ .../incall/impl/InCallButtonGridFragment.java | 137 ++ .../incallui/incall/impl/InCallFragment.java | 501 ++++++ .../incallui/incall/impl/InCallPagerAdapter.java | 59 + .../incallui/incall/impl/MappedButtonConfig.java | 193 ++ .../impl/res/animator/incall_button_elevation.xml | 31 + .../incall/impl/res/color/incall_button_icon.xml | 5 + .../impl/res/drawable-mdpi/ic_addcall_white.png | Bin 0 -> 708 bytes .../impl/res/drawable-xhdpi/ic_addcall_white.png | Bin 0 -> 1259 bytes .../impl/res/drawable/incall_button_background.xml | 22 + .../drawable/incall_button_background_checked.xml | 5 + .../res/drawable/incall_button_background_more.xml | 30 + .../incall_button_background_unchecked.xml | 5 + .../impl/res/drawable/incall_ic_add_call.xml | 4 + .../incall/impl/res/drawable/incall_ic_dialpad.xml | 4 + .../incall/impl/res/drawable/incall_ic_manage.xml | 4 + .../incall/impl/res/drawable/incall_ic_merge.xml | 4 + .../incall/impl/res/drawable/incall_ic_pause.xml | 4 + .../impl/res/drawable/tab_indicator_default.xml | 12 + .../impl/res/drawable/tab_indicator_selected.xml | 12 + .../incall/impl/res/drawable/tab_selector.xml | 6 + .../res/layout/call_composer_data_fragment.xml | 15 + .../incall/impl/res/layout/frag_incall_voice.xml | 104 ++ .../incall/impl/res/layout/incall_button_grid.xml | 77 + .../incall/impl/res/values-h320dp/dimens.xml | 5 + .../incall/impl/res/values-h385dp/dimens.xml | 5 + .../incall/impl/res/values-h480dp/dimens.xml | 4 + .../incall/impl/res/values-h580dp/dimens.xml | 4 + .../incall/impl/res/values-h580dp/styles.xml | 24 + .../impl/res/values-w260dp-h520dp/dimens.xml | 7 + .../impl/res/values-w300dp-h540dp/dimens.xml | 5 + .../incallui/incall/impl/res/values/attrs.xml | 8 + .../incallui/incall/impl/res/values/dimens.xml | 17 + .../incallui/incall/impl/res/values/ids.xml | 6 + .../incallui/incall/impl/res/values/strings.xml | 56 + .../incallui/incall/impl/res/values/styles.xml | 23 + .../incallui/incall/protocol/ContactPhotoType.java | 35 + .../incallui/incall/protocol/InCallButtonIds.java | 59 + .../incall/protocol/InCallButtonIdsExtension.java | 61 + .../incallui/incall/protocol/InCallButtonUi.java | 50 + .../incall/protocol/InCallButtonUiDelegate.java | 67 + .../protocol/InCallButtonUiDelegateFactory.java | 23 + .../incallui/incall/protocol/InCallScreen.java | 53 + .../incall/protocol/InCallScreenDelegate.java | 43 + .../protocol/InCallScreenDelegateFactory.java | 23 + .../incallui/incall/protocol/PrimaryCallState.java | 114 ++ .../incallui/incall/protocol/PrimaryInfo.java | 112 ++ .../incallui/incall/protocol/SecondaryInfo.java | 109 ++ .../incallui/latencyreport/LatencyReport.java | 140 ++ .../BlockedNumberContentObserver.java | 105 ++ .../legacyblocking/DeleteBlockedCallTask.java | 124 ++ .../android/incallui/maps/StaticMapBinding.java | 51 + .../android/incallui/maps/StaticMapFactory.java | 28 + .../incallui/res/anim/activity_open_enter.xml | 43 + .../incallui/res/anim/activity_open_exit.xml | 31 + .../android/incallui/res/anim/decelerate_cubic.xml | 21 + .../android/incallui/res/anim/decelerate_quint.xml | 21 + .../android/incallui/res/anim/on_going_call.xml | 31 + .../android/incallui/res/color/ota_title_color.xml | 21 + .../res/drawable-hdpi/ic_block_grey600_24dp.png | Bin 0 -> 518 bytes .../res/drawable-hdpi/ic_call_end_white_24dp.png | Bin 0 -> 454 bytes .../res/drawable-hdpi/ic_call_split_white_24dp.png | Bin 0 -> 326 bytes .../res/drawable-hdpi/ic_close_grey600_24dp.png | Bin 0 -> 225 bytes .../drawable-hdpi/ic_location_on_white_24dp.png | Bin 0 -> 371 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 577 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 650 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 803 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 1009 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 946 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 856 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 577 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 577 bytes .../res/drawable-hdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 577 bytes .../drawable-hdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 300 bytes .../drawable-hdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 458 bytes .../res/drawable-hdpi/ic_question_mark.png | Bin 0 -> 845 bytes .../res/drawable-hdpi/ic_schedule_white_24dp.png | Bin 0 -> 575 bytes .../incallui/res/drawable-hdpi/img_business.png | Bin 0 -> 3311 bytes .../incallui/res/drawable-hdpi/img_conference.png | Bin 0 -> 7037 bytes .../incallui/res/drawable-hdpi/img_no_image.png | Bin 0 -> 5362 bytes .../incallui/res/drawable-hdpi/img_phone.png | Bin 0 -> 6157 bytes .../res/drawable-mdpi/ic_block_grey600_24dp.png | Bin 0 -> 348 bytes .../res/drawable-mdpi/ic_call_end_white_24dp.png | Bin 0 -> 315 bytes .../res/drawable-mdpi/ic_call_split_white_24dp.png | Bin 0 -> 256 bytes .../res/drawable-mdpi/ic_close_grey600_24dp.png | Bin 0 -> 178 bytes .../drawable-mdpi/ic_location_on_white_24dp.png | Bin 0 -> 265 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 401 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 501 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 638 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 572 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 548 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 375 bytes .../res/drawable-mdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 375 bytes .../drawable-mdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 211 bytes .../drawable-mdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 346 bytes .../res/drawable-mdpi/ic_question_mark.png | Bin 0 -> 569 bytes .../res/drawable-mdpi/ic_schedule_white_24dp.png | Bin 0 -> 377 bytes .../incallui/res/drawable-mdpi/img_business.png | Bin 0 -> 2240 bytes .../incallui/res/drawable-mdpi/img_conference.png | Bin 0 -> 4629 bytes .../incallui/res/drawable-mdpi/img_no_image.png | Bin 0 -> 3509 bytes .../incallui/res/drawable-mdpi/img_phone.png | Bin 0 -> 3798 bytes .../res/drawable-xhdpi/ic_block_grey600_24dp.png | Bin 0 -> 690 bytes .../res/drawable-xhdpi/ic_call_end_white_24dp.png | Bin 0 -> 534 bytes .../drawable-xhdpi/ic_call_split_white_24dp.png | Bin 0 -> 377 bytes .../res/drawable-xhdpi/ic_close_grey600_24dp.png | Bin 0 -> 261 bytes .../drawable-xhdpi/ic_location_on_white_24dp.png | Bin 0 -> 456 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 806 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 1017 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 1313 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 1218 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 1098 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 730 bytes .../drawable-xhdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 341 bytes .../drawable-xhdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 584 bytes .../res/drawable-xhdpi/ic_question_mark.png | Bin 0 -> 1094 bytes .../res/drawable-xhdpi/ic_schedule_white_24dp.png | Bin 0 -> 737 bytes .../incallui/res/drawable-xhdpi/img_business.png | Bin 0 -> 4759 bytes .../incallui/res/drawable-xhdpi/img_conference.png | Bin 0 -> 9517 bytes .../incallui/res/drawable-xhdpi/img_no_image.png | Bin 0 -> 7369 bytes .../incallui/res/drawable-xhdpi/img_phone.png | Bin 0 -> 8189 bytes .../res/drawable-xxhdpi/ic_block_grey600_24dp.png | Bin 0 -> 1029 bytes .../res/drawable-xxhdpi/ic_call_end_white_24dp.png | Bin 0 -> 736 bytes .../drawable-xxhdpi/ic_call_split_white_24dp.png | Bin 0 -> 461 bytes .../res/drawable-xxhdpi/ic_close_grey600_24dp.png | Bin 0 -> 353 bytes .../drawable-xxhdpi/ic_location_on_white_24dp.png | Bin 0 -> 675 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_01.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_02.png | Bin 0 -> 1198 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_03.png | Bin 0 -> 1524 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_04.png | Bin 0 -> 2045 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_05.png | Bin 0 -> 1900 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_06.png | Bin 0 -> 1675 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_07.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_08.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_ongoing_phone_24px_09.png | Bin 0 -> 1051 bytes .../drawable-xxhdpi/ic_person_add_grey600_24dp.png | Bin 0 -> 485 bytes .../drawable-xxhdpi/ic_phone_paused_white_24dp.png | Bin 0 -> 842 bytes .../res/drawable-xxhdpi/ic_question_mark.png | Bin 0 -> 1686 bytes .../res/drawable-xxhdpi/ic_schedule_white_24dp.png | Bin 0 -> 1107 bytes .../incallui/res/drawable-xxhdpi/img_business.png | Bin 0 -> 6499 bytes .../res/drawable-xxhdpi/img_conference.png | Bin 0 -> 16306 bytes .../incallui/res/drawable-xxhdpi/img_no_image.png | Bin 0 -> 9850 bytes .../incallui/res/drawable-xxhdpi/img_phone.png | Bin 0 -> 10848 bytes .../res/drawable-xxxhdpi/ic_block_grey600_24dp.png | Bin 0 -> 1353 bytes .../drawable-xxxhdpi/ic_call_end_white_24dp.png | Bin 0 -> 929 bytes .../drawable-xxxhdpi/ic_call_split_white_24dp.png | Bin 0 -> 646 bytes .../res/drawable-xxxhdpi/ic_close_grey600_24dp.png | Bin 0 -> 444 bytes .../drawable-xxxhdpi/ic_location_on_white_24dp.png | Bin 0 -> 869 bytes .../ic_person_add_grey600_24dp.png | Bin 0 -> 638 bytes .../res/drawable-xxxhdpi/ic_question_mark.png | Bin 0 -> 2304 bytes .../drawable-xxxhdpi/ic_schedule_white_24dp.png | Bin 0 -> 1478 bytes .../incallui/res/drawable-xxxhdpi/img_business.png | Bin 0 -> 10730 bytes .../res/drawable-xxxhdpi/img_conference.png | Bin 0 -> 19584 bytes .../incallui/res/drawable-xxxhdpi/img_no_image.png | Bin 0 -> 16251 bytes .../incallui/res/drawable-xxxhdpi/img_phone.png | Bin 0 -> 18635 bytes .../res/drawable/img_conference_automirrored.xml | 21 + .../res/drawable/img_no_image_automirrored.xml | 21 + .../res/drawable/incall_background_gradient.xml | 8 + .../res/drawable/spam_notification_icon.xml | 34 + .../res/drawable/unknown_notification_icon.xml | 34 + .../res/layout/activity_manage_conference.xml | 6 + .../incallui/res/layout/caller_in_conference.xml | 119 ++ .../res/layout/conference_manager_fragment.xml | 33 + .../res/layout/incall_dialpad_fragment.xml | 24 + .../android/incallui/res/layout/incall_screen.xml | 33 + .../res/layout/video_call_lte_to_wifi_failed.xml | 28 + .../android/incallui/res/values-sw360dp/dimens.xml | 32 + .../incallui/res/values-w500dp-land/colors.xml | 21 + .../incallui/res/values-w500dp-land/dimens.xml | 23 + .../incallui/res/values/animation_constants.xml | 19 + java/com/android/incallui/res/values/colors.xml | 92 + java/com/android/incallui/res/values/config.xml | 23 + java/com/android/incallui/res/values/dimens.xml | 66 + java/com/android/incallui/res/values/strings.xml | 367 ++++ java/com/android/incallui/res/values/styles.xml | 80 + .../incallui/ringtone/DialerRingtoneManager.java | 134 ++ .../incallui/ringtone/InCallTonePlayer.java | 168 ++ .../incallui/ringtone/ToneGeneratorFactory.java | 34 + .../incallui/sessiondata/AndroidManifest.xml | 18 + .../incallui/sessiondata/AvatarPresenter.java | 31 + .../incallui/sessiondata/MultimediaFragment.java | 231 +++ .../res/drawable/answer_data_background.xml | 22 + .../res/layout/fragment_composer_frag.xml | 42 + .../res/layout/fragment_composer_image.xml | 50 + .../res/layout/fragment_composer_image_frag.xml | 59 + .../res/layout/fragment_composer_text.xml | 43 + .../res/layout/fragment_composer_text_frag.xml | 61 + .../res/layout/fragment_composer_text_image.xml | 62 + .../layout/fragment_composer_text_image_frag.xml | 78 + .../incallui/sessiondata/res/values/dimens.xml | 21 + .../incallui/sessiondata/res/values/ids.xml | 23 + .../incallui/sessiondata/res/values/styles.xml | 24 + .../incallui/spam/NumberInCallHistoryTask.java | 107 ++ .../incallui/spam/SpamCallListListener.java | 364 ++++ .../incallui/spam/SpamNotificationActivity.java | 483 +++++ .../incallui/spam/SpamNotificationService.java | 132 ++ .../android/incallui/util/AccessibilityUtil.java | 35 + .../com/android/incallui/util/TelecomCallUtil.java | 51 + .../incallui/video/bindings/VideoBindings.java | 28 + .../incallui/video/impl/AndroidManifest.xml | 3 + .../video/impl/CameraPermissionDialogFragment.java | 62 + .../incallui/video/impl/CheckableImageButton.java | 222 +++ .../video/impl/SpeakerButtonController.java | 118 ++ .../video/impl/SwitchOnHoldCallController.java | 91 + .../incallui/video/impl/VideoCallFragment.java | 1215 +++++++++++++ .../impl/res/color/videocall_button_icon_tint.xml | 5 + .../impl/res/drawable-hdpi/ic_switch_camera.png | Bin 0 -> 1930 bytes .../res/drawable-hdpi/video_button_bg_checked.png | Bin 0 -> 3103 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 3304 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 4836 bytes .../res/drawable-hdpi/video_button_bg_default.png | Bin 0 -> 4209 bytes .../res/drawable-hdpi/video_button_bg_disabled.png | Bin 0 -> 4022 bytes .../res/drawable-hdpi/video_button_bg_pressed.png | Bin 0 -> 5695 bytes .../impl/res/drawable-mdpi/ic_switch_camera.png | Bin 0 -> 1293 bytes .../res/drawable-mdpi/video_button_bg_checked.png | Bin 0 -> 1426 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 1715 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 2724 bytes .../res/drawable-mdpi/video_button_bg_default.png | Bin 0 -> 2155 bytes .../res/drawable-mdpi/video_button_bg_disabled.png | Bin 0 -> 1990 bytes .../res/drawable-mdpi/video_button_bg_pressed.png | Bin 0 -> 3188 bytes .../impl/res/drawable-xhdpi/ic_switch_camera.png | Bin 0 -> 2518 bytes .../res/drawable-xhdpi/video_button_bg_checked.png | Bin 0 -> 4603 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 4957 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 7213 bytes .../res/drawable-xhdpi/video_button_bg_default.png | Bin 0 -> 6352 bytes .../drawable-xhdpi/video_button_bg_disabled.png | Bin 0 -> 6054 bytes .../res/drawable-xhdpi/video_button_bg_pressed.png | Bin 0 -> 8418 bytes .../impl/res/drawable-xxhdpi/ic_switch_camera.png | Bin 0 -> 4001 bytes .../drawable-xxhdpi/video_button_bg_checked.png | Bin 0 -> 9032 bytes .../video_button_bg_checked_disabled.png | Bin 0 -> 8611 bytes .../video_button_bg_checked_pressed.png | Bin 0 -> 13529 bytes .../drawable-xxhdpi/video_button_bg_default.png | Bin 0 -> 11101 bytes .../drawable-xxhdpi/video_button_bg_disabled.png | Bin 0 -> 10736 bytes .../drawable-xxhdpi/video_button_bg_pressed.png | Bin 0 -> 15167 bytes .../impl/res/drawable-xxxhdpi/ic_switch_camera.png | Bin 0 -> 2424 bytes .../drawable/videocall_background_circle_white.xml | 10 + .../drawable/videocall_video_button_background.xml | 27 + .../impl/res/layout-v21/switch_camera_button.xml | 6 + .../video/impl/res/layout/frag_videocall.xml | 114 ++ .../video/impl/res/layout/frag_videocall_land.xml | 111 ++ .../video/impl/res/layout/switch_camera_button.xml | 6 + .../video/impl/res/layout/video_contact_grid.xml | 33 + .../video/impl/res/layout/videocall_controls.xml | 113 ++ .../impl/res/layout/videocall_controls_land.xml | 115 ++ .../video/impl/res/values-h580dp/dimens.xml | 7 + .../video/impl/res/values-w460dp/dimens.xml | 7 + .../incallui/video/impl/res/values/attrs.xml | 8 + .../incallui/video/impl/res/values/dimens.xml | 10 + .../incallui/video/impl/res/values/strings.xml | 28 + .../incallui/video/impl/res/values/styles.xml | 11 + .../incallui/video/protocol/VideoCallScreen.java | 36 + .../video/protocol/VideoCallScreenDelegate.java | 48 + .../protocol/VideoCallScreenDelegateFactory.java | 23 + .../bindings/VideoSurfaceBindings.java | 44 + .../incallui/videosurface/impl/VideoScale.java | 147 ++ .../videosurface/impl/VideoSurfaceTextureImpl.java | 249 +++ .../protocol/VideoSurfaceDelegate.java | 29 + .../videosurface/protocol/VideoSurfaceTexture.java | 57 + java/com/android/incallui/wifi/AndroidManifest.xml | 3 + .../incallui/wifi/EnableWifiCallingPrompt.java | 82 + .../android/incallui/wifi/res/values/strings.xml | 9 + java/com/android/voicemailomtp/ActivationTask.java | 305 ++++ java/com/android/voicemailomtp/AndroidManifest.xml | 105 ++ java/com/android/voicemailomtp/Assert.java | 62 + .../voicemailomtp/DefaultOmtpEventHandler.java | 202 +++ .../android/voicemailomtp/NeededForTesting.java | 25 + java/com/android/voicemailomtp/OmtpConstants.java | 248 +++ java/com/android/voicemailomtp/OmtpEvents.java | 156 ++ java/com/android/voicemailomtp/OmtpService.java | 65 + .../voicemailomtp/OmtpVvmCarrierConfigHelper.java | 423 +++++ .../voicemailomtp/SubscriptionInfoHelper.java | 75 + .../voicemailomtp/TelephonyManagerStub.java | 80 + .../voicemailomtp/TelephonyVvmConfigManager.java | 154 ++ .../voicemailomtp/VisualVoicemailPreferences.java | 143 ++ java/com/android/voicemailomtp/Voicemail.java | 330 ++++ .../com/android/voicemailomtp/VoicemailStatus.java | 158 ++ java/com/android/voicemailomtp/VvmLog.java | 179 ++ .../voicemailomtp/VvmPackageInstallReceiver.java | 70 + .../voicemailomtp/VvmPhoneStateListener.java | 103 ++ .../fetch/FetchVoicemailReceiver.java | 219 +++ .../fetch/VoicemailFetchedCallback.java | 101 ++ .../com/android/voicemailomtp/imap/ImapHelper.java | 711 ++++++++ .../voicemailomtp/imap/VoicemailPayload.java | 38 + java/com/android/voicemailomtp/mail/Address.java | 541 ++++++ .../mail/AuthenticationFailedException.java | 33 + .../com/android/voicemailomtp/mail/Base64Body.java | 62 + java/com/android/voicemailomtp/mail/Body.java | 25 + java/com/android/voicemailomtp/mail/BodyPart.java | 24 + .../mail/CertificateValidationException.java | 29 + .../android/voicemailomtp/mail/FetchProfile.java | 84 + java/com/android/voicemailomtp/mail/Fetchable.java | 23 + .../voicemailomtp/mail/FixedLengthInputStream.java | 79 + java/com/android/voicemailomtp/mail/Flag.java | 29 + .../android/voicemailomtp/mail/MailTransport.java | 344 ++++ .../android/voicemailomtp/mail/MeetingInfo.java | 29 + java/com/android/voicemailomtp/mail/Message.java | 144 ++ .../voicemailomtp/mail/MessageDateComparator.java | 34 + .../voicemailomtp/mail/MessagingException.java | 139 ++ java/com/android/voicemailomtp/mail/Multipart.java | 62 + .../android/voicemailomtp/mail/PackedString.java | 175 ++ java/com/android/voicemailomtp/mail/Part.java | 51 + .../voicemailomtp/mail/PeekableInputStream.java | 80 + .../android/voicemailomtp/mail/TempDirectory.java | 41 + .../mail/internet/BinaryTempFileBody.java | 91 + .../voicemailomtp/mail/internet/MimeBodyPart.java | 207 +++ .../voicemailomtp/mail/internet/MimeHeader.java | 161 ++ .../voicemailomtp/mail/internet/MimeMessage.java | 675 +++++++ .../voicemailomtp/mail/internet/MimeMultipart.java | 112 ++ .../voicemailomtp/mail/internet/MimeUtility.java | 416 +++++ .../voicemailomtp/mail/internet/TextBody.java | 63 + .../voicemailomtp/mail/store/ImapConnection.java | 413 +++++ .../voicemailomtp/mail/store/ImapFolder.java | 784 ++++++++ .../voicemailomtp/mail/store/ImapStore.java | 176 ++ .../mail/store/imap/DigestMd5Utils.java | 335 ++++ .../mail/store/imap/ImapConstants.java | 144 ++ .../voicemailomtp/mail/store/imap/ImapElement.java | 120 ++ .../voicemailomtp/mail/store/imap/ImapList.java | 235 +++ .../mail/store/imap/ImapMemoryLiteral.java | 76 + .../mail/store/imap/ImapResponse.java | 158 ++ .../mail/store/imap/ImapResponseParser.java | 432 +++++ .../mail/store/imap/ImapSimpleString.java | 62 + .../voicemailomtp/mail/store/imap/ImapString.java | 192 ++ .../mail/store/imap/ImapTempFileLiteral.java | 123 ++ .../voicemailomtp/mail/store/imap/ImapUtility.java | 125 ++ .../mail/utility/CountingOutputStream.java | 48 + .../mail/utility/EOLConvertingOutputStream.java | 48 + .../android/voicemailomtp/mail/utils/LogUtils.java | 413 +++++ .../android/voicemailomtp/mail/utils/Utility.java | 80 + java/com/android/voicemailomtp/permissions.xml | 21 + .../voicemailomtp/protocol/CvvmProtocol.java | 59 + .../voicemailomtp/protocol/OmtpProtocol.java | 37 + .../voicemailomtp/protocol/ProtocolHelper.java | 43 + .../protocol/VisualVoicemailProtocol.java | 100 ++ .../protocol/VisualVoicemailProtocolFactory.java | 47 + .../voicemailomtp/protocol/Vvm3EventHandler.java | 271 +++ .../voicemailomtp/protocol/Vvm3Protocol.java | 301 ++++ .../voicemailomtp/protocol/Vvm3Subscriber.java | 326 ++++ .../res/layout/voicemail_change_pin.xml | 97 + .../android/voicemailomtp/res/values/arrays.xml | 19 + .../com/android/voicemailomtp/res/values/attrs.xml | 20 + .../android/voicemailomtp/res/values/colors.xml | 19 + .../android/voicemailomtp/res/values/config.xml | 19 + .../android/voicemailomtp/res/values/dimens.xml | 19 + java/com/android/voicemailomtp/res/values/ids.xml | 20 + .../android/voicemailomtp/res/values/strings.xml | 86 + .../android/voicemailomtp/res/values/styles.xml | 19 + .../voicemailomtp/res/xml/voicemail_settings.xml | 27 + .../android/voicemailomtp/res/xml/vvm_config.xml | 134 ++ .../android/voicemailomtp/scheduling/BaseTask.java | 206 +++ .../voicemailomtp/scheduling/BlockerTask.java | 55 + .../scheduling/MinimalIntervalPolicy.java | 69 + .../android/voicemailomtp/scheduling/Policy.java | 36 + .../voicemailomtp/scheduling/PostponePolicy.java | 69 + .../voicemailomtp/scheduling/RetryPolicy.java | 117 ++ .../com/android/voicemailomtp/scheduling/Task.java | 133 ++ .../scheduling/TaskSchedulerService.java | 392 ++++ .../settings/VisualVoicemailSettingsUtil.java | 77 + .../settings/VoicemailChangePinActivity.java | 634 +++++++ .../settings/VoicemailSettingsActivity.java | 222 +++ .../voicemailomtp/sms/LegacyModeSmsHandler.java | 67 + .../voicemailomtp/sms/OmtpCvvmMessageSender.java | 55 + .../voicemailomtp/sms/OmtpMessageReceiver.java | 162 ++ .../voicemailomtp/sms/OmtpMessageSender.java | 89 + .../sms/OmtpStandardMessageSender.java | 119 ++ .../android/voicemailomtp/sms/StatusMessage.java | 209 +++ .../voicemailomtp/sms/StatusSmsFetcher.java | 162 ++ .../com/android/voicemailomtp/sms/SyncMessage.java | 166 ++ .../voicemailomtp/sms/Vvm3MessageSender.java | 56 + .../src/org/apache/commons/io/IOUtils.java | 1202 +++++++++++++ .../org/apache/james/mime4j/BodyDescriptor.java | 392 ++++ .../james/mime4j/CloseShieldInputStream.java | 129 ++ .../org/apache/james/mime4j/ContentHandler.java | 177 ++ .../james/mime4j/EOLConvertingInputStream.java | 139 ++ .../src/org/apache/james/mime4j/Log.java | 114 ++ .../src/org/apache/james/mime4j/LogFactory.java | 29 + .../james/mime4j/MimeBoundaryInputStream.java | 184 ++ .../org/apache/james/mime4j/MimeStreamParser.java | 324 ++++ .../org/apache/james/mime4j/RootInputStream.java | 111 ++ .../org/apache/james/mime4j/codec/EncoderUtil.java | 630 +++++++ .../james/mime4j/decoder/Base64InputStream.java | 151 ++ .../org/apache/james/mime4j/decoder/ByteQueue.java | 62 + .../apache/james/mime4j/decoder/DecoderUtil.java | 284 +++ .../mime4j/decoder/QuotedPrintableInputStream.java | 229 +++ .../mime4j/decoder/UnboundedFifoByteBuffer.java | 272 +++ .../james/mime4j/field/AddressListField.java | 65 + .../mime4j/field/ContentTransferEncodingField.java | 88 + .../james/mime4j/field/ContentTypeField.java | 259 +++ .../apache/james/mime4j/field/DateTimeField.java | 73 + .../james/mime4j/field/DefaultFieldParser.java | 45 + .../james/mime4j/field/DelegatingFieldParser.java | 47 + .../src/org/apache/james/mime4j/field/Field.java | 192 ++ .../org/apache/james/mime4j/field/FieldParser.java | 21 + .../apache/james/mime4j/field/MailboxField.java | 70 + .../james/mime4j/field/MailboxListField.java | 67 + .../james/mime4j/field/UnstructuredField.java | 49 + .../apache/james/mime4j/field/address/Address.java | 52 + .../james/mime4j/field/address/AddressList.java | 138 ++ .../apache/james/mime4j/field/address/Builder.java | 243 +++ .../james/mime4j/field/address/DomainList.java | 76 + .../apache/james/mime4j/field/address/Group.java | 75 + .../apache/james/mime4j/field/address/Mailbox.java | 121 ++ .../james/mime4j/field/address/MailboxList.java | 71 + .../james/mime4j/field/address/NamedMailbox.java | 71 + .../mime4j/field/address/parser/ASTaddr_spec.java | 19 + .../mime4j/field/address/parser/ASTaddress.java | 19 + .../field/address/parser/ASTaddress_list.java | 19 + .../mime4j/field/address/parser/ASTangle_addr.java | 19 + .../mime4j/field/address/parser/ASTdomain.java | 19 + .../mime4j/field/address/parser/ASTgroup_body.java | 19 + .../mime4j/field/address/parser/ASTlocal_part.java | 19 + .../mime4j/field/address/parser/ASTmailbox.java | 19 + .../mime4j/field/address/parser/ASTname_addr.java | 19 + .../mime4j/field/address/parser/ASTphrase.java | 19 + .../mime4j/field/address/parser/ASTroute.java | 19 + .../field/address/parser/AddressListParser.java | 977 ++++++++++ .../field/address/parser/AddressListParser.jj | 595 ++++++ .../address/parser/AddressListParserConstants.java | 76 + .../parser/AddressListParserTokenManager.java | 1009 +++++++++++ .../parser/AddressListParserTreeConstants.java | 35 + .../address/parser/AddressListParserVisitor.java | 19 + .../mime4j/field/address/parser/BaseNode.java | 30 + .../address/parser/JJTAddressListParserState.java | 123 ++ .../james/mime4j/field/address/parser/Node.java | 37 + .../field/address/parser/ParseException.java | 207 +++ .../field/address/parser/SimpleCharStream.java | 454 +++++ .../mime4j/field/address/parser/SimpleNode.java | 87 + .../james/mime4j/field/address/parser/Token.java | 96 + .../mime4j/field/address/parser/TokenMgrError.java | 148 ++ .../contenttype/parser/ContentTypeParser.java | 268 +++ .../parser/ContentTypeParserConstants.java | 62 + .../parser/ContentTypeParserTokenManager.java | 877 +++++++++ .../field/contenttype/parser/ParseException.java | 207 +++ .../field/contenttype/parser/SimpleCharStream.java | 454 +++++ .../mime4j/field/contenttype/parser/Token.java | 96 + .../field/contenttype/parser/TokenMgrError.java | 148 ++ .../james/mime4j/field/datetime/DateTime.java | 127 ++ .../field/datetime/parser/DateTimeParser.java | 570 ++++++ .../datetime/parser/DateTimeParserConstants.java | 86 + .../parser/DateTimeParserTokenManager.java | 882 +++++++++ .../field/datetime/parser/ParseException.java | 207 +++ .../field/datetime/parser/SimpleCharStream.java | 454 +++++ .../james/mime4j/field/datetime/parser/Token.java | 96 + .../field/datetime/parser/TokenMgrError.java | 148 ++ .../org/apache/james/mime4j/util/CharsetUtil.java | 1249 +++++++++++++ .../voicemailomtp/sync/OmtpVvmSourceManager.java | 120 ++ .../voicemailomtp/sync/OmtpVvmSyncReceiver.java | 61 + .../voicemailomtp/sync/OmtpVvmSyncService.java | 278 +++ .../android/voicemailomtp/sync/SyncOneTask.java | 82 + java/com/android/voicemailomtp/sync/SyncTask.java | 79 + .../com/android/voicemailomtp/sync/UploadTask.java | 68 + .../sync/VoicemailProviderChangeReceiver.java | 41 + .../sync/VoicemailStatusQueryHelper.java | 113 ++ .../voicemailomtp/sync/VoicemailsQueryHelper.java | 244 +++ .../voicemailomtp/sync/VvmNetworkRequest.java | 118 ++ .../sync/VvmNetworkRequestCallback.java | 171 ++ .../voicemailomtp/utils/IndentingPrintWriter.java | 160 ++ .../voicemailomtp/utils/VoicemailDatabaseUtil.java | 90 + .../voicemailomtp/utils/VvmDumpHandler.java | 46 + java/com/android/voicemailomtp/utils/XmlUtils.java | 245 +++ 1730 files changed, 162516 insertions(+) create mode 100644 java/com/android/contacts/common/AndroidManifest.xml create mode 100644 java/com/android/contacts/common/Bindings.java create mode 100644 java/com/android/contacts/common/ClipboardUtils.java create mode 100644 java/com/android/contacts/common/Collapser.java create mode 100644 java/com/android/contacts/common/ContactPhotoManager.java create mode 100644 java/com/android/contacts/common/ContactPhotoManagerImpl.java create mode 100644 java/com/android/contacts/common/ContactPresenceIconUtil.java create mode 100644 java/com/android/contacts/common/ContactStatusUtil.java create mode 100644 java/com/android/contacts/common/ContactTileLoaderFactory.java create mode 100644 java/com/android/contacts/common/ContactsUtils.java create mode 100644 java/com/android/contacts/common/GeoUtil.java create mode 100644 java/com/android/contacts/common/GroupMetaData.java create mode 100644 java/com/android/contacts/common/MoreContactUtils.java create mode 100644 java/com/android/contacts/common/bindings/ContactsCommonBindings.java create mode 100644 java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java create mode 100644 java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java create mode 100644 java/com/android/contacts/common/compat/CallCompat.java create mode 100644 java/com/android/contacts/common/compat/CallableCompat.java create mode 100644 java/com/android/contacts/common/compat/ContactsCompat.java create mode 100644 java/com/android/contacts/common/compat/DirectoryCompat.java create mode 100644 java/com/android/contacts/common/compat/PhoneAccountCompat.java create mode 100644 java/com/android/contacts/common/compat/PhoneCompat.java create mode 100644 java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java create mode 100644 java/com/android/contacts/common/compat/TelephonyManagerCompat.java create mode 100644 java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java create mode 100644 java/com/android/contacts/common/database/ContactUpdateUtils.java create mode 100644 java/com/android/contacts/common/database/EmptyCursor.java create mode 100644 java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java create mode 100644 java/com/android/contacts/common/dialog/CallSubjectDialog.java create mode 100644 java/com/android/contacts/common/dialog/ClearFrequentsDialog.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java create mode 100644 java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java create mode 100644 java/com/android/contacts/common/format/FormatUtils.java create mode 100644 java/com/android/contacts/common/format/TextHighlighter.java create mode 100644 java/com/android/contacts/common/format/testing/SpannedTestUtils.java create mode 100644 java/com/android/contacts/common/lettertiles/LetterTileDrawable.java create mode 100644 java/com/android/contacts/common/list/AutoScrollListView.java create mode 100644 java/com/android/contacts/common/list/ContactEntry.java create mode 100644 java/com/android/contacts/common/list/ContactEntryListAdapter.java create mode 100644 java/com/android/contacts/common/list/ContactEntryListFragment.java create mode 100644 java/com/android/contacts/common/list/ContactListAdapter.java create mode 100644 java/com/android/contacts/common/list/ContactListFilter.java create mode 100644 java/com/android/contacts/common/list/ContactListFilterController.java create mode 100644 java/com/android/contacts/common/list/ContactListItemView.java create mode 100644 java/com/android/contacts/common/list/ContactListPinnedHeaderView.java create mode 100644 java/com/android/contacts/common/list/ContactTileView.java create mode 100644 java/com/android/contacts/common/list/ContactsSectionIndexer.java create mode 100644 java/com/android/contacts/common/list/DefaultContactListAdapter.java create mode 100644 java/com/android/contacts/common/list/DirectoryListLoader.java create mode 100644 java/com/android/contacts/common/list/DirectoryPartition.java create mode 100644 java/com/android/contacts/common/list/IndexerListAdapter.java create mode 100644 java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java create mode 100644 java/com/android/contacts/common/list/PhoneNumberListAdapter.java create mode 100644 java/com/android/contacts/common/list/PhoneNumberPickerFragment.java create mode 100644 java/com/android/contacts/common/list/PinnedHeaderListAdapter.java create mode 100644 java/com/android/contacts/common/list/PinnedHeaderListView.java create mode 100644 java/com/android/contacts/common/list/ViewPagerTabStrip.java create mode 100644 java/com/android/contacts/common/list/ViewPagerTabs.java create mode 100644 java/com/android/contacts/common/location/CountryDetector.java create mode 100644 java/com/android/contacts/common/location/UpdateCountryService.java create mode 100644 java/com/android/contacts/common/model/AccountTypeManager.java create mode 100644 java/com/android/contacts/common/model/BuilderWrapper.java create mode 100644 java/com/android/contacts/common/model/CPOWrapper.java create mode 100644 java/com/android/contacts/common/model/Contact.java create mode 100644 java/com/android/contacts/common/model/ContactLoader.java create mode 100644 java/com/android/contacts/common/model/RawContact.java create mode 100644 java/com/android/contacts/common/model/account/AccountType.java create mode 100644 java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java create mode 100644 java/com/android/contacts/common/model/account/AccountWithDataSet.java create mode 100644 java/com/android/contacts/common/model/account/BaseAccountType.java create mode 100644 java/com/android/contacts/common/model/account/ExchangeAccountType.java create mode 100644 java/com/android/contacts/common/model/account/ExternalAccountType.java create mode 100644 java/com/android/contacts/common/model/account/FallbackAccountType.java create mode 100644 java/com/android/contacts/common/model/account/GoogleAccountType.java create mode 100644 java/com/android/contacts/common/model/account/SamsungAccountType.java create mode 100644 java/com/android/contacts/common/model/dataitem/DataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/DataKind.java create mode 100644 java/com/android/contacts/common/model/dataitem/EmailDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/EventDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/IdentityDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/ImDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/NicknameDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/NoteDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/PhoneDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/PhotoDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/RelationDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java create mode 100644 java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java create mode 100644 java/com/android/contacts/common/preference/ContactsPreferences.java create mode 100644 java/com/android/contacts/common/preference/DisplayOrderPreference.java create mode 100644 java/com/android/contacts/common/preference/SortOrderPreference.java create mode 100644 java/com/android/contacts/common/res/color/popup_menu_color.xml create mode 100644 java/com/android/contacts/common/res/color/tab_text_color.xml create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png create mode 100644 java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png create mode 100644 java/com/android/contacts/common/res/drawable/dialog_background_material.xml create mode 100644 java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_back_arrow.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_call.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_message_24dp.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_more_vert.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_search_video_call.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_tab_all.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_tab_groups.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_tab_starred.xml create mode 100644 java/com/android/contacts/common/res/drawable/ic_work_profile.xml create mode 100644 java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml create mode 100644 java/com/android/contacts/common/res/drawable/item_background_material_dark.xml create mode 100644 java/com/android/contacts/common/res/drawable/item_background_material_light.xml create mode 100644 java/com/android/contacts/common/res/drawable/list_item_activated_background.xml create mode 100644 java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml create mode 100644 java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml create mode 100644 java/com/android/contacts/common/res/drawable/unread_count_background.xml create mode 100644 java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml create mode 100644 java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml create mode 100644 java/com/android/contacts/common/res/layout/account_filter_header.xml create mode 100644 java/com/android/contacts/common/res/layout/account_selector_list_item.xml create mode 100644 java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml create mode 100644 java/com/android/contacts/common/res/layout/call_subject_history.xml create mode 100644 java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml create mode 100644 java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml create mode 100644 java/com/android/contacts/common/res/layout/contact_list_card.xml create mode 100644 java/com/android/contacts/common/res/layout/contact_list_content.xml create mode 100644 java/com/android/contacts/common/res/layout/default_account_checkbox.xml create mode 100644 java/com/android/contacts/common/res/layout/dialog_call_subject.xml create mode 100644 java/com/android/contacts/common/res/layout/directory_header.xml create mode 100644 java/com/android/contacts/common/res/layout/list_separator.xml create mode 100644 java/com/android/contacts/common/res/layout/search_bar_expanded.xml create mode 100644 java/com/android/contacts/common/res/layout/select_account_list_item.xml create mode 100644 java/com/android/contacts/common/res/layout/unread_count_tab.xml create mode 100644 java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png create mode 100644 java/com/android/contacts/common/res/values-ja/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values-ko/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values-land/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw600dp-land/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw600dp/dimens.xml create mode 100644 java/com/android/contacts/common/res/values-sw600dp/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw720dp-land/integers.xml create mode 100644 java/com/android/contacts/common/res/values-sw720dp/integers.xml create mode 100644 java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values/animation_constants.xml create mode 100644 java/com/android/contacts/common/res/values/attrs.xml create mode 100644 java/com/android/contacts/common/res/values/colors.xml create mode 100644 java/com/android/contacts/common/res/values/dimens.xml create mode 100644 java/com/android/contacts/common/res/values/donottranslate_config.xml create mode 100644 java/com/android/contacts/common/res/values/ids.xml create mode 100644 java/com/android/contacts/common/res/values/integers.xml create mode 100644 java/com/android/contacts/common/res/values/strings.xml create mode 100644 java/com/android/contacts/common/res/values/styles.xml create mode 100644 java/com/android/contacts/common/testing/InjectedServices.java create mode 100644 java/com/android/contacts/common/util/AccountFilterUtil.java create mode 100644 java/com/android/contacts/common/util/BitmapUtil.java create mode 100644 java/com/android/contacts/common/util/CommonDateUtils.java create mode 100644 java/com/android/contacts/common/util/Constants.java create mode 100644 java/com/android/contacts/common/util/ContactDisplayUtils.java create mode 100644 java/com/android/contacts/common/util/ContactListViewUtils.java create mode 100644 java/com/android/contacts/common/util/ContactLoaderUtils.java create mode 100644 java/com/android/contacts/common/util/DateUtils.java create mode 100644 java/com/android/contacts/common/util/FabUtil.java create mode 100644 java/com/android/contacts/common/util/MaterialColorMapUtils.java create mode 100644 java/com/android/contacts/common/util/NameConverter.java create mode 100644 java/com/android/contacts/common/util/SearchUtil.java create mode 100644 java/com/android/contacts/common/util/StopWatch.java create mode 100644 java/com/android/contacts/common/util/TelephonyManagerUtils.java create mode 100644 java/com/android/contacts/common/util/TrafficStatsTags.java create mode 100644 java/com/android/contacts/common/util/UriUtils.java create mode 100644 java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java create mode 100644 java/com/android/contacts/common/widget/FloatingActionButtonController.java create mode 100644 java/com/android/contacts/common/widget/LayoutSuppressingImageView.java create mode 100644 java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java create mode 100644 java/com/android/dialer/animation/AnimUtils.java create mode 100644 java/com/android/dialer/animation/AnimationListenerAdapter.java create mode 100644 java/com/android/dialer/app/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/Bindings.java create mode 100644 java/com/android/dialer/app/CallDetailActivity.java create mode 100644 java/com/android/dialer/app/DialerApplication.java create mode 100644 java/com/android/dialer/app/DialtactsActivity.java create mode 100644 java/com/android/dialer/app/FloatingActionButtonBehavior.java create mode 100644 java/com/android/dialer/app/PhoneCallDetails.java create mode 100644 java/com/android/dialer/app/SpecialCharSequenceMgr.java create mode 100644 java/com/android/dialer/app/alert/AlertManager.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindings.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindingsFactory.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindingsStub.java create mode 100644 java/com/android/dialer/app/calllog/BlockReportSpamListener.java create mode 100644 java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAdapter.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAlertManager.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAsync.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java create mode 100644 java/com/android/dialer/app/calllog/CallLogFragment.java create mode 100644 java/com/android/dialer/app/calllog/CallLogGroupBuilder.java create mode 100644 java/com/android/dialer/app/calllog/CallLogListItemHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java create mode 100644 java/com/android/dialer/app/calllog/CallLogModalAlertManager.java create mode 100644 java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallLogNotificationsService.java create mode 100644 java/com/android/dialer/app/calllog/CallLogReceiver.java create mode 100644 java/com/android/dialer/app/calllog/CallTypeHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallTypeIconsView.java create mode 100644 java/com/android/dialer/app/calllog/ClearCallLogDialog.java create mode 100644 java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java create mode 100644 java/com/android/dialer/app/calllog/GroupingListAdapter.java create mode 100644 java/com/android/dialer/app/calllog/IntentProvider.java create mode 100644 java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java create mode 100644 java/com/android/dialer/app/calllog/MissedCallNotifier.java create mode 100644 java/com/android/dialer/app/calllog/PhoneAccountUtils.java create mode 100644 java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java create mode 100644 java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java create mode 100644 java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java create mode 100644 java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java create mode 100644 java/com/android/dialer/app/calllog/VoicemailQueryHandler.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactInfoCache.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactInfoRequest.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java create mode 100644 java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java create mode 100644 java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java create mode 100644 java/com/android/dialer/app/dialpad/DialpadFragment.java create mode 100644 java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java create mode 100644 java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java create mode 100644 java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java create mode 100644 java/com/android/dialer/app/filterednumber/NumbersAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java create mode 100644 java/com/android/dialer/app/list/AllContactsFragment.java create mode 100644 java/com/android/dialer/app/list/BlockedListSearchAdapter.java create mode 100644 java/com/android/dialer/app/list/BlockedListSearchFragment.java create mode 100644 java/com/android/dialer/app/list/ContentChangedFilter.java create mode 100644 java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java create mode 100644 java/com/android/dialer/app/list/DragDropController.java create mode 100644 java/com/android/dialer/app/list/ListsFragment.java create mode 100644 java/com/android/dialer/app/list/OnDragDropListener.java create mode 100644 java/com/android/dialer/app/list/OnListFragmentScrolledListener.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteListView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteTileView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java create mode 100644 java/com/android/dialer/app/list/RegularSearchFragment.java create mode 100644 java/com/android/dialer/app/list/RegularSearchListAdapter.java create mode 100644 java/com/android/dialer/app/list/RemoveView.java create mode 100644 java/com/android/dialer/app/list/SearchFragment.java create mode 100644 java/com/android/dialer/app/list/SmartDialNumberListAdapter.java create mode 100644 java/com/android/dialer/app/list/SmartDialSearchFragment.java create mode 100644 java/com/android/dialer/app/list/SpeedDialFragment.java create mode 100644 java/com/android/dialer/app/manifests/activities/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/res/color/settings_text_color_primary.xml create mode 100644 java/com/android/dialer/app/res/color/settings_text_color_secondary.xml create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml create mode 100644 java/com/android/dialer/app/res/drawable/floating_action_button.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_pause.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_play_arrow.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_search_phone.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml create mode 100644 java/com/android/dialer/app/res/drawable/oval_ripple.xml create mode 100644 java/com/android/dialer/app/res/drawable/overflow_menu.xml create mode 100644 java/com/android/dialer/app/res/drawable/rounded_corner.xml create mode 100644 java/com/android/dialer/app/res/drawable/seekbar_drawable.xml create mode 100644 java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml create mode 100644 java/com/android/dialer/app/res/drawable/shadow_fade_left.xml create mode 100644 java/com/android/dialer/app/res/drawable/shadow_fade_up.xml create mode 100644 java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml create mode 100644 java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml create mode 100644 java/com/android/dialer/app/res/layout/all_contacts_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/all_contacts_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_footer.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_header.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_item.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_footer.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_header.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_history_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_alert_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_list_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml create mode 100644 java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml create mode 100644 java/com/android/dialer/app/res/layout/dialpad_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/dialtacts_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/empty_content_view.xml create mode 100644 java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml create mode 100644 java/com/android/dialer/app/res/layout/keyguard_preview.xml create mode 100644 java/com/android/dialer/app/res/layout/lists_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml create mode 100644 java/com/android/dialer/app/res/layout/search_edittext.xml create mode 100644 java/com/android/dialer/app/res/layout/speed_dial_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml create mode 100644 java/com/android/dialer/app/res/menu/dialpad_options.xml create mode 100644 java/com/android/dialer/app/res/menu/dialtacts_options.xml create mode 100644 java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/values/animation_constants.xml create mode 100644 java/com/android/dialer/app/res/values/attrs.xml create mode 100644 java/com/android/dialer/app/res/values/colors.xml create mode 100644 java/com/android/dialer/app/res/values/dimens.xml create mode 100644 java/com/android/dialer/app/res/values/donottranslate_config.xml create mode 100644 java/com/android/dialer/app/res/values/ids.xml create mode 100644 java/com/android/dialer/app/res/values/strings.xml create mode 100644 java/com/android/dialer/app/res/values/styles.xml create mode 100644 java/com/android/dialer/app/res/xml/display_options_settings.xml create mode 100644 java/com/android/dialer/app/res/xml/file_paths.xml create mode 100644 java/com/android/dialer/app/res/xml/searchable.xml create mode 100644 java/com/android/dialer/app/res/xml/sound_settings.xml create mode 100644 java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java create mode 100644 java/com/android/dialer/app/settings/DefaultRingtonePreference.java create mode 100644 java/com/android/dialer/app/settings/DialerSettingsActivity.java create mode 100644 java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java create mode 100644 java/com/android/dialer/app/settings/SoundSettingsFragment.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailAudioManager.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailErrorManager.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java create mode 100644 java/com/android/dialer/app/voicemail/WiredHeadsetManager.java create mode 100644 java/com/android/dialer/app/voicemail/error/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatus.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java create mode 100644 java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/dimens.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/strings.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/styles.xml create mode 100644 java/com/android/dialer/app/widget/ActionBarController.java create mode 100644 java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java create mode 100644 java/com/android/dialer/app/widget/EmptyContentView.java create mode 100644 java/com/android/dialer/app/widget/SearchEditTextLayout.java create mode 100644 java/com/android/dialer/backup/AndroidManifest.xml create mode 100644 java/com/android/dialer/backup/DialerBackupAgent.java create mode 100644 java/com/android/dialer/backup/DialerBackupUtils.java create mode 100644 java/com/android/dialer/backup/proto/VoicemailInfo.java create mode 100644 java/com/android/dialer/blocking/AndroidManifest.xml create mode 100644 java/com/android/dialer/blocking/BlockNumberDialogFragment.java create mode 100644 java/com/android/dialer/blocking/BlockReportSpamDialogs.java create mode 100644 java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java create mode 100644 java/com/android/dialer/blocking/BlockedNumbersMigrator.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberCompat.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberProvider.java create mode 100644 java/com/android/dialer/blocking/FilteredNumbersUtil.java create mode 100644 java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable/blocked_contact.xml create mode 100644 java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml create mode 100644 java/com/android/dialer/blocking/res/values/colors.xml create mode 100644 java/com/android/dialer/blocking/res/values/dimens.xml create mode 100644 java/com/android/dialer/blocking/res/values/strings.xml create mode 100644 java/com/android/dialer/buildtype/BuildType.java create mode 100644 java/com/android/dialer/buildtype/BuildTypeAccessor.java create mode 100644 java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java create mode 100644 java/com/android/dialer/callcomposer/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/CallComposerActivity.java create mode 100644 java/com/android/dialer/callcomposer/CallComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java create mode 100644 java/com/android/dialer/callcomposer/CameraComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/GalleryComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/GalleryCursorLoader.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridAdapter.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridItemData.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridItemView.java create mode 100644 java/com/android/dialer/callcomposer/MessageComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/camera/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/camera/CameraManager.java create mode 100644 java/com/android/dialer/callcomposer/camera/CameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/ImagePersistTask.java create mode 100644 java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifData.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifParser.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifReader.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifTag.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/IfdData.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/IfdId.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/Rational.java create mode 100644 java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml create mode 100644 java/com/android/dialer/callcomposer/nano/CallComposerContact.java create mode 100644 java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/permission_view.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/colors.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/strings.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/styles.xml create mode 100644 java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java create mode 100644 java/com/android/dialer/callintent/CallIntentBuilder.java create mode 100644 java/com/android/dialer/callintent/CallIntentParser.java create mode 100644 java/com/android/dialer/callintent/Constants.java create mode 100644 java/com/android/dialer/callintent/nano/CallInitiationType.java create mode 100644 java/com/android/dialer/callintent/nano/CallSpecificAppData.java create mode 100644 java/com/android/dialer/common/AndroidManifest.xml create mode 100644 java/com/android/dialer/common/Assert.java create mode 100644 java/com/android/dialer/common/AsyncTaskExecutor.java create mode 100644 java/com/android/dialer/common/AsyncTaskExecutors.java create mode 100644 java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java create mode 100644 java/com/android/dialer/common/ConfigProvider.java create mode 100644 java/com/android/dialer/common/ConfigProviderBindings.java create mode 100644 java/com/android/dialer/common/ConfigProviderFactory.java create mode 100644 java/com/android/dialer/common/DpUtil.java create mode 100644 java/com/android/dialer/common/FallibleAsyncTask.java create mode 100644 java/com/android/dialer/common/FragmentUtils.java create mode 100644 java/com/android/dialer/common/LogUtil.java create mode 100644 java/com/android/dialer/common/MathUtil.java create mode 100644 java/com/android/dialer/common/NetworkUtil.java create mode 100644 java/com/android/dialer/common/UiUtil.java create mode 100644 java/com/android/dialer/common/res/values/strings.xml create mode 100644 java/com/android/dialer/compat/ActivityCompat.java create mode 100644 java/com/android/dialer/compat/AppCompatConstants.java create mode 100644 java/com/android/dialer/compat/CompatUtils.java create mode 100644 java/com/android/dialer/compat/PathInterpolatorCompat.java create mode 100644 java/com/android/dialer/compat/SdkVersionOverride.java create mode 100644 java/com/android/dialer/constants/Constants.java create mode 100644 java/com/android/dialer/constants/ScheduledJobIds.java create mode 100644 java/com/android/dialer/constants/aospdialer/ConstantsImpl.java create mode 100644 java/com/android/dialer/database/CallLogQueryHandler.java create mode 100644 java/com/android/dialer/database/Database.java create mode 100644 java/com/android/dialer/database/DatabaseBindings.java create mode 100644 java/com/android/dialer/database/DatabaseBindingsFactory.java create mode 100644 java/com/android/dialer/database/DatabaseBindingsStub.java create mode 100644 java/com/android/dialer/database/DialerDatabaseHelper.java create mode 100644 java/com/android/dialer/database/FilteredNumberContract.java create mode 100644 java/com/android/dialer/database/VoicemailStatusQuery.java create mode 100644 java/com/android/dialer/debug/AndroidManifest.xml create mode 100644 java/com/android/dialer/debug/bindings/impl/DebugBindings.java create mode 100644 java/com/android/dialer/debug/impl/AndroidManifest.xml create mode 100644 java/com/android/dialer/debug/impl/DebugConnection.java create mode 100644 java/com/android/dialer/debug/impl/DebugConnectionService.java create mode 100644 java/com/android/dialer/dialpadview/AndroidManifest.xml create mode 100644 java/com/android/dialer/dialpadview/DialpadKeyButton.java create mode 100644 java/com/android/dialer/dialpadview/DialpadTextView.java create mode 100644 java/com/android/dialer/dialpadview/DialpadView.java create mode 100644 java/com/android/dialer/dialpadview/DigitsEditText.java create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml create mode 100644 java/com/android/dialer/dialpadview/res/values-land/dimens.xml create mode 100644 java/com/android/dialer/dialpadview/res/values-land/styles.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/animation_constants.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/attrs.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/colors.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/dimens.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/strings.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/styles.xml create mode 100644 java/com/android/dialer/disabled_lint_checks.txt create mode 100644 java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java create mode 100644 java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallManager.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java create mode 100644 java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java create mode 100644 java/com/android/dialer/enrichedcall/Session.java create mode 100644 java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java create mode 100644 java/com/android/dialer/enrichedcall/extensions/StateExtension.java create mode 100644 java/com/android/dialer/inject/ApplicationModule.java create mode 100644 java/com/android/dialer/inject/DialerAppComponent.java create mode 100644 java/com/android/dialer/interactions/AndroidManifest.xml create mode 100644 java/com/android/dialer/interactions/ContactUpdateService.java create mode 100644 java/com/android/dialer/interactions/PhoneNumberInteraction.java create mode 100644 java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java create mode 100644 java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml create mode 100644 java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml create mode 100644 java/com/android/dialer/interactions/res/values/strings.xml create mode 100644 java/com/android/dialer/logging/Logger.java create mode 100644 java/com/android/dialer/logging/LoggingBindings.java create mode 100644 java/com/android/dialer/logging/LoggingBindingsFactory.java create mode 100644 java/com/android/dialer/logging/LoggingBindingsStub.java create mode 100644 java/com/android/dialer/logging/nano/ContactLookupResult.java create mode 100644 java/com/android/dialer/logging/nano/ContactSource.java create mode 100644 java/com/android/dialer/logging/nano/DialerImpression.java create mode 100644 java/com/android/dialer/logging/nano/InteractionEvent.java create mode 100644 java/com/android/dialer/logging/nano/ReportingLocation.java create mode 100644 java/com/android/dialer/logging/nano/ScreenEvent.java create mode 100644 java/com/android/dialer/multimedia/AutoValue_MultimediaData.java create mode 100644 java/com/android/dialer/multimedia/MultimediaData.java create mode 100644 java/com/android/dialer/p13n/inference/P13nRanking.java create mode 100644 java/com/android/dialer/p13n/inference/protocol/P13nRanker.java create mode 100644 java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLogger.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLoggerFactory.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLogging.java create mode 100644 java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java create mode 100644 java/com/android/dialer/phonenumbercache/CallLogQuery.java create mode 100644 java/com/android/dialer/phonenumbercache/ContactInfo.java create mode 100644 java/com/android/dialer/phonenumbercache/ContactInfoHelper.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCache.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneQuery.java create mode 100644 java/com/android/dialer/phonenumberutil/AndroidManifest.xml create mode 100644 java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java create mode 100644 java/com/android/dialer/phonenumberutil/res/values/strings.xml create mode 100644 java/com/android/dialer/proguard/UsedByReflection.java create mode 100644 java/com/android/dialer/protos/ProtoParsers.java create mode 100644 java/com/android/dialer/shortcuts/AndroidManifest.xml create mode 100644 java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java create mode 100644 java/com/android/dialer/shortcuts/CallContactActivity.java create mode 100644 java/com/android/dialer/shortcuts/DialerShortcut.java create mode 100644 java/com/android/dialer/shortcuts/DynamicShortcuts.java create mode 100644 java/com/android/dialer/shortcuts/IconFactory.java create mode 100644 java/com/android/dialer/shortcuts/PeriodicJobService.java create mode 100644 java/com/android/dialer/shortcuts/PinnedShortcuts.java create mode 100644 java/com/android/dialer/shortcuts/RefreshShortcutsTask.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutInfoFactory.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutRefresher.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutUsageReporter.java create mode 100644 java/com/android/dialer/shortcuts/Shortcuts.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java create mode 100644 java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/colors.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/dimens.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/strings.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/themes.xml create mode 100644 java/com/android/dialer/shortcuts/res/xml/shortcuts.xml create mode 100644 java/com/android/dialer/simulator/Simulator.java create mode 100644 java/com/android/dialer/simulator/impl/AndroidManifest.xml create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorActionProvider.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorCallLog.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnection.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnectionService.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorContacts.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorModule.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVoicemail.java create mode 100644 java/com/android/dialer/smartdial/LatinSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialMatchPosition.java create mode 100644 java/com/android/dialer/smartdial/SmartDialNameMatcher.java create mode 100644 java/com/android/dialer/smartdial/SmartDialPrefix.java create mode 100644 java/com/android/dialer/spam/Spam.java create mode 100644 java/com/android/dialer/spam/SpamBindings.java create mode 100644 java/com/android/dialer/spam/SpamBindingsFactory.java create mode 100644 java/com/android/dialer/spam/SpamBindingsStub.java create mode 100644 java/com/android/dialer/telecom/TelecomUtil.java create mode 100644 java/com/android/dialer/theme/AndroidManifest.xml create mode 100644 java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml create mode 100644 java/com/android/dialer/theme/res/animator/activated_button_elevation.xml create mode 100644 java/com/android/dialer/theme/res/animator/button_elevation.xml create mode 100644 java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml create mode 100644 java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml create mode 100644 java/com/android/dialer/theme/res/values/colors.xml create mode 100644 java/com/android/dialer/theme/res/values/dimens.xml create mode 100644 java/com/android/dialer/theme/res/values/strings.xml create mode 100644 java/com/android/dialer/theme/res/values/styles.xml create mode 100644 java/com/android/dialer/theme/res/values/themes.xml create mode 100644 java/com/android/dialer/util/AndroidManifest.xml create mode 100644 java/com/android/dialer/util/CallUtil.java create mode 100644 java/com/android/dialer/util/DialerUtils.java create mode 100644 java/com/android/dialer/util/DrawableConverter.java create mode 100644 java/com/android/dialer/util/ExpirableCache.java create mode 100644 java/com/android/dialer/util/IntentUtil.java create mode 100644 java/com/android/dialer/util/MoreStrings.java create mode 100644 java/com/android/dialer/util/OrientationUtil.java create mode 100644 java/com/android/dialer/util/PermissionsUtil.java create mode 100644 java/com/android/dialer/util/SettingsUtil.java create mode 100644 java/com/android/dialer/util/TouchPointManager.java create mode 100644 java/com/android/dialer/util/TransactionSafeActivity.java create mode 100644 java/com/android/dialer/util/ViewUtil.java create mode 100644 java/com/android/dialer/util/res/values/strings.xml create mode 100644 java/com/android/dialer/voicemailstatus/AndroidManifest.xml create mode 100644 java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java create mode 100644 java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java create mode 100644 java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java create mode 100644 java/com/android/dialer/voicemailstatus/res/values/strings.xml create mode 100644 java/com/android/dialer/widget/AndroidManifest.xml create mode 100644 java/com/android/dialer/widget/ResizingTextEditText.java create mode 100644 java/com/android/dialer/widget/ResizingTextTextView.java create mode 100644 java/com/android/dialer/widget/res/values/attrs.xml create mode 100644 java/com/android/incallui/AccelerometerListener.java create mode 100644 java/com/android/incallui/AndroidManifest.xml create mode 100644 java/com/android/incallui/AnswerScreenPresenter.java create mode 100644 java/com/android/incallui/AnswerScreenPresenterStub.java create mode 100644 java/com/android/incallui/AudioModeProvider.java create mode 100644 java/com/android/incallui/Bindings.java create mode 100644 java/com/android/incallui/CallButtonPresenter.java create mode 100644 java/com/android/incallui/CallCardPresenter.java create mode 100644 java/com/android/incallui/CallerInfo.java create mode 100644 java/com/android/incallui/CallerInfoAsyncQuery.java create mode 100644 java/com/android/incallui/CallerInfoUtils.java create mode 100644 java/com/android/incallui/ConferenceManagerFragment.java create mode 100644 java/com/android/incallui/ConferenceManagerPresenter.java create mode 100644 java/com/android/incallui/ConferenceParticipantListAdapter.java create mode 100644 java/com/android/incallui/ContactInfoCache.java create mode 100644 java/com/android/incallui/ContactsAsyncHelper.java create mode 100644 java/com/android/incallui/ContactsPreferencesFactory.java create mode 100644 java/com/android/incallui/DialpadFragment.java create mode 100644 java/com/android/incallui/DialpadPresenter.java create mode 100644 java/com/android/incallui/ExternalCallNotifier.java create mode 100644 java/com/android/incallui/InCallActivity.java create mode 100644 java/com/android/incallui/InCallActivityCommon.java create mode 100644 java/com/android/incallui/InCallCameraManager.java create mode 100644 java/com/android/incallui/InCallOrientationEventListener.java create mode 100644 java/com/android/incallui/InCallPresenter.java create mode 100644 java/com/android/incallui/InCallServiceImpl.java create mode 100644 java/com/android/incallui/InCallUIMaterialColorMapUtils.java create mode 100644 java/com/android/incallui/Log.java create mode 100644 java/com/android/incallui/ManageConferenceActivity.java create mode 100644 java/com/android/incallui/NotificationBroadcastReceiver.java create mode 100644 java/com/android/incallui/PostCharDialogFragment.java create mode 100644 java/com/android/incallui/ProximitySensor.java create mode 100644 java/com/android/incallui/StatusBarNotifier.java create mode 100644 java/com/android/incallui/ThemeColorManager.java create mode 100644 java/com/android/incallui/TransactionSafeFragmentActivity.java create mode 100644 java/com/android/incallui/VideoCallPresenter.java create mode 100644 java/com/android/incallui/VideoPauseController.java create mode 100644 java/com/android/incallui/answer/bindings/AnswerBindings.java create mode 100644 java/com/android/incallui/answer/impl/AffordanceHolderLayout.java create mode 100644 java/com/android/incallui/answer/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/AnswerFragment.java create mode 100644 java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java create mode 100644 java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java create mode 100644 java/com/android/incallui/answer/impl/PillDrawable.java create mode 100644 java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java create mode 100644 java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java create mode 100644 java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java create mode 100644 java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml create mode 100644 java/com/android/incallui/answer/impl/answermethod/res/values/values.xml create mode 100644 java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/Classifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/ClassifierData.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/FalsingManager.java create mode 100644 java/com/android/incallui/answer/impl/classifier/GestureClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/Point.java create mode 100644 java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java create mode 100644 java/com/android/incallui/answer/impl/classifier/Stroke.java create mode 100644 java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java create mode 100644 java/com/android/incallui/answer/impl/hint/AndroidManifest.xml create mode 100644 java/com/android/incallui/answer/impl/hint/AnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java create mode 100644 java/com/android/incallui/answer/impl/hint/DotAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventAnswerHint.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java create mode 100644 java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/hint/res/values/strings.xml create mode 100644 java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml create mode 100644 java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml create mode 100644 java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml create mode 100644 java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml create mode 100644 java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/answer/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java create mode 100644 java/com/android/incallui/answer/impl/utils/Interpolators.java create mode 100644 java/com/android/incallui/answer/protocol/AnswerScreen.java create mode 100644 java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java create mode 100644 java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java create mode 100644 java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java create mode 100644 java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java create mode 100644 java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java create mode 100644 java/com/android/incallui/answerproximitysensor/PseudoScreenState.java create mode 100644 java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java create mode 100644 java/com/android/incallui/async/PausableExecutor.java create mode 100644 java/com/android/incallui/async/PausableExecutorImpl.java create mode 100644 java/com/android/incallui/audioroute/AndroidManifest.xml create mode 100644 java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java create mode 100644 java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png create mode 100644 java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml create mode 100644 java/com/android/incallui/audioroute/res/values/strings.xml create mode 100644 java/com/android/incallui/audioroute/res/values/styles.xml create mode 100644 java/com/android/incallui/autoresizetext/AndroidManifest.xml create mode 100644 java/com/android/incallui/autoresizetext/AutoResizeTextView.java create mode 100644 java/com/android/incallui/autoresizetext/res/values/attrs.xml create mode 100644 java/com/android/incallui/baseui/BaseFragment.java create mode 100644 java/com/android/incallui/baseui/Presenter.java create mode 100644 java/com/android/incallui/baseui/Ui.java create mode 100644 java/com/android/incallui/bindings/ContactUtils.java create mode 100644 java/com/android/incallui/bindings/DistanceHelper.java create mode 100644 java/com/android/incallui/bindings/InCallUiBindings.java create mode 100644 java/com/android/incallui/bindings/InCallUiBindingsFactory.java create mode 100644 java/com/android/incallui/bindings/InCallUiBindingsStub.java create mode 100644 java/com/android/incallui/bindings/PhoneNumberService.java create mode 100644 java/com/android/incallui/call/CallList.java create mode 100644 java/com/android/incallui/call/DialerCall.java create mode 100644 java/com/android/incallui/call/DialerCallDelegate.java create mode 100644 java/com/android/incallui/call/DialerCallListener.java create mode 100644 java/com/android/incallui/call/ExternalCallList.java create mode 100644 java/com/android/incallui/call/InCallServiceListener.java create mode 100644 java/com/android/incallui/call/InCallUiLegacyBindings.java create mode 100644 java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java create mode 100644 java/com/android/incallui/call/InCallUiLegacyBindingsStub.java create mode 100644 java/com/android/incallui/call/InCallVideoCallCallback.java create mode 100644 java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java create mode 100644 java/com/android/incallui/call/TelecomAdapter.java create mode 100644 java/com/android/incallui/call/VideoUtils.java create mode 100644 java/com/android/incallui/commontheme/AndroidManifest.xml create mode 100644 java/com/android/incallui/commontheme/res/animator/button_state.xml create mode 100644 java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml create mode 100644 java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml create mode 100644 java/com/android/incallui/commontheme/res/color/incall_button_white.xml create mode 100644 java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png create mode 100644 java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml create mode 100644 java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml create mode 100644 java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml create mode 100644 java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml create mode 100644 java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml create mode 100644 java/com/android/incallui/commontheme/res/values/colors.xml create mode 100644 java/com/android/incallui/commontheme/res/values/dimens.xml create mode 100644 java/com/android/incallui/commontheme/res/values/strings.xml create mode 100644 java/com/android/incallui/commontheme/res/values/styles.xml create mode 100644 java/com/android/incallui/contactgrid/AndroidManifest.xml create mode 100644 java/com/android/incallui/contactgrid/BottomRow.java create mode 100644 java/com/android/incallui/contactgrid/ContactGridManager.java create mode 100644 java/com/android/incallui/contactgrid/TopRow.java create mode 100644 java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml create mode 100644 java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml create mode 100644 java/com/android/incallui/contactgrid/res/values/ids.xml create mode 100644 java/com/android/incallui/contactgrid/res/values/strings.xml create mode 100644 java/com/android/incallui/hold/AndroidManifest.xml create mode 100644 java/com/android/incallui/hold/OnHoldFragment.java create mode 100644 java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml create mode 100644 java/com/android/incallui/hold/res/values/strings.xml create mode 100644 java/com/android/incallui/incall/bindings/InCallBindings.java create mode 100644 java/com/android/incallui/incall/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java create mode 100644 java/com/android/incallui/incall/impl/ButtonChooser.java create mode 100644 java/com/android/incallui/incall/impl/ButtonChooserFactory.java create mode 100644 java/com/android/incallui/incall/impl/ButtonController.java create mode 100644 java/com/android/incallui/incall/impl/CheckableLabeledButton.java create mode 100644 java/com/android/incallui/incall/impl/InCallButtonGridFragment.java create mode 100644 java/com/android/incallui/incall/impl/InCallFragment.java create mode 100644 java/com/android/incallui/incall/impl/InCallPagerAdapter.java create mode 100644 java/com/android/incallui/incall/impl/MappedButtonConfig.java create mode 100644 java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml create mode 100644 java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png create mode 100644 java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml create mode 100644 java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml create mode 100644 java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml create mode 100644 java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml create mode 100644 java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/attrs.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/ids.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/incall/impl/res/values/styles.xml create mode 100644 java/com/android/incallui/incall/protocol/ContactPhotoType.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonIds.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonUi.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java create mode 100644 java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java create mode 100644 java/com/android/incallui/incall/protocol/InCallScreen.java create mode 100644 java/com/android/incallui/incall/protocol/InCallScreenDelegate.java create mode 100644 java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java create mode 100644 java/com/android/incallui/incall/protocol/PrimaryCallState.java create mode 100644 java/com/android/incallui/incall/protocol/PrimaryInfo.java create mode 100644 java/com/android/incallui/incall/protocol/SecondaryInfo.java create mode 100644 java/com/android/incallui/latencyreport/LatencyReport.java create mode 100644 java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java create mode 100644 java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java create mode 100644 java/com/android/incallui/maps/StaticMapBinding.java create mode 100644 java/com/android/incallui/maps/StaticMapFactory.java create mode 100644 java/com/android/incallui/res/anim/activity_open_enter.xml create mode 100644 java/com/android/incallui/res/anim/activity_open_exit.xml create mode 100644 java/com/android/incallui/res/anim/decelerate_cubic.xml create mode 100644 java/com/android/incallui/res/anim/decelerate_quint.xml create mode 100644 java/com/android/incallui/res/anim/on_going_call.xml create mode 100644 java/com/android/incallui/res/color/ota_title_color.xml create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-hdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-mdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-xhdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-xxhdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_business.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png create mode 100644 java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png create mode 100644 java/com/android/incallui/res/drawable/img_conference_automirrored.xml create mode 100644 java/com/android/incallui/res/drawable/img_no_image_automirrored.xml create mode 100644 java/com/android/incallui/res/drawable/incall_background_gradient.xml create mode 100644 java/com/android/incallui/res/drawable/spam_notification_icon.xml create mode 100644 java/com/android/incallui/res/drawable/unknown_notification_icon.xml create mode 100644 java/com/android/incallui/res/layout/activity_manage_conference.xml create mode 100644 java/com/android/incallui/res/layout/caller_in_conference.xml create mode 100644 java/com/android/incallui/res/layout/conference_manager_fragment.xml create mode 100644 java/com/android/incallui/res/layout/incall_dialpad_fragment.xml create mode 100644 java/com/android/incallui/res/layout/incall_screen.xml create mode 100644 java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml create mode 100644 java/com/android/incallui/res/values-sw360dp/dimens.xml create mode 100644 java/com/android/incallui/res/values-w500dp-land/colors.xml create mode 100644 java/com/android/incallui/res/values-w500dp-land/dimens.xml create mode 100644 java/com/android/incallui/res/values/animation_constants.xml create mode 100644 java/com/android/incallui/res/values/colors.xml create mode 100644 java/com/android/incallui/res/values/config.xml create mode 100644 java/com/android/incallui/res/values/dimens.xml create mode 100644 java/com/android/incallui/res/values/strings.xml create mode 100644 java/com/android/incallui/res/values/styles.xml create mode 100644 java/com/android/incallui/ringtone/DialerRingtoneManager.java create mode 100644 java/com/android/incallui/ringtone/InCallTonePlayer.java create mode 100644 java/com/android/incallui/ringtone/ToneGeneratorFactory.java create mode 100644 java/com/android/incallui/sessiondata/AndroidManifest.xml create mode 100644 java/com/android/incallui/sessiondata/AvatarPresenter.java create mode 100644 java/com/android/incallui/sessiondata/MultimediaFragment.java create mode 100644 java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml create mode 100644 java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml create mode 100644 java/com/android/incallui/sessiondata/res/values/dimens.xml create mode 100644 java/com/android/incallui/sessiondata/res/values/ids.xml create mode 100644 java/com/android/incallui/sessiondata/res/values/styles.xml create mode 100644 java/com/android/incallui/spam/NumberInCallHistoryTask.java create mode 100644 java/com/android/incallui/spam/SpamCallListListener.java create mode 100644 java/com/android/incallui/spam/SpamNotificationActivity.java create mode 100644 java/com/android/incallui/spam/SpamNotificationService.java create mode 100644 java/com/android/incallui/util/AccessibilityUtil.java create mode 100644 java/com/android/incallui/util/TelecomCallUtil.java create mode 100644 java/com/android/incallui/video/bindings/VideoBindings.java create mode 100644 java/com/android/incallui/video/impl/AndroidManifest.xml create mode 100644 java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java create mode 100644 java/com/android/incallui/video/impl/CheckableImageButton.java create mode 100644 java/com/android/incallui/video/impl/SpeakerButtonController.java create mode 100644 java/com/android/incallui/video/impl/SwitchOnHoldCallController.java create mode 100644 java/com/android/incallui/video/impl/VideoCallFragment.java create mode 100644 java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png create mode 100644 java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png create mode 100644 java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml create mode 100644 java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml create mode 100644 java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/frag_videocall.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/videocall_controls.xml create mode 100644 java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml create mode 100644 java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml create mode 100644 java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml create mode 100644 java/com/android/incallui/video/impl/res/values/attrs.xml create mode 100644 java/com/android/incallui/video/impl/res/values/dimens.xml create mode 100644 java/com/android/incallui/video/impl/res/values/strings.xml create mode 100644 java/com/android/incallui/video/impl/res/values/styles.xml create mode 100644 java/com/android/incallui/video/protocol/VideoCallScreen.java create mode 100644 java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java create mode 100644 java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java create mode 100644 java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java create mode 100644 java/com/android/incallui/videosurface/impl/VideoScale.java create mode 100644 java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java create mode 100644 java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java create mode 100644 java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java create mode 100644 java/com/android/incallui/wifi/AndroidManifest.xml create mode 100644 java/com/android/incallui/wifi/EnableWifiCallingPrompt.java create mode 100644 java/com/android/incallui/wifi/res/values/strings.xml create mode 100644 java/com/android/voicemailomtp/ActivationTask.java create mode 100644 java/com/android/voicemailomtp/AndroidManifest.xml create mode 100644 java/com/android/voicemailomtp/Assert.java create mode 100644 java/com/android/voicemailomtp/DefaultOmtpEventHandler.java create mode 100644 java/com/android/voicemailomtp/NeededForTesting.java create mode 100644 java/com/android/voicemailomtp/OmtpConstants.java create mode 100644 java/com/android/voicemailomtp/OmtpEvents.java create mode 100644 java/com/android/voicemailomtp/OmtpService.java create mode 100644 java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java create mode 100644 java/com/android/voicemailomtp/SubscriptionInfoHelper.java create mode 100644 java/com/android/voicemailomtp/TelephonyManagerStub.java create mode 100644 java/com/android/voicemailomtp/TelephonyVvmConfigManager.java create mode 100644 java/com/android/voicemailomtp/VisualVoicemailPreferences.java create mode 100644 java/com/android/voicemailomtp/Voicemail.java create mode 100644 java/com/android/voicemailomtp/VoicemailStatus.java create mode 100644 java/com/android/voicemailomtp/VvmLog.java create mode 100644 java/com/android/voicemailomtp/VvmPackageInstallReceiver.java create mode 100644 java/com/android/voicemailomtp/VvmPhoneStateListener.java create mode 100644 java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java create mode 100644 java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java create mode 100644 java/com/android/voicemailomtp/imap/ImapHelper.java create mode 100644 java/com/android/voicemailomtp/imap/VoicemailPayload.java create mode 100644 java/com/android/voicemailomtp/mail/Address.java create mode 100644 java/com/android/voicemailomtp/mail/AuthenticationFailedException.java create mode 100644 java/com/android/voicemailomtp/mail/Base64Body.java create mode 100644 java/com/android/voicemailomtp/mail/Body.java create mode 100644 java/com/android/voicemailomtp/mail/BodyPart.java create mode 100644 java/com/android/voicemailomtp/mail/CertificateValidationException.java create mode 100644 java/com/android/voicemailomtp/mail/FetchProfile.java create mode 100644 java/com/android/voicemailomtp/mail/Fetchable.java create mode 100644 java/com/android/voicemailomtp/mail/FixedLengthInputStream.java create mode 100644 java/com/android/voicemailomtp/mail/Flag.java create mode 100644 java/com/android/voicemailomtp/mail/MailTransport.java create mode 100644 java/com/android/voicemailomtp/mail/MeetingInfo.java create mode 100644 java/com/android/voicemailomtp/mail/Message.java create mode 100644 java/com/android/voicemailomtp/mail/MessageDateComparator.java create mode 100644 java/com/android/voicemailomtp/mail/MessagingException.java create mode 100644 java/com/android/voicemailomtp/mail/Multipart.java create mode 100644 java/com/android/voicemailomtp/mail/PackedString.java create mode 100644 java/com/android/voicemailomtp/mail/Part.java create mode 100644 java/com/android/voicemailomtp/mail/PeekableInputStream.java create mode 100644 java/com/android/voicemailomtp/mail/TempDirectory.java create mode 100644 java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeHeader.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeMessage.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeMultipart.java create mode 100644 java/com/android/voicemailomtp/mail/internet/MimeUtility.java create mode 100644 java/com/android/voicemailomtp/mail/internet/TextBody.java create mode 100644 java/com/android/voicemailomtp/mail/store/ImapConnection.java create mode 100644 java/com/android/voicemailomtp/mail/store/ImapFolder.java create mode 100644 java/com/android/voicemailomtp/mail/store/ImapStore.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapElement.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapList.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapString.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java create mode 100644 java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java create mode 100644 java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java create mode 100644 java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java create mode 100644 java/com/android/voicemailomtp/mail/utils/LogUtils.java create mode 100644 java/com/android/voicemailomtp/mail/utils/Utility.java create mode 100644 java/com/android/voicemailomtp/permissions.xml create mode 100644 java/com/android/voicemailomtp/protocol/CvvmProtocol.java create mode 100644 java/com/android/voicemailomtp/protocol/OmtpProtocol.java create mode 100644 java/com/android/voicemailomtp/protocol/ProtocolHelper.java create mode 100644 java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java create mode 100644 java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java create mode 100644 java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java create mode 100644 java/com/android/voicemailomtp/protocol/Vvm3Protocol.java create mode 100644 java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java create mode 100644 java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml create mode 100644 java/com/android/voicemailomtp/res/values/arrays.xml create mode 100644 java/com/android/voicemailomtp/res/values/attrs.xml create mode 100644 java/com/android/voicemailomtp/res/values/colors.xml create mode 100644 java/com/android/voicemailomtp/res/values/config.xml create mode 100644 java/com/android/voicemailomtp/res/values/dimens.xml create mode 100644 java/com/android/voicemailomtp/res/values/ids.xml create mode 100644 java/com/android/voicemailomtp/res/values/strings.xml create mode 100644 java/com/android/voicemailomtp/res/values/styles.xml create mode 100644 java/com/android/voicemailomtp/res/xml/voicemail_settings.xml create mode 100644 java/com/android/voicemailomtp/res/xml/vvm_config.xml create mode 100644 java/com/android/voicemailomtp/scheduling/BaseTask.java create mode 100644 java/com/android/voicemailomtp/scheduling/BlockerTask.java create mode 100644 java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java create mode 100644 java/com/android/voicemailomtp/scheduling/Policy.java create mode 100644 java/com/android/voicemailomtp/scheduling/PostponePolicy.java create mode 100644 java/com/android/voicemailomtp/scheduling/RetryPolicy.java create mode 100644 java/com/android/voicemailomtp/scheduling/Task.java create mode 100644 java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java create mode 100644 java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java create mode 100644 java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java create mode 100644 java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java create mode 100644 java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpMessageSender.java create mode 100644 java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java create mode 100644 java/com/android/voicemailomtp/sms/StatusMessage.java create mode 100644 java/com/android/voicemailomtp/sms/StatusSmsFetcher.java create mode 100644 java/com/android/voicemailomtp/sms/SyncMessage.java create mode 100644 java/com/android/voicemailomtp/sms/Vvm3MessageSender.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java create mode 100644 java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java create mode 100644 java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java create mode 100644 java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java create mode 100644 java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java create mode 100644 java/com/android/voicemailomtp/sync/SyncOneTask.java create mode 100644 java/com/android/voicemailomtp/sync/SyncTask.java create mode 100644 java/com/android/voicemailomtp/sync/UploadTask.java create mode 100644 java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java create mode 100644 java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java create mode 100644 java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java create mode 100644 java/com/android/voicemailomtp/sync/VvmNetworkRequest.java create mode 100644 java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java create mode 100644 java/com/android/voicemailomtp/utils/IndentingPrintWriter.java create mode 100644 java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java create mode 100644 java/com/android/voicemailomtp/utils/VvmDumpHandler.java create mode 100644 java/com/android/voicemailomtp/utils/XmlUtils.java (limited to 'java/com/android') 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 @@ + + + + + + + + + + + + + + + + + + + + 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 > to be collapsed. + */ + public static > void collapseList(List 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 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 { + + 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(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 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 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 mPendingRequests = + new ConcurrentHashMap(); + /** 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(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(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); + 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> 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> iterator = mPendingRequests.entrySet().iterator(); + while (iterator.hasNext()) { + final Entry 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 ? "" : 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 photoIds, Set photoIdsAsStrings, Set 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 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 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 mPhotoIds = new HashSet<>(); + private final Set mPhotoIdsAsStrings = new HashSet<>(); + private final Set mPhotoUris = new HashSet<>(); + private final List 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. + * + *

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. + * + *

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 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 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. + * + *

3 types of query + * + *

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 + * + *

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 + * + *

3. work remote query: + * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000003 + * contact_id is random. only directory_id is available + * + *

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". + *

"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. + *

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) + * + *

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. + * + *

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 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: + * + *

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. + * + *

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. + * + *

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. + * + *

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 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 loadSubjectHistory(SharedPreferences prefs) { + int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0); + List 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 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 adapter = + new ArrayAdapter( + 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 task = + new AsyncTask() { + @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 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 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. + * + *

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

bits to it. + Assert.assertEquals("", actualHtmlText); + } else { + Assert.assertEquals("

" + expectedHtmlText + "

\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. + * + *

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 directoryIds = new HashSet(); + + 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 extends Fragment + implements OnItemClickListener, + OnScrollListener, + OnFocusChangeListener, + OnTouchListener, + OnItemLongClickListener, + LoaderCallbacks { + 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 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 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 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}. + * + *

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, 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 CREATOR = + new Parcelable.Creator() { + @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. + * + *

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 mListeners = + new ArrayList(); + 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. + * + *

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. + * + *

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 mNameHighlightSequence; + private ArrayList 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(); + mNumberHighlightSequence = new ArrayList(); + } + + 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(); + mNumberHighlightSequence = new ArrayList(); + + 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 split(String content) { + final Matcher matcher = SPLIT_PATTERN.matcher(content); + final ArrayList 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. + * + *

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 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. + * + *

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 sections + */ + 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 selectionArgs = new ArrayList(); + + 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 { + + 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}. + * + *

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 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 selectionArgs = new ArrayList(); + + 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 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 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 + 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 mLoadFinishedListeners = + new ArraySet(); + + 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 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 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: + * + *

    + *
  • Mobile network + *
  • Location manager + *
  • SIM's country + *
  • User's default locale + *
+ * + * 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
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 getAccounts(boolean contactWritableOnly); + + /** Returns the list of accounts that are group writable. */ + public abstract List 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. + *

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). + *

Warning: Don't use on the UI thread because this can scan the database. + */ + public abstract Map 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 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 + EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP = + Collections.unmodifiableMap(new HashMap()); + + /** + * 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 ACCOUNT_COMPARATOR = + new Comparator() { + @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 mAccounts = new ArrayList<>(); + private List mContactWritableAccounts = new ArrayList<>(); + private List mGroupWritableAccounts = new ArrayList<>(); + private Map mAccountTypesWithDataSets = new ArrayMap<>(); + private Map 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 findAllInvitableAccountTypes( + Context context, + Collection accounts, + Map accountTypesByTypeAndDataSet) { + Map 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 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> accountTypesByType = new ArrayMap<>(); + + final List allAccounts = new ArrayList<>(); + final List contactWritableAccounts = new ArrayList<>(); + final List groupWritableAccounts = new ArrayList<>(); + final Set 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 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 accountTypesByTypeAndDataSet, + Map> accountTypesByType) { + accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType); + List 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 getAccounts(boolean contactWritableOnly) { + ensureAccountsLoaded(); + return contactWritableOnly ? mContactWritableAccounts : mAccounts; + } + + /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */ + public List 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 getAllInvitableAccountTypes() { + ensureAccountsLoaded(); + return mInvitableAccountTypes; + } + + @Override + public Map 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. + * + *

Warning: Don't use on the UI thread because this can scan the database. + */ + private Map findUsableInvitableAccountTypes( + Context context) { + Map allInvitables = getAllInvitableAccountTypes(); + if (allInvitables.isEmpty()) { + return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP; + } + + final Map 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 getAccountTypes(boolean contactWritableOnly) { + ensureAccountsLoaded(); + final List 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}. 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 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 getCachedValue() { + return mInvitableAccountTypes; + } + + public void setCachedValue(Map 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> { + + @Override + protected Map doInBackground(Void... params) { + return findUsableInvitableAccountTypes(mContext); + } + + @Override + protected void onPostExecute(Map 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. + * + *

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. + * + *

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 mRawContacts; + private ImmutableList mInvitableAccountTypes; + private String mDirectoryDisplayName; + private String mDirectoryType; + private String mDirectoryAccountType; + private String mDirectoryAccountName; + private int mDirectoryExportSupport; + private ImmutableList 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 getInvitableAccountTypes() { + return mInvitableAccountTypes; + } + + /* package */ void setInvitableAccountTypes(ImmutableList accountTypes) { + mInvitableAccountTypes = accountTypes; + } + + public ImmutableList getRawContacts() { + return mRawContacts; + } + + /* package */ void setRawContacts(ImmutableList 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 getContentValues() { + if (mRawContacts.size() != 1) { + throw new IllegalStateException("Cannot extract content values from an aggregated contact"); + } + + RawContact rawContact = mRawContacts.get(0); + ArrayList 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 getGroupMetaData() { + return mGroups; + } + + /* package */ void setGroupMetaData(ImmutableList 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 { + + 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 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().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 rawContactsBuilder = + new ImmutableList.Builder(); + 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 resultListBuilder = + new ImmutableList.Builder(); + if (!contactData.isUserProfile()) { + Map invitables = + AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes(); + if (!invitables.isEmpty()) { + final Map 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 selectionArgs = new ArrayList(); + final HashSet 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 groupListBuilder = + new ImmutableList.Builder(); + 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 rawContacts = contactData.getRawContacts(); + final int rawContactCount = rawContacts.size(); + for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) { + final RawContact rawContact = rawContacts.get(rawContactIndex); + final List 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 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. + * + *

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 CREATOR = + new Parcelable.Creator() { + + @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 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(); + } + + /** + * 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 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 getContentValues() { + final ArrayList list = new ArrayList<>(mDataItems.size()); + for (NamedDataItem dataItem : mDataItems) { + if (Data.CONTENT_URI.equals(dataItem.mUri)) { + list.add(dataItem.mContentValues); + } + } + return list; + } + + public List getDataItems() { + final ArrayList 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 CREATOR = + new Parcelable.Creator() { + + @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. + * + *

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 sWeightComparator = + new Comparator() { + @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. + * + *

TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link + * #getViewContactNotifyServicePackageName()}. + * + *

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 mKinds = new ArrayList<>(); + /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */ + private Map 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}. + * + *

(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}. + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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. + * + *

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 getExtensionPackageNames() { + return new ArrayList(); + } + + /** + * 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 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 { + + 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 CREATOR = + new Creator() { + 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 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 unstringifyList(String s) { + final ArrayList 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 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.) + * + *

This method returns a list, because we need to add 3 kinds for the name data kind. + * (structured, display and phonetic) + */ + public List 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class PhotoKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "photo"; + } + + @Override + public List 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 result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class NoteKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "note"; + } + + @Override + public List 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 result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class WebsiteKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "website"; + } + + @Override + public List 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 result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class SipAddressKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "sip_address"; + } + + @Override + public List 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 result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + private static class GroupMembershipKindBuilder extends KindBuilder { + + @Override + public String getTagName() { + return "group_membership"; + } + + @Override + public List 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 result = new ArrayList<>(); + result.add(kind); + return result; + } + } + + /** + * Event DataKind parser. + * + *

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 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 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. + * + *

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 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 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". + * + *

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 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(); + 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. + * + *

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. + * + *

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 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. + * + *

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 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 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 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. + * + *

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 { + + 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 typeList; + public List 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 @@ + + + + + + + \ 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 @@ + + + + + + + \ 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png 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 @@ + + + + + + + + + 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 @@ + + + + + + \ 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 @@ + + + \ 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + 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 @@ + + + + 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 @@ + + + + \ 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 @@ + + + + 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 @@ + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + + 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 @@ + + + + + + + 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 @@ + + + + + \ 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 @@ + + + + + + + + + \ 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 @@ + + + + + + + + + \ 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 @@ + + + + + + + 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 @@ + + + + + + + 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 @@ + + + + + + + \ 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 @@ + + + + + + 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 @@ + + + + + + + \ 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 @@ + + + + + + + + + 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 @@ + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + 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 @@ + + + + + + + + \ 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 @@ + + + + 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 @@ + + + + + + + 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 @@ + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + 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 @@ + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + 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 @@ + + + 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 @@ + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + 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 @@ + + + + + + + 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 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png 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 Binary files /dev/null and b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png 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 @@ + + + + false + + + true + + + false + + + true + + + false + + + true + \ 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 @@ + + + + false + + + false + + + false + + + false + + + false + \ 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 @@ + + + + 3 + + + 60 + 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 @@ + + + + 3 + + + 20 + 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 @@ + + + + 0dip + + @dimen/list_visible_scrollbar_padding + + 24dip + 16dip + + + 32dp + + 32dp + 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 @@ + + + + 3 + + + + 15 + 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 @@ + + + + 4 + + + 30 + 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 @@ + + + + 2 + + + 20 + 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 @@ + + + + false + + + true + + + false + + + true + + + false + \ 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 @@ + + + + false + + + true + + + false + + + true + + + false + \ 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 @@ + + + + 250 + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + #eeeeee + + #44ff0000 + + + #a0ffffff + + + #30000000 + + + #363636 + + @color/dialer_secondary_text_color + + + #2A56C6 + + + #AAAAAA + + + #D0D0D0 + + + #363636 + + + #DDDDDD + + + #7F000000 + + + #CCCCCC + + #7f000000 + + #fff + #000 + + + + #DB4437 + #E91E63 + #9C27B0 + #673AB7 + #3F51B5 + #4285F4 + #039BE5 + #0097A7 + #009688 + #0F9D58 + #689F38 + #EF6C00 + #FF5722 + #757575 + + + + + #C53929 + #C2185B + #7B1FA2 + #512DA8 + #303F9F + #3367D6 + #0277BD + #006064 + #00796B + #0B8043 + #33691E + #E65100 + #E64A19 + #424242 + + + + #607D8B + + #455A64 + + + #cccccc + + #ffffff + + @color/dialer_theme_color + + #ffffff + + #008aa1 + + #ffffff + @color/tab_ripple_color + #f50057 + #1C3AA9 + + + @color/contactscommon_actionbar_background_color + + + + #ffffff + #a6ffffff + + + #000000 + + #ffffff + + #737373 + @color/searchbox_hint_text_color + + @color/dialtacts_theme_color + + + #f9f9f9 + #FFFFFF + + + #d1041c + + + #000000 + + + #d8d8d8 + + + #00c853 + + + #ffffff + @color/searchbox_hint_text_color + 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 @@ + + + + + + 0dip + + + 32dip + + 18dp + 8dp + + + 23dip + + 16dip + + + 16dip + + + + 48sp + + + 0dip + + + 32dip + + 16dip + @dimen/list_visible_scrollbar_padding + + 8dip + + 56dp + + + 0dip + + + 12dp + + + 1dp + + + 32dip + + + 48dip + + + 16sp + 40dp + 15dp + 12dp + + + 20sp + 10dip + + + 40dp + 20dp + 1dp + 67% + + + 56dp + + 56dp + + 8dp + + 88dp + + 16dp + + 16dp + + + 2dp + + 14sp + 2dp + 16dp + 2dp + 0dp + 2dp + 12sp + 2dp + + + 4dp + + 48dp + + 56dp + + 16dp + + 14dp + + 15dp + + 20sp + + + 16dp + + 57dp + + 24sp + + + 40dp + + 2dp + + + 20dp + + 8dp + + 50dp + + 60dp + + 16sp + + 14sp + + 15dp + 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 @@ + + + + + true + + + true + + + true + + + true + + + true + + + true + + + true + + + ContactEditorUtils_default_account + + + ContactEditorUtils_anything_saved + + + default + + + default + + + + + + + + + vcf + + + contacts.vcf + + + 1 + + + 99999 + + + + + + true + + + true + + + true + + pref_build_version + pref_open_source_licenses + pref_privacy_policy + pref_terms_of_service + + + 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 @@ + + + + + + + + + + + + + + + + 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 @@ + + + + + + + 2 + 3 + + + 30 + + + 0 + + 0 + + + 250 + + + 100 + 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 @@ + + + + + + Text copied + + Copy to clipboard + + + Call + %s + + + Call home + + Call mobile + + Call work + + Call work fax + + Call home fax + + Call pager + + Call + + Call callback + + Call car + + Call company main + + Call ISDN + + Call main + + Call fax + + Call radio + + Call telex + + Call TTY/TDD + + Call work mobile + + Call work pager + + Call + %s + + + Call MMS + + %s (Call) + + + Text + %s + + + Text home + + Text mobile + + Text work + + Text work fax + + Text home fax + + Text pager + + Text + + Text callback + + Text car + + Text company main + + Text ISDN + + Text main + + Text fax + + Text radio + + Text telex + + Text TTY/TDD + + Text work mobile + + Text work pager + + Text + %s + + + Text MMS + + %s (Message) + + + Clear frequently contacted? + + + You\'ll clear the frequently contacted list in the + Contacts and Phone apps, and force email apps to learn your addressing preferences from + scratch. + + + + Clearing frequently contacted\u2026 + + + Available + + + Away + + + Busy + + + Contacts + + + Other + + + Directory + + + Work directory + + + All contacts + + + Me + + + Searching\u2026 + + + More than %d found. + + + No contacts + + + + 1 found + %d found + + + + Quick contact for %1$s + + + (No name) + + + Frequently called + + + Frequently contacted + + + View contact + + + All contacts with phone numbers + + + Work profile contacts + + + View updates + + + Device-only, unsynced + + + Name + + + Nickname + + + Name + + First name + + Last name + + Name prefix + + Middle name + + Name suffix + + + Phonetic name + + + Phonetic first name + + Phonetic middle name + + Phonetic last name + + + Phone + + + Email + + + Address + + + IM + + + Organization + + + Relationship + + + Special date + + + Text message + + + Address + + + Company + + + Title + + + Notes + + + SIP + + + Website + + + Groups + + + Email home + + Email mobile + + Email work + + Email + + Email %s + + + Email + + + Street + + PO box + + Neighborhood + + City + + State + + ZIP code + + Country + + + View home address + + View work address + + View address + + View %s address + + + Chat using AIM + + Chat using Windows Live + + Chat using Yahoo + + Chat using Skype + + Chat using QQ + + Chat using Google Talk + + Chat using ICQ + + Chat using Jabber + + + Chat + + + delete + + + Expand or collapse name fields + + + Expand or collapse phonetic + name fields + + + All contacts + + + Done + + + Cancel + + + Contacts in %s + + + Contacts in custom view + + + Single contact + + + Save imported contacts to: + + + Import from SIM card + + + Import from SIM ^1 - ^2 + + + Import from SIM %1$s + + + Import from .vcf file + + + Cancel import of %s? + + + Cancel export of %s? + + + Couldn\'t cancel vCard import/export + + + Unknown error. + + + Couldn\'t open \"%s\": %s. + + + Couldn\'t start the exporter: \"%s\". + + + There is no exportable contact. + + + You have disabled a required permission. + + + An error occurred during export: \"%s\". + + + Required filename is too long (\"%s\"). + + + I/O error + + + Not enough memory. The file may be too large. + + + Couldn\'t parse vCard for an unexpected reason. + + + The format isn\'t supported. + + + Couldn\'t collect meta information of given vCard file(s). + + + One or more files couldn\'t be imported (%s). + + + Finished exporting %s. + + + Finished exporting contacts. + + + Finished exporting contacts, click the notification to share contacts. + + + Tap to share contacts. + + + Exporting %s canceled. + + + Exporting contact data + + + Contact data is being exported. + + + Couldn\'t get database information. + + + 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. + + + The vCard composer didn\'t start properly. + + + Couldn\'t export + + + The contact data wasn\'t exported.\nReason: \"%s\" + + + Importing %s + + + Couldn\'t read vCard data + + + Reading vCard data canceled + + + Finished importing vCard %s + + + Importing %s canceled + + + %s will be imported shortly. + + The file will be imported shortly. + + vCard import request was rejected. Try again later. + + %s will be exported shortly. + + + The file will be exported shortly. + + + Contacts will be exported shortly. + + + vCard export request was rejected. Try again later. + + contact + + + Caching vCard(s) to local temporary storage. The actual import will start soon. + + + Couldn\'t import vCard. + + + Contact received over NFC + + + Export contacts? + + + Caching + + + Importing %s/%s: %s + + + Export to .vcf file + + + + + Sort by + + + First name + + + Last name + + + Name format + + + First name first + + + Last name first + + + Default account for new contacts + + + Sync contact metadata [DOGFOOD] + + + Sync contact metadata + + + About Contacts + + + Settings + + + Share visible contacts + + + Failed to share visible contacts. + + + Share favorite contacts + + + Share all contacts + + + Failed to share contacts. + + + Import/export contacts + + + Import contacts + + + This contact can\'t be shared. + + + There are no contacts to share. + + + Search + + + Find contacts + + + Favorites + + + No contacts. + + + No visible contacts. + + + No favorites + + + No contacts in %s + + + Clear frequents + + + Select SIM card + + + Manage accounts + + + Import/export + + + sans-serif + + + via %1$s + + + %1$s via %2$s + + + sans-serif-medium + + + stop searching + + + Clear search + + + sans-serif + + + Contact display options + + + Account + + + Always use this for calls + + + Call with + + + Call with a note + + + Type a note to send with call ... + + + SEND & CALL + + + %1$s / %2$s + + + %1$s%2$s + + + %1$s tab. + + + + + %1$s tab. %2$d unread item. + + + %1$s tab. %2$d unread items. + + + + + Build version + + + Open source licenses + + + License details for open source software + + + Privacy policy + + + Terms of service + + + Open source licenses + + + Failed to open the url. + + + Place video call + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 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. + *

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. + * + *

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. + * + *

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 CREATOR = + new Creator() { + @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. + * + *

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 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). + * + *

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 displayNameToStructuredName( + Context context, String displayName) { + Map structuredName = new TreeMap(); + 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). + * + *

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 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: + * + *

1) Only searches token prefixes. A token is defined as any combination of letters or + * numbers. + * + *

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 mTimes = new ArrayList<>(); + private final ArrayList 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 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. + * + *

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 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 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 accountHandles, + SelectPhoneAccountListener listener, + @Nullable String callId) { + ArrayList 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 { + + private int mResId; + + public SelectAccountListAdapter( + Context context, int resource, List 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; + } + } +} diff --git a/java/com/android/dialer/animation/AnimUtils.java b/java/com/android/dialer/animation/AnimUtils.java new file mode 100644 index 000000000..9c9396e56 --- /dev/null +++ b/java/com/android/dialer/animation/AnimUtils.java @@ -0,0 +1,247 @@ +/* + * 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.dialer.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import com.android.dialer.compat.PathInterpolatorCompat; + +public class AnimUtils { + + public static final int DEFAULT_DURATION = -1; + public static final int NO_DELAY = 0; + + public static final Interpolator EASE_IN = PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f); + public static final Interpolator EASE_OUT = PathInterpolatorCompat.create(0.4f, 0.0f, 1.0f, 1.0f); + public static final Interpolator EASE_OUT_EASE_IN = + PathInterpolatorCompat.create(0.4f, 0, 0.2f, 1); + + public static void crossFadeViews(View fadeIn, View fadeOut, int duration) { + fadeIn(fadeIn, duration); + fadeOut(fadeOut, duration); + } + + public static void fadeOut(View fadeOut, int duration) { + fadeOut(fadeOut, duration, null); + } + + public static void fadeOut(final View fadeOut, int durationMs, final AnimationCallback callback) { + fadeOut.setAlpha(1); + final ViewPropertyAnimator animator = fadeOut.animate(); + animator.cancel(); + animator + .alpha(0) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fadeOut.setVisibility(View.GONE); + if (callback != null) { + callback.onAnimationEnd(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeOut.setVisibility(View.GONE); + fadeOut.setAlpha(0); + if (callback != null) { + callback.onAnimationCancel(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + public static void fadeIn(View fadeIn, int durationMs) { + fadeIn(fadeIn, durationMs, NO_DELAY, null); + } + + public static void fadeIn( + final View fadeIn, int durationMs, int delay, final AnimationCallback callback) { + fadeIn.setAlpha(0); + final ViewPropertyAnimator animator = fadeIn.animate(); + animator.cancel(); + + animator.setStartDelay(delay); + animator + .alpha(1) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + fadeIn.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeIn.setAlpha(1); + if (callback != null) { + callback.onAnimationCancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (callback != null) { + callback.onAnimationEnd(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + /** + * Scales in the view from scale of 0 to actual dimensions. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + * @param startDelayMs The delay to applying the scaling in milliseconds. + */ + public static void scaleIn(final View view, int durationMs, int startDelayMs) { + AnimatorListenerAdapter listener = + (new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setScaleX(1); + view.setScaleY(1); + } + }); + scaleInternal( + view, + 0 /* startScaleValue */, + 1 /* endScaleValue */, + durationMs, + startDelayMs, + listener, + EASE_IN); + } + + /** + * Scales out the view from actual dimensions to 0. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + */ + public static void scaleOut(final View view, int durationMs) { + AnimatorListenerAdapter listener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setVisibility(View.GONE); + view.setScaleX(0); + view.setScaleY(0); + } + }; + + scaleInternal( + view, + 1 /* startScaleValue */, + 0 /* endScaleValue */, + durationMs, + NO_DELAY, + listener, + EASE_OUT); + } + + private static void scaleInternal( + final View view, + int startScaleValue, + int endScaleValue, + int durationMs, + int startDelay, + AnimatorListenerAdapter listener, + Interpolator interpolator) { + view.setScaleX(startScaleValue); + view.setScaleY(startScaleValue); + + final ViewPropertyAnimator animator = view.animate(); + animator.cancel(); + + animator + .setInterpolator(interpolator) + .scaleX(endScaleValue) + .scaleY(endScaleValue) + .setListener(listener) + .withLayer(); + + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.setStartDelay(startDelay); + + animator.start(); + } + + /** + * Animates a view to the new specified dimensions. + * + * @param view The view to change the dimensions of. + * @param newWidth The new width of the view. + * @param newHeight The new height of the view. + */ + public static void changeDimensions(final View view, final int newWidth, final int newHeight) { + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + + final int oldWidth = view.getWidth(); + final int oldHeight = view.getHeight(); + final int deltaWidth = newWidth - oldWidth; + final int deltaHeight = newHeight - oldHeight; + + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + Float value = (Float) animator.getAnimatedValue(); + + view.getLayoutParams().width = (int) (value * deltaWidth + oldWidth); + view.getLayoutParams().height = (int) (value * deltaHeight + oldHeight); + view.requestLayout(); + } + }); + animator.start(); + } + + public static class AnimationCallback { + + public void onAnimationEnd() {} + + public void onAnimationCancel() {} + } +} diff --git a/java/com/android/dialer/animation/AnimationListenerAdapter.java b/java/com/android/dialer/animation/AnimationListenerAdapter.java new file mode 100644 index 000000000..3f847f2b6 --- /dev/null +++ b/java/com/android/dialer/animation/AnimationListenerAdapter.java @@ -0,0 +1,39 @@ +/* + * 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.dialer.animation; + +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; + +/** + * Provides empty implementations of the methods in {@link AnimationListener} for convenience + * reasons. + */ +public class AnimationListenerAdapter implements AnimationListener { + + /** {@inheritDoc} */ + @Override + public void onAnimationStart(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationEnd(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationRepeat(Animation animation) {} +} diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml new file mode 100644 index 000000000..80f294acc --- /dev/null +++ b/java/com/android/dialer/app/AndroidManifest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/Bindings.java b/java/com/android/dialer/app/Bindings.java new file mode 100644 index 000000000..2beb40184 --- /dev/null +++ b/java/com/android/dialer/app/Bindings.java @@ -0,0 +1,77 @@ +/* + * 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.dialer.app; + +import android.content.Context; +import com.android.dialer.app.bindings.DialerBindings; +import com.android.dialer.app.bindings.DialerBindingsFactory; +import com.android.dialer.app.bindings.DialerBindingsStub; +import com.android.dialer.app.legacybindings.DialerLegacyBindings; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsFactory; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsStub; +import java.util.Objects; + +/** Accessor for the in call UI bindings. */ +public class Bindings { + + private static DialerBindings instance; + private static DialerLegacyBindings legacyInstance; + + private Bindings() {} + + public static DialerBindings get(Context context) { + Objects.requireNonNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerBindingsFactory) { + instance = ((DialerBindingsFactory) application).newDialerBindings(); + } + + if (instance == null) { + instance = new DialerBindingsStub(); + } + return instance; + } + + public static DialerLegacyBindings getLegacy(Context context) { + Objects.requireNonNull(context); + if (legacyInstance != null) { + return legacyInstance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerLegacyBindingsFactory) { + legacyInstance = ((DialerLegacyBindingsFactory) application).newDialerLegacyBindings(); + } + + if (legacyInstance == null) { + legacyInstance = new DialerLegacyBindingsStub(); + } + return legacyInstance; + } + + public static void setForTesting(DialerBindings testInstance) { + instance = testInstance; + } + + public static void setLegacyBindingForTesting(DialerLegacyBindings testLegacyInstance) { + legacyInstance = testLegacyInstance; + } +} diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java new file mode 100644 index 000000000..cda2b2e2c --- /dev/null +++ b/java/com/android/dialer/app/CallDetailActivity.java @@ -0,0 +1,480 @@ +/* + * 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.dialer.app; + +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.app.AppCompatActivity; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.calllog.CallDetailHistoryAdapter; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener; +import com.android.dialer.app.calllog.CallTypeHelper; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.spam.Spam; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.TouchPointManager; + +/** + * Displays the details of a specific call log entry. + * + *

This activity can be either started with the URI of a single call log entry, or with the + * {@link #EXTRA_CALL_LOG_IDS} extra to specify a group of call log entries. + */ +@UsedByReflection(value = "AndroidManifest-app.xml") +public class CallDetailActivity extends AppCompatActivity + implements MenuItem.OnMenuItemClickListener, View.OnClickListener { + + /** A long array extra containing ids of call log entries to display. */ + public static final String EXTRA_CALL_LOG_IDS = "EXTRA_CALL_LOG_IDS"; + /** If we are started with a voicemail, we'll find the uri to play with this extra. */ + public static final String EXTRA_VOICEMAIL_URI = "EXTRA_VOICEMAIL_URI"; + /** If the activity was triggered from a notification. */ + public static final String EXTRA_FROM_NOTIFICATION = "EXTRA_FROM_NOTIFICATION"; + + public static final String BLOCKED_OR_SPAM_QUERY_IDENTIFIER = "blockedOrSpamIdentifier"; + + private final AsyncTaskExecutor executor = AsyncTaskExecutors.createAsyncTaskExecutor(); + protected String mNumber; + private Context mContext; + private ContactInfoHelper mContactInfoHelper; + private ContactsPreferences mContactsPreferences; + private CallTypeHelper mCallTypeHelper; + private ContactPhotoManager mContactPhotoManager; + private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private LayoutInflater mInflater; + private Resources mResources; + private PhoneCallDetails mDetails; + private Uri mVoicemailUri; + private String mPostDialDigits = ""; + private ListView mHistoryList; + private QuickContactBadge mQuickContactBadge; + private TextView mCallerName; + private TextView mCallerNumber; + private TextView mAccountLabel; + private View mCallButton; + private View mEditBeforeCallActionItem; + private View mReportActionItem; + private View mCopyNumberActionItem; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + private CallLogAsyncTaskListener mCallLogAsyncTaskListener = + new CallLogAsyncTaskListener() { + @Override + public void onDeleteCall() { + finish(); + } + + @Override + public void onDeleteVoicemail() { + finish(); + } + + @Override + public void onGetCallDetails(final PhoneCallDetails[] details) { + if (details == null) { + // Somewhere went wrong: we're going to bail out and show error to users. + Toast.makeText(mContext, R.string.toast_call_detail_error, Toast.LENGTH_SHORT).show(); + finish(); + return; + } + + // All calls are from the same number and same contact, so pick the first detail. + mDetails = details[0]; + mNumber = TextUtils.isEmpty(mDetails.number) ? null : mDetails.number.toString(); + + if (mNumber == null) { + updateDataAndRender(details); + return; + } + + executor.submit( + BLOCKED_OR_SPAM_QUERY_IDENTIFIER, + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + mDetails.isBlocked = + mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly( + mNumber, mDetails.countryIso) + != null; + if (Spam.get(mContext).isSpamEnabled()) { + mDetails.isSpam = + hasIncomingCalls(details) + && Spam.get(mContext) + .checkSpamStatusSynchronous(mNumber, mDetails.countryIso); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + updateDataAndRender(details); + } + }); + } + + private void updateDataAndRender(PhoneCallDetails[] details) { + mPostDialDigits = + TextUtils.isEmpty(mDetails.postDialDigits) ? "" : mDetails.postDialDigits; + + final CharSequence callLocationOrType = getNumberTypeOrLocation(mDetails); + + final CharSequence displayNumber; + if (!TextUtils.isEmpty(mDetails.postDialDigits)) { + displayNumber = mDetails.number + mDetails.postDialDigits; + } else { + displayNumber = mDetails.displayNumber; + } + + final String displayNumberStr = + mBidiFormatter.unicodeWrap(displayNumber.toString(), TextDirectionHeuristics.LTR); + + mDetails.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); + + if (!TextUtils.isEmpty(mDetails.getPreferredName())) { + mCallerName.setText(mDetails.getPreferredName()); + mCallerNumber.setText(callLocationOrType + " " + displayNumberStr); + } else { + mCallerName.setText(displayNumberStr); + if (!TextUtils.isEmpty(callLocationOrType)) { + mCallerNumber.setText(callLocationOrType); + mCallerNumber.setVisibility(View.VISIBLE); + } else { + mCallerNumber.setVisibility(View.GONE); + } + } + + CharSequence accountLabel = + PhoneAccountUtils.getAccountLabel(mContext, mDetails.accountHandle); + CharSequence accountContentDescription = + PhoneCallDetails.createAccountLabelDescription( + mResources, mDetails.viaNumber, accountLabel); + if (!TextUtils.isEmpty(mDetails.viaNumber)) { + if (!TextUtils.isEmpty(accountLabel)) { + accountLabel = + mResources.getString( + R.string.call_log_via_number_phone_account, accountLabel, mDetails.viaNumber); + } else { + accountLabel = mResources.getString(R.string.call_log_via_number, mDetails.viaNumber); + } + } + if (!TextUtils.isEmpty(accountLabel)) { + mAccountLabel.setText(accountLabel); + mAccountLabel.setContentDescription(accountContentDescription); + mAccountLabel.setVisibility(View.VISIBLE); + } else { + mAccountLabel.setVisibility(View.GONE); + } + + final boolean canPlaceCallsTo = + PhoneNumberHelper.canPlaceCallsTo(mNumber, mDetails.numberPresentation); + mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); + mCopyNumberActionItem.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE); + + final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber); + final boolean isVoicemailNumber = + PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); + final boolean showEditNumberBeforeCallAction = + canPlaceCallsTo && !isSipNumber && !isVoicemailNumber; + mEditBeforeCallActionItem.setVisibility( + showEditNumberBeforeCallAction ? View.VISIBLE : View.GONE); + + final boolean showReportAction = + mContactInfoHelper.canReportAsInvalid(mDetails.sourceType, mDetails.objectId); + mReportActionItem.setVisibility(showReportAction ? View.VISIBLE : View.GONE); + + invalidateOptionsMenu(); + + mHistoryList.setAdapter( + new CallDetailHistoryAdapter(mContext, mInflater, mCallTypeHelper, details)); + + updateContactPhoto(mDetails.isSpam); + + findViewById(R.id.call_detail).setVisibility(View.VISIBLE); + } + + /** + * Determines the location geocode text for a call, or the phone number type (if available). + * + * @param details The call details. + * @return The phone number type or location. + */ + private CharSequence getNumberTypeOrLocation(PhoneCallDetails details) { + if (details.isSpam) { + return mResources.getString(R.string.spam_number_call_log_label); + } else if (details.isBlocked) { + return mResources.getString(R.string.blocked_number_call_log_label); + } else if (!TextUtils.isEmpty(details.namePrimary)) { + return Phone.getTypeLabel(mResources, details.numberType, details.numberLabel); + } else { + return details.geocode; + } + } + }; + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + mContext = this; + mResources = getResources(); + mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this)); + mContactsPreferences = new ContactsPreferences(mContext); + mCallTypeHelper = new CallTypeHelper(getResources()); + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mContext); + + mVoicemailUri = getIntent().getParcelableExtra(EXTRA_VOICEMAIL_URI); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + setContentView(R.layout.call_detail); + mInflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); + + mHistoryList = (ListView) findViewById(R.id.history); + mHistoryList.addHeaderView(mInflater.inflate(R.layout.call_detail_header, null)); + mHistoryList.addFooterView(mInflater.inflate(R.layout.call_detail_footer, null), null, false); + + mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo); + mQuickContactBadge.setOverlay(null); + if (CompatUtils.hasPrioritizedMimeType()) { + mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + mCallerName = (TextView) findViewById(R.id.caller_name); + mCallerNumber = (TextView) findViewById(R.id.caller_number); + mAccountLabel = (TextView) findViewById(R.id.phone_account_label); + mContactPhotoManager = ContactPhotoManager.getInstance(this); + + mCallButton = findViewById(R.id.call_back_button); + mCallButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + if (TextUtils.isEmpty(mNumber)) { + return; + } + DialerUtils.startActivityWithErrorToast( + CallDetailActivity.this, + new CallIntentBuilder(getDialableNumber(), CallInitiationType.Type.CALL_DETAILS) + .build()); + } + }); + + mEditBeforeCallActionItem = findViewById(R.id.call_detail_action_edit_before_call); + mEditBeforeCallActionItem.setOnClickListener(this); + mReportActionItem = findViewById(R.id.call_detail_action_report); + mReportActionItem.setOnClickListener(this); + + mCopyNumberActionItem = findViewById(R.id.call_detail_action_copy); + mCopyNumberActionItem.setOnClickListener(this); + + if (getIntent().getBooleanExtra(EXTRA_FROM_NOTIFICATION, false)) { + closeSystemDialogs(); + } + + Logger.get(this).logScreenView(ScreenEvent.Type.CALL_DETAILS, this); + } + + @Override + public void onResume() { + super.onResume(); + mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); + getCallDetails(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return super.dispatchTouchEvent(ev); + } + + public void getCallDetails() { + CallLogAsyncTaskUtil.getCallDetails(this, mCallLogAsyncTaskListener, getCallLogEntryUris()); + } + + /** + * Returns the list of URIs to show. + * + *

There are two ways the URIs can be provided to the activity: as the data on the intent, or + * as a list of ids in the call log added as an extra on the URI. + * + *

If both are available, the data on the intent takes precedence. + */ + private Uri[] getCallLogEntryUris() { + final Uri uri = getIntent().getData(); + if (uri != null) { + // If there is a data on the intent, it takes precedence over the extra. + return new Uri[] {uri}; + } + final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS); + final int numIds = ids == null ? 0 : ids.length; + final Uri[] uris = new Uri[numIds]; + for (int index = 0; index < numIds; ++index) { + uris[index] = + ContentUris.withAppendedId( + TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]); + } + return uris; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuItem deleteMenuItem = + menu.add( + Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete); + deleteMenuItem.setIcon(R.drawable.ic_delete_24dp); + deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + deleteMenuItem.setOnMenuItemClickListener(this); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.call_detail_delete_menu_item) { + Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM); + if (hasVoicemail()) { + CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener); + } else { + final StringBuilder callIds = new StringBuilder(); + for (Uri callUri : getCallLogEntryUris()) { + if (callIds.length() != 0) { + callIds.append(","); + } + callIds.append(ContentUris.parseId(callUri)); + } + CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener); + } + } + return true; + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.call_detail_action_copy) { + ClipboardUtils.copyText(mContext, null, mNumber, true); + } else if (resId == R.id.call_detail_action_edit_before_call) { + Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber())); + DialerUtils.startActivityWithErrorToast(mContext, dialIntent); + } else { + Assert.fail("Unexpected onClick event from " + view); + } + } + + // Loads and displays the contact photo. + private void updateContactPhoto(boolean isSpam) { + if (mDetails == null) { + return; + } + + mQuickContactBadge.assignContactUri(mDetails.contactUri); + final String displayName = + TextUtils.isEmpty(mDetails.namePrimary) + ? mDetails.displayNumber + : mDetails.namePrimary.toString(); + mQuickContactBadge.setContentDescription( + mResources.getString(R.string.description_contact_details, displayName)); + + final boolean isVoicemailNumber = + PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); + if (isSpam) { + mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); + return; + } + + final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType); + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemailNumber) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + final String lookupKey = + mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri); + + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + + mContactPhotoManager.loadDirectoryPhoto( + mQuickContactBadge, + mDetails.photoUri, + false /* darkTheme */, + true /* isCircular */, + request); + } + + private void closeSystemDialogs() { + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + + private String getDialableNumber() { + return mNumber + mPostDialDigits; + } + + public boolean hasVoicemail() { + return mVoicemailUri != null; + } + + private static boolean hasIncomingCalls(PhoneCallDetails[] details) { + for (int i = 0; i < details.length; i++) { + if (details[i].hasIncomingCalls()) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java new file mode 100644 index 000000000..3b979212b --- /dev/null +++ b/java/com/android/dialer/app/DialerApplication.java @@ -0,0 +1,77 @@ +/* + * 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.dialer.app; + +import android.app.Application; +import android.os.Trace; +import android.preference.PreferenceManager; +import com.android.dialer.blocking.BlockedNumbersAutoMigrator; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.inject.ApplicationModule; +import com.android.dialer.inject.DaggerDialerAppComponent; +import com.android.dialer.inject.DialerAppComponent; + +public class DialerApplication extends Application implements EnrichedCallManager.Factory { + + private static final String TAG = "DialerApplication"; + + private volatile DialerAppComponent component; + + @Override + public void onCreate() { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(); + new BlockedNumbersAutoMigrator( + this, + PreferenceManager.getDefaultSharedPreferences(this), + new FilteredNumberAsyncQueryHandler(this)) + .autoMigrate(); + Trace.endSection(); + } + + @Override + public EnrichedCallManager getEnrichedCallManager() { + return component().enrichedCallManager(); + } + + protected DialerAppComponent buildApplicationComponent() { + return DaggerDialerAppComponent.builder() + .applicationModule(new ApplicationModule(this)) + .build(); + } + + /** + * Returns the application component. + * + *

A single Component is created per application instance. Note that it won't be instantiated + * until it's first requested, but guarantees that only one will ever be created. + */ + private final DialerAppComponent component() { + // Double-check idiom for lazy initialization + DialerAppComponent result = component; + if (result == null) { + synchronized (this) { + result = component; + if (result == null) { + component = result = buildApplicationComponent(); + } + } + } + return result; + } +} diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java new file mode 100644 index 000000000..4c57cda70 --- /dev/null +++ b/java/com/android/dialer/app/DialtactsActivity.java @@ -0,0 +1,1484 @@ +/* + * 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.dialer.app; + +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Trace; +import android.provider.CallLog.Calls; +import android.speech.RecognizerIntent; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.Snackbar; +import android.support.v4.app.ActivityCompat; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.telecom.PhoneAccount; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.DragEvent; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnDragListener; +import android.view.ViewTreeObserver; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.AbsListView.OnScrollListener; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.dialog.ClearFrequentsDialog; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.contacts.common.list.PhoneNumberPickerFragment.CursorReranker; +import com.android.contacts.common.list.PhoneNumberPickerFragment.OnLoadFinishedListener; +import com.android.contacts.common.widget.FloatingActionButtonController; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.animation.AnimationListenerAdapter; +import com.android.dialer.app.calllog.CallLogFragment; +import com.android.dialer.app.calllog.CallLogNotificationsService; +import com.android.dialer.app.calllog.ClearCallLogDialog; +import com.android.dialer.app.dialpad.DialpadFragment; +import com.android.dialer.app.list.DragDropController; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.list.OnDragDropListener; +import com.android.dialer.app.list.OnListFragmentScrolledListener; +import com.android.dialer.app.list.PhoneFavoriteSquareTileView; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.list.SearchFragment; +import com.android.dialer.app.list.SmartDialSearchFragment; +import com.android.dialer.app.list.SpeedDialFragment; +import com.android.dialer.app.settings.DialerSettingsActivity; +import com.android.dialer.app.widget.ActionBarController; +import com.android.dialer.app.widget.SearchEditTextLayout; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.interactions.PhoneNumberInteraction; +import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.p13n.inference.P13nRanking; +import com.android.dialer.p13n.inference.protocol.P13nRanker; +import com.android.dialer.p13n.inference.protocol.P13nRanker.P13nRefreshCompleteListener; +import com.android.dialer.p13n.logging.P13nLogger; +import com.android.dialer.p13n.logging.P13nLogging; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.util.TouchPointManager; +import com.android.dialer.util.TransactionSafeActivity; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +/** The dialer tab's title is 'phone', a more common name (see strings.xml). */ +@UsedByReflection(value = "AndroidManifest-app.xml") +public class DialtactsActivity extends TransactionSafeActivity + implements View.OnClickListener, + DialpadFragment.OnDialpadQueryChangedListener, + OnListFragmentScrolledListener, + CallLogFragment.HostInterface, + DialpadFragment.HostInterface, + ListsFragment.HostInterface, + SpeedDialFragment.HostInterface, + SearchFragment.HostInterface, + OnDragDropListener, + OnPhoneNumberPickerActionListener, + PopupMenu.OnMenuItemClickListener, + ViewPager.OnPageChangeListener, + ActionBarController.ActivityUi, + PhoneNumberInteraction.InteractionErrorListener, + PhoneNumberInteraction.DisambigDialogDismissedListener, + ActivityCompat.OnRequestPermissionsResultCallback { + + public static final boolean DEBUG = false; + @VisibleForTesting public static final String TAG_DIALPAD_FRAGMENT = "dialpad"; + private static final String ACTION_SHOW_TAB = "ACTION_SHOW_TAB"; + @VisibleForTesting public static final String EXTRA_SHOW_TAB = "EXTRA_SHOW_TAB"; + public static final String EXTRA_CLEAR_NEW_VOICEMAILS = "EXTRA_CLEAR_NEW_VOICEMAILS"; + private static final String TAG = "DialtactsActivity"; + private static final String KEY_IN_REGULAR_SEARCH_UI = "in_regular_search_ui"; + private static final String KEY_IN_DIALPAD_SEARCH_UI = "in_dialpad_search_ui"; + private static final String KEY_SEARCH_QUERY = "search_query"; + private static final String KEY_FIRST_LAUNCH = "first_launch"; + private static final String KEY_WAS_CONFIGURATION_CHANGE = "was_configuration_change"; + private static final String KEY_IS_DIALPAD_SHOWN = "is_dialpad_shown"; + private static final String TAG_REGULAR_SEARCH_FRAGMENT = "search"; + private static final String TAG_SMARTDIAL_SEARCH_FRAGMENT = "smartdial"; + private static final String TAG_FAVORITES_FRAGMENT = "favorites"; + /** Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}. */ + private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER"; + + private static final int ACTIVITY_REQUEST_CODE_VOICE_SEARCH = 1; + public static final int ACTIVITY_REQUEST_CODE_CALL_COMPOSE = 2; + + private static final int FAB_SCALE_IN_DELAY_MS = 300; + /** Fragment containing the dialpad that slides into view */ + protected DialpadFragment mDialpadFragment; + + private CoordinatorLayout mParentLayout; + /** Fragment for searching phone numbers using the alphanumeric keyboard. */ + private RegularSearchFragment mRegularSearchFragment; + + /** Fragment for searching phone numbers using the dialpad. */ + private SmartDialSearchFragment mSmartDialSearchFragment; + + /** Animation that slides in. */ + private Animation mSlideIn; + + /** Animation that slides out. */ + private Animation mSlideOut; + /** Fragment containing the speed dial list, call history list, and all contacts list. */ + private ListsFragment mListsFragment; + /** + * Tracks whether onSaveInstanceState has been called. If true, no fragment transactions can be + * commited. + */ + private boolean mStateSaved; + + private boolean mIsRestarting; + private boolean mInDialpadSearch; + private boolean mInRegularSearch; + private boolean mClearSearchOnPause; + private boolean mIsDialpadShown; + private boolean mShowDialpadOnResume; + /** Whether or not the device is in landscape orientation. */ + private boolean mIsLandscape; + /** True if the dialpad is only temporarily showing due to being in call */ + private boolean mInCallDialpadUp; + /** True when this activity has been launched for the first time. */ + private boolean mFirstLaunch; + /** + * Search query to be applied to the SearchView in the ActionBar once onCreateOptionsMenu has been + * called. + */ + private String mPendingSearchViewQuery; + + private PopupMenu mOverflowMenu; + private EditText mSearchView; + private View mVoiceSearchButton; + private String mSearchQuery; + private String mDialpadQuery; + private DialerDatabaseHelper mDialerDatabaseHelper; + private DragDropController mDragDropController; + private ActionBarController mActionBarController; + private FloatingActionButtonController mFloatingActionButtonController; + private boolean mWasConfigurationChange; + + private P13nLogger mP13nLogger; + private P13nRanker mP13nRanker; + + AnimationListenerAdapter mSlideInListener = + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + maybeEnterSearchUi(); + } + }; + /** Listener for after slide out animation completes on dialer fragment. */ + AnimationListenerAdapter mSlideOutListener = + new AnimationListenerAdapter() { + @Override + public void onAnimationEnd(Animation animation) { + commitDialpadFragmentHide(); + } + }; + /** Listener used to send search queries to the phone search fragment. */ + private final TextWatcher mPhoneSearchQueryTextListener = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + final String newText = s.toString(); + if (newText.equals(mSearchQuery)) { + // If the query hasn't changed (perhaps due to activity being destroyed + // and restored, or user launching the same DIAL intent twice), then there is + // no need to do anything here. + return; + } + if (DEBUG) { + LogUtil.v("DialtactsActivity.onTextChanged", "called with new query: " + newText); + LogUtil.v("DialtactsActivity.onTextChanged", "previous query: " + mSearchQuery); + } + mSearchQuery = newText; + + // Show search fragment only when the query string is changed to non-empty text. + if (!TextUtils.isEmpty(newText)) { + // Call enterSearchUi only if we are switching search modes, or showing a search + // fragment for the first time. + final boolean sameSearchMode = + (mIsDialpadShown && mInDialpadSearch) || (!mIsDialpadShown && mInRegularSearch); + if (!sameSearchMode) { + enterSearchUi(mIsDialpadShown, mSearchQuery, true /* animate */); + } + } + + if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { + mSmartDialSearchFragment.setQueryString(mSearchQuery); + } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { + mRegularSearchFragment.setQueryString(mSearchQuery); + } + } + + @Override + public void afterTextChanged(Editable s) {} + }; + /** Open the search UI when the user clicks on the search box. */ + private final View.OnClickListener mSearchViewOnClickListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + if (!isInSearchUi()) { + mActionBarController.onSearchBoxTapped(); + enterSearchUi( + false /* smartDialSearch */, mSearchView.getText().toString(), true /* animate */); + } + } + }; + + private int mActionBarHeight; + private int mPreviouslySelectedTabIndex; + /** Handles the user closing the soft keyboard. */ + private final View.OnKeyListener mSearchEditTextLayoutListener = + new View.OnKeyListener() { + @Override + public boolean onKey(View v, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) { + if (TextUtils.isEmpty(mSearchView.getText().toString())) { + // If the search term is empty, close the search UI. + maybeExitSearchUi(); + } else { + // If the search term is not empty, show the dialpad fab. + showFabInSearchUi(); + } + } + return false; + } + }; + /** + * The text returned from a voice search query. Set in {@link #onActivityResult} and used in + * {@link #onResume()} to populate the search box. + */ + private String mVoiceSearchQuery; + + /** + * @param tab the TAB_INDEX_* constant in {@link ListsFragment} + * @return A intent that will open the DialtactsActivity into the specified tab. The intent for + * each tab will be unique. + */ + public static Intent getShowTabIntent(Context context, int tab) { + Intent intent = new Intent(context, DialtactsActivity.class); + intent.setAction(ACTION_SHOW_TAB); + intent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, tab); + intent.setData( + new Uri.Builder() + .scheme("intent") + .authority(context.getPackageName()) + .appendPath(TAG) + .appendQueryParameter(DialtactsActivity.EXTRA_SHOW_TAB, String.valueOf(tab)) + .build()); + + return intent; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY()); + } + return super.dispatchTouchEvent(ev); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedInstanceState); + + mFirstLaunch = true; + + final Resources resources = getResources(); + mActionBarHeight = resources.getDimensionPixelSize(R.dimen.action_bar_height_large); + + Trace.beginSection(TAG + " setContentView"); + setContentView(R.layout.dialtacts_activity); + Trace.endSection(); + getWindow().setBackgroundDrawable(null); + + Trace.beginSection(TAG + " setup Views"); + final ActionBar actionBar = getActionBarSafely(); + actionBar.setCustomView(R.layout.search_edittext); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setBackgroundDrawable(null); + + SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); + searchEditTextLayout.setPreImeKeyListener(mSearchEditTextLayoutListener); + + mActionBarController = new ActionBarController(this, searchEditTextLayout); + + mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); + mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); + mVoiceSearchButton = searchEditTextLayout.findViewById(R.id.voice_search_button); + searchEditTextLayout + .findViewById(R.id.search_magnifying_glass) + .setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout + .findViewById(R.id.search_box_start_search) + .setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout.setOnClickListener(mSearchViewOnClickListener); + searchEditTextLayout.setCallback( + new SearchEditTextLayout.Callback() { + @Override + public void onBackButtonClicked() { + onBackPressed(); + } + + @Override + public void onSearchViewClicked() { + // Hide FAB, as the keyboard is shown. + mFloatingActionButtonController.scaleOut(); + } + }); + + mIsLandscape = + getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + mPreviouslySelectedTabIndex = ListsFragment.TAB_INDEX_SPEED_DIAL; + final View floatingActionButtonContainer = findViewById(R.id.floating_action_button_container); + ImageButton floatingActionButton = (ImageButton) findViewById(R.id.floating_action_button); + floatingActionButton.setOnClickListener(this); + mFloatingActionButtonController = + new FloatingActionButtonController( + this, floatingActionButtonContainer, floatingActionButton); + + ImageButton optionsMenuButton = + (ImageButton) searchEditTextLayout.findViewById(R.id.dialtacts_options_menu_button); + optionsMenuButton.setOnClickListener(this); + mOverflowMenu = buildOptionsMenu(optionsMenuButton); + optionsMenuButton.setOnTouchListener(mOverflowMenu.getDragToOpenListener()); + + // Add the favorites fragment but only if savedInstanceState is null. Otherwise the + // fragment manager is responsible for recreating it. + if (savedInstanceState == null) { + getFragmentManager() + .beginTransaction() + .add(R.id.dialtacts_frame, new ListsFragment(), TAG_FAVORITES_FRAGMENT) + .commit(); + } else { + mSearchQuery = savedInstanceState.getString(KEY_SEARCH_QUERY); + mInRegularSearch = savedInstanceState.getBoolean(KEY_IN_REGULAR_SEARCH_UI); + mInDialpadSearch = savedInstanceState.getBoolean(KEY_IN_DIALPAD_SEARCH_UI); + mFirstLaunch = savedInstanceState.getBoolean(KEY_FIRST_LAUNCH); + mWasConfigurationChange = savedInstanceState.getBoolean(KEY_WAS_CONFIGURATION_CHANGE); + mShowDialpadOnResume = savedInstanceState.getBoolean(KEY_IS_DIALPAD_SHOWN); + mActionBarController.restoreInstanceState(savedInstanceState); + } + + final boolean isLayoutRtl = ViewUtil.isRtl(); + if (mIsLandscape) { + mSlideIn = + AnimationUtils.loadAnimation( + this, isLayoutRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right); + mSlideOut = + AnimationUtils.loadAnimation( + this, isLayoutRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right); + } else { + mSlideIn = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_in_bottom); + mSlideOut = AnimationUtils.loadAnimation(this, R.anim.dialpad_slide_out_bottom); + } + + mSlideIn.setInterpolator(AnimUtils.EASE_IN); + mSlideOut.setInterpolator(AnimUtils.EASE_OUT); + + mSlideIn.setAnimationListener(mSlideInListener); + mSlideOut.setAnimationListener(mSlideOutListener); + + mParentLayout = (CoordinatorLayout) findViewById(R.id.dialtacts_mainlayout); + mParentLayout.setOnDragListener(new LayoutOnDragListener()); + floatingActionButtonContainer + .getViewTreeObserver() + .addOnGlobalLayoutListener( + new ViewTreeObserver.OnGlobalLayoutListener() { + @Override + public void onGlobalLayout() { + final ViewTreeObserver observer = + floatingActionButtonContainer.getViewTreeObserver(); + if (!observer.isAlive()) { + return; + } + observer.removeOnGlobalLayoutListener(this); + int screenWidth = mParentLayout.getWidth(); + mFloatingActionButtonController.setScreenWidth(screenWidth); + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + } + }); + + Trace.endSection(); + + Trace.beginSection(TAG + " initialize smart dialing"); + mDialerDatabaseHelper = Database.get(this).getDatabaseHelper(this); + SmartDialPrefix.initializeNanpSettings(this); + Trace.endSection(); + + mP13nLogger = P13nLogging.get(getApplicationContext()); + mP13nRanker = P13nRanking.get(getApplicationContext()); + Trace.endSection(); + } + + @NonNull + private ActionBar getActionBarSafely() { + return Assert.isNotNull(getSupportActionBar()); + } + + @Override + protected void onResume() { + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + mStateSaved = false; + if (mFirstLaunch) { + displayFragment(getIntent()); + } else if (!phoneIsInUse() && mInCallDialpadUp) { + hideDialpadFragment(false, true); + mInCallDialpadUp = false; + } else if (mShowDialpadOnResume) { + showDialpadFragment(false); + mShowDialpadOnResume = false; + } + + // If there was a voice query result returned in the {@link #onActivityResult} callback, it + // will have been stashed in mVoiceSearchQuery since the search results fragment cannot be + // shown until onResume has completed. Active the search UI and set the search term now. + if (!TextUtils.isEmpty(mVoiceSearchQuery)) { + mActionBarController.onSearchBoxTapped(); + mSearchView.setText(mVoiceSearchQuery); + mVoiceSearchQuery = null; + } + + mFirstLaunch = false; + + if (mIsRestarting) { + // This is only called when the activity goes from resumed -> paused -> resumed, so it + // will not cause an extra view to be sent out on rotation + if (mIsDialpadShown) { + Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); + } + mIsRestarting = false; + } + + prepareVoiceSearchButton(); + if (!mWasConfigurationChange) { + mDialerDatabaseHelper.startSmartDialUpdateThread(); + } + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + + if (Calls.CONTENT_TYPE.equals(getIntent().getType())) { + // Externally specified extras take precedence to EXTRA_SHOW_TAB, which is only + // used internally. + final Bundle extras = getIntent().getExtras(); + if (extras != null && extras.getInt(Calls.EXTRA_CALL_TYPE_FILTER) == Calls.VOICEMAIL_TYPE) { + mListsFragment.showTab(ListsFragment.TAB_INDEX_VOICEMAIL); + } else { + mListsFragment.showTab(ListsFragment.TAB_INDEX_HISTORY); + } + } else if (getIntent().hasExtra(EXTRA_SHOW_TAB)) { + int index = getIntent().getIntExtra(EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_SPEED_DIAL); + if (index < mListsFragment.getTabCount()) { + // Hide dialpad since this is an explicit intent to show a specific tab, which is coming + // from missed call or voicemail notification. + hideDialpadFragment(false, false); + exitSearchUi(); + mListsFragment.showTab(index); + } + } + + if (getIntent().getBooleanExtra(EXTRA_CLEAR_NEW_VOICEMAILS, false)) { + CallLogNotificationsService.markNewVoicemailsAsOld(this); + } + + setSearchBoxHint(); + + mP13nLogger.reset(); + mP13nRanker.refresh( + new P13nRefreshCompleteListener() { + @Override + public void onP13nRefreshComplete() { + // TODO: make zero-query search results visible + } + }); + Trace.endSection(); + } + + @Override + protected void onRestart() { + super.onRestart(); + mIsRestarting = true; + } + + @Override + protected void onPause() { + if (mClearSearchOnPause) { + hideDialpadAndSearchUi(); + mClearSearchOnPause = false; + } + if (mSlideOut.hasStarted() && !mSlideOut.hasEnded()) { + commitDialpadFragmentHide(); + } + super.onPause(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putString(KEY_SEARCH_QUERY, mSearchQuery); + outState.putBoolean(KEY_IN_REGULAR_SEARCH_UI, mInRegularSearch); + outState.putBoolean(KEY_IN_DIALPAD_SEARCH_UI, mInDialpadSearch); + outState.putBoolean(KEY_FIRST_LAUNCH, mFirstLaunch); + outState.putBoolean(KEY_IS_DIALPAD_SHOWN, mIsDialpadShown); + outState.putBoolean(KEY_WAS_CONFIGURATION_CHANGE, isChangingConfigurations()); + mActionBarController.saveInstanceState(outState); + mStateSaved = true; + } + + @Override + public void onAttachFragment(final Fragment fragment) { + if (fragment instanceof DialpadFragment) { + mDialpadFragment = (DialpadFragment) fragment; + if (!mIsDialpadShown && !mShowDialpadOnResume) { + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + transaction.hide(mDialpadFragment); + transaction.commit(); + } + } else if (fragment instanceof SmartDialSearchFragment) { + mSmartDialSearchFragment = (SmartDialSearchFragment) fragment; + mSmartDialSearchFragment.setOnPhoneNumberPickerActionListener(this); + if (!TextUtils.isEmpty(mDialpadQuery)) { + mSmartDialSearchFragment.setAddToContactNumber(mDialpadQuery); + } + } else if (fragment instanceof SearchFragment) { + mRegularSearchFragment = (RegularSearchFragment) fragment; + mRegularSearchFragment.setOnPhoneNumberPickerActionListener(this); + } else if (fragment instanceof ListsFragment) { + mListsFragment = (ListsFragment) fragment; + mListsFragment.addOnPageChangeListener(this); + } + if (fragment instanceof SearchFragment) { + final SearchFragment searchFragment = (SearchFragment) fragment; + searchFragment.setReranker( + new CursorReranker() { + @Override + @MainThread + public Cursor rerankCursor(Cursor data) { + Assert.isMainThread(); + return mP13nRanker.rankCursor(data, PhoneQuery.PHONE_NUMBER); + } + }); + searchFragment.addOnLoadFinishedListener( + new OnLoadFinishedListener() { + @Override + public void onLoadFinished() { + mP13nLogger.onSearchQuery( + searchFragment.getQueryString(), + (PhoneNumberListAdapter) searchFragment.getAdapter()); + } + }); + } + } + + protected void handleMenuSettings() { + final Intent intent = new Intent(this, DialerSettingsActivity.class); + startActivity(intent); + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.floating_action_button) { + if (mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_ALL_CONTACTS + && !mInRegularSearch + && !mInDialpadSearch) { + DialerUtils.startActivityWithErrorToast( + this, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); + Logger.get(this).logImpression(DialerImpression.Type.NEW_CONTACT_FAB); + } else if (!mIsDialpadShown) { + mInCallDialpadUp = false; + showDialpadFragment(true); + } + } else if (resId == R.id.voice_search_button) { + try { + startActivityForResult( + new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), + ACTIVITY_REQUEST_CODE_VOICE_SEARCH); + } catch (ActivityNotFoundException e) { + Toast.makeText( + DialtactsActivity.this, R.string.voice_search_not_available, Toast.LENGTH_SHORT) + .show(); + } + } else if (resId == R.id.dialtacts_options_menu_button) { + mOverflowMenu.show(); + } else { + Assert.fail("Unexpected onClick event from " + view); + } + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (!isSafeToCommitTransactions()) { + return true; + } + + int resId = item.getItemId(); + if (item.getItemId() == R.id.menu_delete_all) { + ClearCallLogDialog.show(getFragmentManager()); + return true; + } else if (resId == R.id.menu_clear_frequents) { + ClearFrequentsDialog.show(getFragmentManager()); + Logger.get(this).logScreenView(ScreenEvent.Type.CLEAR_FREQUENTS, this); + return true; + } else if (resId == R.id.menu_call_settings) { + handleMenuSettings(); + Logger.get(this).logScreenView(ScreenEvent.Type.SETTINGS, this); + return true; + } + return false; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == ACTIVITY_REQUEST_CODE_VOICE_SEARCH) { + if (resultCode == RESULT_OK) { + final ArrayList matches = + data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); + if (matches.size() > 0) { + mVoiceSearchQuery = matches.get(0); + } else { + LogUtil.i("DialtactsActivity.onActivityResult", "voice search - nothing heard"); + } + } else { + LogUtil.e("DialtactsActivity.onActivityResult", "voice search failed: " + resultCode); + } + } else if (requestCode == ACTIVITY_REQUEST_CODE_CALL_COMPOSE) { + if (resultCode != RESULT_OK) { + LogUtil.i( + "DialtactsActivity.onActivityResult", + "returned from call composer, error occurred (resultCode=" + resultCode + ")"); + String message = + getString(R.string.call_composer_connection_failed, getString(R.string.share_and_call)); + Snackbar.make(mParentLayout, message, Snackbar.LENGTH_LONG).show(); + } else { + LogUtil.i("DialtactsActivity.onActivityResult", "returned from call composer, no error"); + } + } + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Update the number of unread voicemails (potentially other tabs) displayed next to the tab icon. + */ + public void updateTabUnreadCounts() { + mListsFragment.updateTabUnreadCounts(); + } + + /** + * Initiates a fragment transaction to show the dialpad fragment. Animations and other visual + * updates are handled by a callback which is invoked after the dialpad fragment is shown. + * + * @see #onDialpadShown + */ + private void showDialpadFragment(boolean animate) { + if (mIsDialpadShown || mStateSaved) { + return; + } + mIsDialpadShown = true; + + mListsFragment.setUserVisibleHint(false); + + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + if (mDialpadFragment == null) { + mDialpadFragment = new DialpadFragment(); + ft.add(R.id.dialtacts_container, mDialpadFragment, TAG_DIALPAD_FRAGMENT); + } else { + ft.show(mDialpadFragment); + } + + mDialpadFragment.setAnimate(animate); + Logger.get(this).logScreenView(ScreenEvent.Type.DIALPAD, this); + ft.commit(); + + if (animate) { + mFloatingActionButtonController.scaleOut(); + } else { + mFloatingActionButtonController.setVisible(false); + maybeEnterSearchUi(); + } + mActionBarController.onDialpadUp(); + + Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); + + //adjust the title, so the user will know where we're at when the activity start/resumes. + setTitle(R.string.launcherDialpadActivityLabel); + } + + /** Callback from child DialpadFragment when the dialpad is shown. */ + public void onDialpadShown() { + Assert.isNotNull(mDialpadFragment); + if (mDialpadFragment.getAnimate()) { + Assert.isNotNull(mDialpadFragment.getView()).startAnimation(mSlideIn); + } else { + mDialpadFragment.setYFraction(0); + } + + updateSearchFragmentPosition(); + } + + /** + * Initiates animations and other visual updates to hide the dialpad. The fragment is hidden in a + * callback after the hide animation ends. + * + * @see #commitDialpadFragmentHide + */ + public void hideDialpadFragment(boolean animate, boolean clearDialpad) { + if (mDialpadFragment == null || mDialpadFragment.getView() == null) { + return; + } + if (clearDialpad) { + // Temporarily disable accessibility when we clear the dialpad, since it should be + // invisible and should not announce anything. + mDialpadFragment + .getDigitsWidget() + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + mDialpadFragment.clearDialpad(); + mDialpadFragment + .getDigitsWidget() + .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_AUTO); + } + if (!mIsDialpadShown) { + return; + } + mIsDialpadShown = false; + mDialpadFragment.setAnimate(animate); + mListsFragment.setUserVisibleHint(true); + mListsFragment.sendScreenViewForCurrentPosition(); + + updateSearchFragmentPosition(); + + mFloatingActionButtonController.align(getFabAlignment(), animate); + if (animate) { + mDialpadFragment.getView().startAnimation(mSlideOut); + } else { + commitDialpadFragmentHide(); + } + + mActionBarController.onDialpadDown(); + + if (isInSearchUi()) { + if (TextUtils.isEmpty(mSearchQuery)) { + exitSearchUi(); + } + } + //reset the title to normal. + setTitle(R.string.launcherActivityLabel); + } + + /** Finishes hiding the dialpad fragment after any animations are completed. */ + private void commitDialpadFragmentHide() { + if (!mStateSaved && mDialpadFragment != null && !mDialpadFragment.isHidden()) { + final FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.hide(mDialpadFragment); + ft.commit(); + } + mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); + } + + private void updateSearchFragmentPosition() { + SearchFragment fragment = null; + if (mSmartDialSearchFragment != null && mSmartDialSearchFragment.isVisible()) { + fragment = mSmartDialSearchFragment; + } else if (mRegularSearchFragment != null && mRegularSearchFragment.isVisible()) { + fragment = mRegularSearchFragment; + } + if (fragment != null && fragment.isVisible()) { + fragment.updatePosition(true /* animate */); + } + } + + @Override + public boolean isInSearchUi() { + return mInDialpadSearch || mInRegularSearch; + } + + @Override + public boolean hasSearchQuery() { + return !TextUtils.isEmpty(mSearchQuery); + } + + @Override + public boolean shouldShowActionBar() { + return mListsFragment.shouldShowActionBar(); + } + + private void setNotInSearchUi() { + mInDialpadSearch = false; + mInRegularSearch = false; + } + + private void hideDialpadAndSearchUi() { + if (mIsDialpadShown) { + hideDialpadFragment(false, true); + } else { + exitSearchUi(); + } + } + + private void prepareVoiceSearchButton() { + final Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + if (canIntentBeHandled(voiceIntent)) { + mVoiceSearchButton.setVisibility(View.VISIBLE); + mVoiceSearchButton.setOnClickListener(this); + } else { + mVoiceSearchButton.setVisibility(View.GONE); + } + } + + public boolean isNearbyPlacesSearchEnabled() { + return false; + } + + protected int getSearchBoxHint() { + return R.string.dialer_hint_find_contact; + } + + /** Sets the hint text for the contacts search box */ + private void setSearchBoxHint() { + SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) + getActionBarSafely().getCustomView().findViewById(R.id.search_view_container); + ((TextView) searchEditTextLayout.findViewById(R.id.search_box_start_search)) + .setHint(getSearchBoxHint()); + } + + protected OptionsPopupMenu buildOptionsMenu(View invoker) { + final OptionsPopupMenu popupMenu = new OptionsPopupMenu(this, invoker); + popupMenu.inflate(R.menu.dialtacts_options); + popupMenu.setOnMenuItemClickListener(this); + return popupMenu; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (mPendingSearchViewQuery != null) { + mSearchView.setText(mPendingSearchViewQuery); + mPendingSearchViewQuery = null; + } + if (mActionBarController != null) { + mActionBarController.restoreActionBarOffset(); + } + return false; + } + + /** + * Returns true if the intent is due to hitting the green send key (hardware call button: + * KEYCODE_CALL) while in a call. + * + * @param intent the intent that launched this activity + * @return true if the intent is due to hitting the green send key while in a call + */ + private boolean isSendKeyWhileInCall(Intent intent) { + // If there is a call in progress and the user launched the dialer by hitting the call + // button, go straight to the in-call screen. + final boolean callKey = Intent.ACTION_CALL_BUTTON.equals(intent.getAction()); + + // When KEYCODE_CALL event is handled it dispatches an intent with the ACTION_CALL_BUTTON. + // Besides of checking the intent action, we must check if the phone is really during a + // call in order to decide whether to ignore the event or continue to display the activity. + if (callKey && phoneIsInUse()) { + TelecomUtil.showInCallScreen(this, false); + return true; + } + + return false; + } + + /** + * Sets the current tab based on the intent's request type + * + * @param intent Intent that contains information about which tab should be selected + */ + private void displayFragment(Intent intent) { + // If we got here by hitting send and we're in call forward along to the in-call activity + if (isSendKeyWhileInCall(intent)) { + finish(); + return; + } + + final boolean showDialpadChooser = + !ACTION_SHOW_TAB.equals(intent.getAction()) + && phoneIsInUse() + && !DialpadFragment.isAddCallMode(intent); + if (showDialpadChooser || (intent.getData() != null && isDialIntent(intent))) { + showDialpadFragment(false); + mDialpadFragment.setStartedFromNewIntent(true); + if (showDialpadChooser && !mDialpadFragment.isVisible()) { + mInCallDialpadUp = true; + } + } + } + + @Override + public void onNewIntent(Intent newIntent) { + setIntent(newIntent); + + mStateSaved = false; + displayFragment(newIntent); + + invalidateOptionsMenu(); + } + + /** Returns true if the given intent contains a phone number to populate the dialer with */ + private boolean isDialIntent(Intent intent) { + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || ACTION_TOUCH_DIALER.equals(action)) { + return true; + } + if (Intent.ACTION_VIEW.equals(action)) { + final Uri data = intent.getData(); + if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { + return true; + } + } + return false; + } + + /** Shows the search fragment */ + private void enterSearchUi(boolean smartDialSearch, String query, boolean animate) { + if (mStateSaved || getFragmentManager().isDestroyed()) { + // Weird race condition where fragment is doing work after the activity is destroyed + // due to talkback being on (b/10209937). Just return since we can't do any + // constructive here. + return; + } + + if (DEBUG) { + LogUtil.v("DialtactsActivity.enterSearchUi", "smart dial " + smartDialSearch); + } + + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + if (mInDialpadSearch && mSmartDialSearchFragment != null) { + transaction.remove(mSmartDialSearchFragment); + } else if (mInRegularSearch && mRegularSearchFragment != null) { + transaction.remove(mRegularSearchFragment); + } + + final String tag; + if (smartDialSearch) { + tag = TAG_SMARTDIAL_SEARCH_FRAGMENT; + } else { + tag = TAG_REGULAR_SEARCH_FRAGMENT; + } + mInDialpadSearch = smartDialSearch; + mInRegularSearch = !smartDialSearch; + + mFloatingActionButtonController.scaleOut(); + + SearchFragment fragment = (SearchFragment) getFragmentManager().findFragmentByTag(tag); + if (animate) { + transaction.setCustomAnimations(android.R.animator.fade_in, 0); + } else { + transaction.setTransition(FragmentTransaction.TRANSIT_NONE); + } + if (fragment == null) { + if (smartDialSearch) { + fragment = new SmartDialSearchFragment(); + } else { + fragment = Bindings.getLegacy(this).newRegularSearchFragment(); + fragment.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + // Show the FAB when the user touches the lists fragment and the soft + // keyboard is hidden. + hideDialpadFragment(true, false); + showFabInSearchUi(); + v.performClick(); + return false; + } + }); + } + transaction.add(R.id.dialtacts_frame, fragment, tag); + } else { + transaction.show(fragment); + } + // DialtactsActivity will provide the options menu + fragment.setHasOptionsMenu(false); + fragment.setShowEmptyListForNullQuery(true); + if (!smartDialSearch) { + fragment.setQueryString(query); + } + transaction.commit(); + + if (animate) { + Assert.isNotNull(mListsFragment.getView()).animate().alpha(0).withLayer(); + } + mListsFragment.setUserVisibleHint(false); + + if (smartDialSearch) { + Logger.get(this).logScreenView(ScreenEvent.Type.SMART_DIAL_SEARCH, this); + } else { + Logger.get(this).logScreenView(ScreenEvent.Type.REGULAR_SEARCH, this); + } + } + + /** Hides the search fragment */ + private void exitSearchUi() { + // See related bug in enterSearchUI(); + if (getFragmentManager().isDestroyed() || mStateSaved) { + return; + } + + mSearchView.setText(null); + + if (mDialpadFragment != null) { + mDialpadFragment.clearDialpad(); + } + + setNotInSearchUi(); + + // Restore the FAB for the lists fragment. + if (getFabAlignment() != FloatingActionButtonController.ALIGN_END) { + mFloatingActionButtonController.setVisible(false); + } + mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); + onPageScrolled(mListsFragment.getCurrentTabIndex(), 0 /* offset */, 0 /* pixelOffset */); + onPageSelected(mListsFragment.getCurrentTabIndex()); + + final FragmentTransaction transaction = getFragmentManager().beginTransaction(); + if (mSmartDialSearchFragment != null) { + transaction.remove(mSmartDialSearchFragment); + } + if (mRegularSearchFragment != null) { + transaction.remove(mRegularSearchFragment); + } + transaction.commit(); + + Assert.isNotNull(mListsFragment.getView()).animate().alpha(1).withLayer(); + + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { + // If the dialpad fragment wasn't previously visible, then send a screen view because + // we are exiting regular search. Otherwise, the screen view will be sent by + // {@link #hideDialpadFragment}. + mListsFragment.sendScreenViewForCurrentPosition(); + mListsFragment.setUserVisibleHint(true); + } + + mActionBarController.onSearchUiExited(); + } + + @Override + public void onBackPressed() { + if (mStateSaved) { + return; + } + if (mIsDialpadShown) { + if (TextUtils.isEmpty(mSearchQuery) + || (mSmartDialSearchFragment != null + && mSmartDialSearchFragment.isVisible() + && mSmartDialSearchFragment.getAdapter().getCount() == 0)) { + exitSearchUi(); + } + hideDialpadFragment(true, false); + } else if (isInSearchUi()) { + exitSearchUi(); + DialerUtils.hideInputMethod(mParentLayout); + } else { + super.onBackPressed(); + } + } + + private void maybeEnterSearchUi() { + if (!isInSearchUi()) { + enterSearchUi(true /* isSmartDial */, mSearchQuery, false); + } + } + + /** @return True if the search UI was exited, false otherwise */ + private boolean maybeExitSearchUi() { + if (isInSearchUi() && TextUtils.isEmpty(mSearchQuery)) { + exitSearchUi(); + DialerUtils.hideInputMethod(mParentLayout); + return true; + } + return false; + } + + private void showFabInSearchUi() { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.fab_ic_dial, null), + getResources().getString(R.string.action_menu_dialpad_button)); + mFloatingActionButtonController.align(getFabAlignment(), false /* animate */); + mFloatingActionButtonController.scaleIn(FAB_SCALE_IN_DELAY_MS); + } + + @Override + public void onDialpadQueryChanged(String query) { + mDialpadQuery = query; + if (mSmartDialSearchFragment != null) { + mSmartDialSearchFragment.setAddToContactNumber(query); + } + final String normalizedQuery = + SmartDialNameMatcher.normalizeNumber(query, SmartDialNameMatcher.LATIN_SMART_DIAL_MAP); + + if (!TextUtils.equals(mSearchView.getText(), normalizedQuery)) { + if (DEBUG) { + LogUtil.v("DialtactsActivity.onDialpadQueryChanged", "new query: " + query); + } + if (mDialpadFragment == null || !mDialpadFragment.isVisible()) { + // This callback can happen if the dialpad fragment is recreated because of + // activity destruction. In that case, don't update the search view because + // that would bring the user back to the search fragment regardless of the + // previous state of the application. Instead, just return here and let the + // fragment manager correctly figure out whatever fragment was last displayed. + if (!TextUtils.isEmpty(normalizedQuery)) { + mPendingSearchViewQuery = normalizedQuery; + } + return; + } + mSearchView.setText(normalizedQuery); + } + + try { + if (mDialpadFragment != null && mDialpadFragment.isVisible()) { + mDialpadFragment.process_quote_emergency_unquote(normalizedQuery); + } + } catch (Exception ignored) { + // Skip any exceptions for this piece of code + } + } + + @Override + public boolean onDialpadSpacerTouchWithEmptyQuery() { + if (mInDialpadSearch + && mSmartDialSearchFragment != null + && !mSmartDialSearchFragment.isShowingPermissionRequest()) { + hideDialpadFragment(true /* animate */, true /* clearDialpad */); + return true; + } + return false; + } + + @Override + public void onListFragmentScrollStateChange(int scrollState) { + if (scrollState == OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) { + hideDialpadFragment(true, false); + DialerUtils.hideInputMethod(mParentLayout); + } + } + + @Override + public void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) { + // TODO: No-op for now. This should eventually show/hide the actionBar based on + // interactions with the ListsFragments. + } + + private boolean phoneIsInUse() { + return TelecomUtil.isInCall(this); + } + + private boolean canIntentBeHandled(Intent intent) { + final PackageManager packageManager = getPackageManager(); + final List resolveInfo = + packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + return resolveInfo != null && resolveInfo.size() > 0; + } + + /** Called when the user has long-pressed a contact tile to start a drag operation. */ + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { + mListsFragment.showRemoveView(true); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) {} + + /** Called when the user has released a contact tile after long-pressing it. */ + @Override + public void onDragFinished(int x, int y) { + mListsFragment.showRemoveView(false); + } + + @Override + public void onDroppedOnRemove() {} + + /** + * Allows the SpeedDialFragment to attach the drag controller to mRemoveViewContainer once it has + * been attached to the activity. + */ + @Override + public void setDragDropController(DragDropController dragController) { + mDragDropController = dragController; + mListsFragment.getRemoveView().setDragDropController(dragController); + } + + /** Implemented to satisfy {@link SpeedDialFragment.HostInterface} */ + @Override + public void showAllContactsTab() { + if (mListsFragment != null) { + mListsFragment.showTab(ListsFragment.TAB_INDEX_ALL_CONTACTS); + } + } + + /** Implemented to satisfy {@link CallLogFragment.HostInterface} */ + @Override + public void showDialpad() { + showDialpadFragment(true); + } + + @Override + public void enableFloatingButton(boolean enabled) { + LogUtil.d("DialtactsActivity.enableFloatingButton", "enable: %b", enabled); + // Floating button shouldn't be enabled when dialpad is shown. + if (!isDialpadShown() || !enabled) { + mFloatingActionButtonController.setVisible(enabled); + } + } + + @Override + public void onPickDataUri( + Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { + mClearSearchOnPause = true; + PhoneNumberInteraction.startInteractionForPhoneCall( + DialtactsActivity.this, dataUri, isVideoCall, callSpecificAppData); + } + + @Override + public void onPickPhoneNumber( + String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData) { + if (phoneNumber == null) { + // Invalid phone number, but let the call go through so that InCallUI can show + // an error message. + phoneNumber = ""; + } + + Intent intent = + new CallIntentBuilder(phoneNumber, callSpecificAppData).setIsVideoCall(isVideoCall).build(); + + DialerUtils.startActivityWithErrorToast(this, intent); + mClearSearchOnPause = true; + } + + @Override + public void onHomeInActionBarSelected() { + exitSearchUi(); + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + int tabIndex = mListsFragment.getCurrentTabIndex(); + + // Scroll the button from center to end when moving from the Speed Dial to Call History tab. + // In RTL, scroll when the current tab is Call History instead, since the order of the tabs + // is reversed and the ViewPager returns the left tab position during scroll. + boolean isRtl = ViewUtil.isRtl(); + if (!isRtl && tabIndex == ListsFragment.TAB_INDEX_SPEED_DIAL && !mIsLandscape) { + mFloatingActionButtonController.onPageScrolled(positionOffset); + } else if (isRtl && tabIndex == ListsFragment.TAB_INDEX_HISTORY && !mIsLandscape) { + mFloatingActionButtonController.onPageScrolled(1 - positionOffset); + } else if (tabIndex != ListsFragment.TAB_INDEX_SPEED_DIAL) { + mFloatingActionButtonController.onPageScrolled(1); + } + } + + @Override + public void onPageSelected(int position) { + updateMissedCalls(); + int tabIndex = mListsFragment.getCurrentTabIndex(); + mPreviouslySelectedTabIndex = tabIndex; + mFloatingActionButtonController.setVisible(true); + if (tabIndex == ListsFragment.TAB_INDEX_ALL_CONTACTS + && !mInRegularSearch + && !mInDialpadSearch) { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.ic_person_add_24dp, null), + getResources().getString(R.string.search_shortcut_create_new_contact)); + } else { + mFloatingActionButtonController.changeIcon( + getResources().getDrawable(R.drawable.fab_ic_dial, null), + getResources().getString(R.string.action_menu_dialpad_button)); + } + } + + @Override + public void onPageScrollStateChanged(int state) {} + + @Override + public boolean isActionBarShowing() { + return mActionBarController.isActionBarShowing(); + } + + @Override + public ActionBarController getActionBarController() { + return mActionBarController; + } + + @Override + public boolean isDialpadShown() { + return mIsDialpadShown; + } + + @Override + public int getDialpadHeight() { + if (mDialpadFragment != null) { + return mDialpadFragment.getDialpadHeight(); + } + return 0; + } + + @Override + public int getActionBarHideOffset() { + return getActionBarSafely().getHideOffset(); + } + + @Override + public void setActionBarHideOffset(int offset) { + getActionBarSafely().setHideOffset(offset); + } + + @Override + public int getActionBarHeight() { + return mActionBarHeight; + } + + private int getFabAlignment() { + if (!mIsLandscape + && !isInSearchUi() + && mListsFragment.getCurrentTabIndex() == ListsFragment.TAB_INDEX_SPEED_DIAL) { + return FloatingActionButtonController.ALIGN_MIDDLE; + } + return FloatingActionButtonController.ALIGN_END; + } + + private void updateMissedCalls() { + if (mPreviouslySelectedTabIndex == ListsFragment.TAB_INDEX_HISTORY) { + mListsFragment.markMissedCallsAsReadAndRemoveNotifications(); + } + } + + @Override + public void onDisambigDialogDismissed() { + // Don't do anything; the app will remain open with favorites tiles displayed. + } + + @Override + public void interactionError(@InteractionErrorCode int interactionErrorCode) { + switch (interactionErrorCode) { + case InteractionErrorCode.USER_LEAVING_ACTIVITY: + // This is expected to happen if the user exits the activity before the interaction occurs. + return; + case InteractionErrorCode.CONTACT_NOT_FOUND: + case InteractionErrorCode.CONTACT_HAS_NO_NUMBER: + case InteractionErrorCode.OTHER_ERROR: + default: + // All other error codes are unexpected. For example, it should be impossible to start an + // interaction with an invalid contact from the Dialtacts activity. + Assert.fail("PhoneNumberInteraction error: " + interactionErrorCode); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + // This should never happen; it should be impossible to start an interaction without the + // contacts permission from the Dialtacts activity. + Assert.fail( + String.format( + Locale.US, + "Permissions requested unexpectedly: %d/%s/%s", + requestCode, + Arrays.toString(permissions), + Arrays.toString(grantResults))); + } + + protected class OptionsPopupMenu extends PopupMenu { + + public OptionsPopupMenu(Context context, View anchor) { + super(context, anchor, Gravity.END); + } + + @Override + public void show() { + final boolean hasContactsPermission = + PermissionsUtil.hasContactsPermissions(DialtactsActivity.this); + final Menu menu = getMenu(); + final MenuItem clearFrequents = menu.findItem(R.id.menu_clear_frequents); + clearFrequents.setVisible( + mListsFragment != null + && mListsFragment.getSpeedDialFragment() != null + && mListsFragment.getSpeedDialFragment().hasFrequents() + && hasContactsPermission); + + menu.findItem(R.id.menu_delete_all) + .setVisible(PermissionsUtil.hasPhonePermissions(DialtactsActivity.this)); + super.show(); + } + } + + /** + * Listener that listens to drag events and sends their x and y coordinates to a {@link + * DragDropController}. + */ + private class LayoutOnDragListener implements OnDragListener { + + @Override + public boolean onDrag(View v, DragEvent event) { + if (event.getAction() == DragEvent.ACTION_DRAG_LOCATION) { + mDragDropController.handleDragHovered(v, (int) event.getX(), (int) event.getY()); + } + return true; + } + } +} diff --git a/java/com/android/dialer/app/FloatingActionButtonBehavior.java b/java/com/android/dialer/app/FloatingActionButtonBehavior.java new file mode 100644 index 000000000..d4a79ca19 --- /dev/null +++ b/java/com/android/dialer/app/FloatingActionButtonBehavior.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.dialer.app; + +import android.content.Context; +import android.support.design.widget.CoordinatorLayout; +import android.support.design.widget.Snackbar.SnackbarLayout; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import com.android.dialer.proguard.UsedByReflection; + +/** + * Implements custom behavior for the movement of the FAB in response to the Snackbar. Because we + * are not using the design framework FloatingActionButton widget, we need to manually implement the + * Material Design behavior of having the FAB translate upward and downward with the appearance and + * disappearance of a Snackbar. + */ +@UsedByReflection(value = "dialtacts_activity.xml") +public class FloatingActionButtonBehavior extends CoordinatorLayout.Behavior { + + @UsedByReflection(value = "dialtacts_activity.xml") + public FloatingActionButtonBehavior(Context context, AttributeSet attrs) {} + + @Override + public boolean layoutDependsOn(CoordinatorLayout parent, FrameLayout child, View dependency) { + return dependency instanceof SnackbarLayout; + } + + @Override + public boolean onDependentViewChanged( + CoordinatorLayout parent, FrameLayout child, View dependency) { + float translationY = Math.min(0, dependency.getTranslationY() - dependency.getHeight()); + child.setTranslationY(translationY); + return true; + } +} diff --git a/java/com/android/dialer/app/PhoneCallDetails.java b/java/com/android/dialer/app/PhoneCallDetails.java new file mode 100644 index 000000000..436f68eec --- /dev/null +++ b/java/com/android/dialer/app/PhoneCallDetails.java @@ -0,0 +1,207 @@ +/* + * 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.dialer.app; + +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.contacts.common.ContactsUtils.UserType; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.calllog.PhoneNumberDisplayUtil; +import com.android.dialer.phonenumbercache.ContactInfo; + +/** The details of a phone call to be shown in the UI. */ +public class PhoneCallDetails { + + // The number of the other party involved in the call. + public CharSequence number; + // Post-dial digits associated with the outgoing call. + public String postDialDigits; + // The secondary line number the call was received via. + public String viaNumber; + // The number presenting rules set by the network, e.g., {@link Calls#PRESENTATION_ALLOWED} + public int numberPresentation; + // The country corresponding with the phone number. + public String countryIso; + // The geocoded location for the phone number. + public String geocode; + + /** + * The type of calls, as defined in the call log table, e.g., {@link Calls#INCOMING_TYPE}. + * + *

There might be multiple types if this represents a set of entries grouped together. + */ + public int[] callTypes; + + // The date of the call, in milliseconds since the epoch. + public long date; + // The duration of the call in milliseconds, or 0 for missed calls. + public long duration; + // The name of the contact, or the empty string. + public CharSequence namePrimary; + // The alternative name of the contact, e.g. last name first, or the empty string + public CharSequence nameAlternative; + /** + * The user's preference on name display order, last name first or first time first. {@see + * ContactsPreferences} + */ + public int nameDisplayOrder; + // The type of phone, e.g., {@link Phone#TYPE_HOME}, 0 if not available. + public int numberType; + // The custom label associated with the phone number in the contact, or the empty string. + public CharSequence numberLabel; + // The URI of the contact associated with this phone call. + public Uri contactUri; + + /** + * The photo URI of the picture of the contact that is associated with this phone call or null if + * there is none. + * + *

This is meant to store the high-res photo only. + */ + public Uri photoUri; + + // The source type of the contact associated with this call. + public int sourceType; + + // The object id type of the contact associated with this call. + public String objectId; + + // The unique identifier for the account associated with the call. + public PhoneAccountHandle accountHandle; + + // Features applicable to this call. + public int features; + + // Total data usage for this call. + public Long dataUsage; + + // Voicemail transcription + public String transcription; + + // The display string for the number. + public String displayNumber; + + // Whether the contact number is a voicemail number. + public boolean isVoicemail; + + /** The {@link UserType} of the contact */ + public @UserType long contactUserType; + + /** + * If this is a voicemail, whether the message is read. For other types of calls, this defaults to + * {@code true}. + */ + public boolean isRead = true; + + // If this call is a spam number. + public boolean isSpam = false; + + // If this call is a blocked number. + public boolean isBlocked = false; + + // Call location and date text. + public CharSequence callLocationAndDate; + + // Call description. + public CharSequence callDescription; + public String accountComponentName; + public String accountId; + public ContactInfo cachedContactInfo; + public int voicemailId; + public int previousGroup; + + /** + * Constructor with required fields for the details of a call with a number associated with a + * contact. + */ + public PhoneCallDetails( + CharSequence number, int numberPresentation, CharSequence postDialDigits) { + this.number = number; + this.numberPresentation = numberPresentation; + this.postDialDigits = postDialDigits.toString(); + } + /** + * Construct the "on {accountLabel} via {viaNumber}" accessibility description for the account + * list item, depending on the existence of the accountLabel and viaNumber. + * + * @param viaNumber The number that this call is being placed via. + * @param accountLabel The {@link PhoneAccount} label that this call is being placed with. + * @return The description of the account that this call has been placed on. + */ + public static CharSequence createAccountLabelDescription( + Resources resources, @Nullable String viaNumber, @Nullable CharSequence accountLabel) { + + if ((!TextUtils.isEmpty(viaNumber)) && !TextUtils.isEmpty(accountLabel)) { + String msg = + resources.getString( + R.string.description_via_number_phone_account, accountLabel, viaNumber); + CharSequence accountNumberLabel = + ContactDisplayUtils.getTelephoneTtsSpannable(msg, viaNumber); + return (accountNumberLabel == null) ? msg : accountNumberLabel; + } else if (!TextUtils.isEmpty(viaNumber)) { + CharSequence viaNumberLabel = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + resources, R.string.description_via_number, viaNumber); + return (viaNumberLabel == null) ? viaNumber : viaNumberLabel; + } else if (!TextUtils.isEmpty(accountLabel)) { + return TextUtils.expandTemplate( + resources.getString(R.string.description_phone_account), accountLabel); + } + return ""; + } + + /** + * Returns the preferred name for the call details as specified by the {@link #nameDisplayOrder} + * + * @return the preferred name + */ + public CharSequence getPreferredName() { + if (nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY + || TextUtils.isEmpty(nameAlternative)) { + return namePrimary; + } + return nameAlternative; + } + + public void updateDisplayNumber( + Context context, CharSequence formattedNumber, boolean isVoicemail) { + displayNumber = + PhoneNumberDisplayUtil.getDisplayNumber( + context, number, numberPresentation, formattedNumber, postDialDigits, isVoicemail) + .toString(); + } + + public boolean hasIncomingCalls() { + for (int i = 0; i < callTypes.length; i++) { + if (callTypes[i] == CallLog.Calls.INCOMING_TYPE + || callTypes[i] == CallLog.Calls.MISSED_TYPE + || callTypes[i] == CallLog.Calls.VOICEMAIL_TYPE + || callTypes[i] == CallLog.Calls.REJECTED_TYPE + || callTypes[i] == CallLog.Calls.BLOCKED_TYPE) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/SpecialCharSequenceMgr.java b/java/com/android/dialer/app/SpecialCharSequenceMgr.java new file mode 100644 index 000000000..2ae19704a --- /dev/null +++ b/java/com/android/dialer/app/SpecialCharSequenceMgr.java @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2006 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.dialer.app; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.DialogFragment; +import android.app.KeyguardManager; +import android.app.ProgressDialog; +import android.content.ActivityNotFoundException; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Looper; +import android.provider.Settings; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.Toast; +import com.android.common.io.MoreCloseables; +import com.android.contacts.common.compat.TelephonyManagerCompat; +import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment; +import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.telecom.TelecomUtil; +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class to listen for some magic character sequences that are handled specially by the + * dialer. + * + *

Note the Phone app also handles these sequences too (in a couple of relatively obscure places + * in the UI), so there's a separate version of this class under apps/Phone. + * + *

TODO: there's lots of duplicated code between this class and the corresponding class under + * apps/Phone. Let's figure out a way to unify these two classes (in the framework? in a common + * shared library?) + */ +public class SpecialCharSequenceMgr { + + private static final String TAG = "SpecialCharSequenceMgr"; + + private static final String TAG_SELECT_ACCT_FRAGMENT = "tag_select_acct_fragment"; + + private static final String SECRET_CODE_ACTION = "android.provider.Telephony.SECRET_CODE"; + private static final String MMI_IMEI_DISPLAY = "*#06#"; + private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#"; + /** ***** This code is used to handle SIM Contact queries ***** */ + private static final String ADN_PHONE_NUMBER_COLUMN_NAME = "number"; + + private static final String ADN_NAME_COLUMN_NAME = "name"; + private static final int ADN_QUERY_TOKEN = -1; + /** + * Remembers the previous {@link QueryHandler} and cancel the operation when needed, to prevent + * possible crash. + * + *

QueryHandler may call {@link ProgressDialog#dismiss()} when the screen is already gone, + * which will cause the app crash. This variable enables the class to prevent the crash on {@link + * #cleanup()}. + * + *

TODO: Remove this and replace it (and {@link #cleanup()}) with better implementation. One + * complication is that we have SpecialCharSequenceMgr in Phone package too, which has *slightly* + * different implementation. Note that Phone package doesn't have this problem, so the class on + * Phone side doesn't have this functionality. Fundamental fix would be to have one shared + * implementation and resolve this corner case more gracefully. + */ + private static QueryHandler sPreviousAdnQueryHandler; + + /** This class is never instantiated. */ + private SpecialCharSequenceMgr() {} + + public static boolean handleChars(Context context, String input, EditText textField) { + //get rid of the separators so that the string gets parsed correctly + String dialString = PhoneNumberUtils.stripSeparators(input); + + return handleDeviceIdDisplay(context, dialString) + || handleRegulatoryInfoDisplay(context, dialString) + || handlePinEntry(context, dialString) + || handleAdnEntry(context, dialString, textField) + || handleSecretCode(context, dialString); + + } + + /** + * Cleanup everything around this class. Must be run inside the main thread. + * + *

This should be called when the screen becomes background. + */ + public static void cleanup() { + if (Looper.myLooper() != Looper.getMainLooper()) { + Log.wtf(TAG, "cleanup() is called outside the main thread"); + return; + } + + if (sPreviousAdnQueryHandler != null) { + sPreviousAdnQueryHandler.cancel(); + sPreviousAdnQueryHandler = null; + } + } + + /** + * Handles secret codes to launch arbitrary activities in the form of *#*##*#*. If a secret + * code is encountered an Intent is started with the android_secret_code:// URI. + * + * @param context the context to use + * @param input the text to check for a secret code in + * @return true if a secret code was encountered + */ + static boolean handleSecretCode(Context context, String input) { + // Secret codes are in the form *#*##*#* + int len = input.length(); + if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) { + final Intent intent = + new Intent( + SECRET_CODE_ACTION, + Uri.parse("android_secret_code://" + input.substring(4, len - 4))); + context.sendBroadcast(intent); + return true; + } + + return false; + } + + /** + * Handle ADN requests by filling in the SIM contact number into the requested EditText. + * + *

This code works alongside the Asynchronous query handler {@link QueryHandler} and query + * cancel handler implemented in {@link SimContactQueryCookie}. + */ + static boolean handleAdnEntry(Context context, String input, EditText textField) { + /* ADN entries are of the form "N(N)(N)#" */ + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null + || telephonyManager.getPhoneType() != TelephonyManager.PHONE_TYPE_GSM) { + return false; + } + + // if the phone is keyguard-restricted, then just ignore this + // input. We want to make sure that sim card contacts are NOT + // exposed unless the phone is unlocked, and this code can be + // accessed from the emergency dialer. + KeyguardManager keyguardManager = + (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + if (keyguardManager.inKeyguardRestrictedInputMode()) { + return false; + } + + int len = input.length(); + if ((len > 1) && (len < 5) && (input.endsWith("#"))) { + try { + // get the ordinal number of the sim contact + final int index = Integer.parseInt(input.substring(0, len - 1)); + + // The original code that navigated to a SIM Contacts list view did not + // highlight the requested contact correctly, a requirement for PTCRB + // certification. This behaviour is consistent with the UI paradigm + // for touch-enabled lists, so it does not make sense to try to work + // around it. Instead we fill in the the requested phone number into + // the dialer text field. + + // create the async query handler + final QueryHandler handler = new QueryHandler(context.getContentResolver()); + + // create the cookie object + final SimContactQueryCookie sc = + new SimContactQueryCookie(index - 1, handler, ADN_QUERY_TOKEN); + + // setup the cookie fields + sc.contactNum = index - 1; + sc.setTextField(textField); + + // create the progress dialog + sc.progressDialog = new ProgressDialog(context); + sc.progressDialog.setTitle(R.string.simContacts_title); + sc.progressDialog.setMessage(context.getText(R.string.simContacts_emptyLoading)); + sc.progressDialog.setIndeterminate(true); + sc.progressDialog.setCancelable(true); + sc.progressDialog.setOnCancelListener(sc); + sc.progressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND); + + List subscriptionAccountHandles = + PhoneAccountUtils.getSubscriptionPhoneAccounts(context); + Context applicationContext = context.getApplicationContext(); + boolean hasUserSelectedDefault = + subscriptionAccountHandles.contains( + TelecomUtil.getDefaultOutgoingPhoneAccount( + applicationContext, PhoneAccount.SCHEME_TEL)); + + if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) { + Uri uri = TelecomUtil.getAdnUriForPhoneAccount(applicationContext, null); + handleAdnQuery(handler, sc, uri); + } else { + SelectPhoneAccountListener callback = + new HandleAdnEntryAccountSelectedCallback(applicationContext, handler, sc); + + DialogFragment dialogFragment = + SelectPhoneAccountDialogFragment.newInstance( + subscriptionAccountHandles, callback, null); + dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT); + } + + return true; + } catch (NumberFormatException ex) { + // Ignore + } + } + return false; + } + + private static void handleAdnQuery(QueryHandler handler, SimContactQueryCookie cookie, Uri uri) { + if (handler == null || cookie == null || uri == null) { + Log.w(TAG, "queryAdn parameters incorrect"); + return; + } + + // display the progress dialog + cookie.progressDialog.show(); + + // run the query. + handler.startQuery( + ADN_QUERY_TOKEN, + cookie, + uri, + new String[] {ADN_PHONE_NUMBER_COLUMN_NAME}, + null, + null, + null); + + if (sPreviousAdnQueryHandler != null) { + // It is harmless to call cancel() even after the handler's gone. + sPreviousAdnQueryHandler.cancel(); + } + sPreviousAdnQueryHandler = handler; + } + + static boolean handlePinEntry(final Context context, final String input) { + if ((input.startsWith("**04") || input.startsWith("**05")) && input.endsWith("#")) { + List subscriptionAccountHandles = + PhoneAccountUtils.getSubscriptionPhoneAccounts(context); + boolean hasUserSelectedDefault = + subscriptionAccountHandles.contains( + TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL)); + + if (subscriptionAccountHandles.size() <= 1 || hasUserSelectedDefault) { + // Don't bring up the dialog for single-SIM or if the default outgoing account is + // a subscription account. + return TelecomUtil.handleMmi(context, input, null); + } else { + SelectPhoneAccountListener listener = new HandleMmiAccountSelectedCallback(context, input); + + DialogFragment dialogFragment = + SelectPhoneAccountDialogFragment.newInstance( + subscriptionAccountHandles, listener, null); + dialogFragment.show(((Activity) context).getFragmentManager(), TAG_SELECT_ACCT_FRAGMENT); + } + return true; + } + return false; + } + + // TODO: Use TelephonyCapabilities.getDeviceIdLabel() to get the device id label instead of a + // hard-coded string. + static boolean handleDeviceIdDisplay(Context context, String input) { + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + if (telephonyManager != null && input.equals(MMI_IMEI_DISPLAY)) { + int labelResId = + (telephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM) + ? R.string.imei + : R.string.meid; + + List deviceIds = new ArrayList(); + if (TelephonyManagerCompat.getPhoneCount(telephonyManager) > 1 + && CompatUtils.isMethodAvailable( + TelephonyManagerCompat.TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.TYPE)) { + for (int slot = 0; slot < telephonyManager.getPhoneCount(); slot++) { + String deviceId = telephonyManager.getDeviceId(slot); + if (!TextUtils.isEmpty(deviceId)) { + deviceIds.add(deviceId); + } + } + } else { + deviceIds.add(telephonyManager.getDeviceId()); + } + + new AlertDialog.Builder(context) + .setTitle(labelResId) + .setItems(deviceIds.toArray(new String[deviceIds.size()]), null) + .setPositiveButton(android.R.string.ok, null) + .setCancelable(false) + .show(); + return true; + } + return false; + } + + private static boolean handleRegulatoryInfoDisplay(Context context, String input) { + if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) { + Log.d(TAG, "handleRegulatoryInfoDisplay() sending intent to settings app"); + Intent showRegInfoIntent = new Intent(Settings.ACTION_SHOW_REGULATORY_INFO); + try { + context.startActivity(showRegInfoIntent); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "startActivity() failed: " + e); + } + return true; + } + return false; + } + + public static class HandleAdnEntryAccountSelectedCallback extends SelectPhoneAccountListener { + + private final Context mContext; + private final QueryHandler mQueryHandler; + private final SimContactQueryCookie mCookie; + + public HandleAdnEntryAccountSelectedCallback( + Context context, QueryHandler queryHandler, SimContactQueryCookie cookie) { + mContext = context; + mQueryHandler = queryHandler; + mCookie = cookie; + } + + @Override + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) { + Uri uri = TelecomUtil.getAdnUriForPhoneAccount(mContext, selectedAccountHandle); + handleAdnQuery(mQueryHandler, mCookie, uri); + // TODO: Show error dialog if result isn't valid. + } + } + + public static class HandleMmiAccountSelectedCallback extends SelectPhoneAccountListener { + + private final Context mContext; + private final String mInput; + + public HandleMmiAccountSelectedCallback(Context context, String input) { + mContext = context.getApplicationContext(); + mInput = input; + } + + @Override + public void onPhoneAccountSelected( + PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) { + TelecomUtil.handleMmi(mContext, mInput, selectedAccountHandle); + } + } + + /** + * Cookie object that contains everything we need to communicate to the handler's onQuery + * Complete, as well as what we need in order to cancel the query (if requested). + * + *

Note, access to the textField field is going to be synchronized, because the user can + * request a cancel at any time through the UI. + */ + private static class SimContactQueryCookie implements DialogInterface.OnCancelListener { + + public ProgressDialog progressDialog; + public int contactNum; + + // Used to identify the query request. + private int mToken; + private QueryHandler mHandler; + + // The text field we're going to update + private EditText textField; + + public SimContactQueryCookie(int number, QueryHandler handler, int token) { + contactNum = number; + mHandler = handler; + mToken = token; + } + + /** Synchronized getter for the EditText. */ + public synchronized EditText getTextField() { + return textField; + } + + /** Synchronized setter for the EditText. */ + public synchronized void setTextField(EditText text) { + textField = text; + } + + /** + * Cancel the ADN query by stopping the operation and signaling the cookie that a cancel request + * is made. + */ + @Override + public synchronized void onCancel(DialogInterface dialog) { + // close the progress dialog + if (progressDialog != null) { + progressDialog.dismiss(); + } + + // setting the textfield to null ensures that the UI does NOT get + // updated. + textField = null; + + // Cancel the operation if possible. + mHandler.cancelOperation(mToken); + } + } + + /** + * Asynchronous query handler that services requests to look up ADNs + * + *

Queries originate from {@link #handleAdnEntry}. + */ + private static class QueryHandler extends NoNullCursorAsyncQueryHandler { + + private boolean mCanceled; + + public QueryHandler(ContentResolver cr) { + super(cr); + } + + /** Override basic onQueryComplete to fill in the textfield when we're handed the ADN cursor. */ + @Override + protected void onNotNullableQueryComplete(int token, Object cookie, Cursor c) { + try { + sPreviousAdnQueryHandler = null; + if (mCanceled) { + return; + } + + SimContactQueryCookie sc = (SimContactQueryCookie) cookie; + + // close the progress dialog. + sc.progressDialog.dismiss(); + + // get the EditText to update or see if the request was cancelled. + EditText text = sc.getTextField(); + + // if the TextView is valid, and the cursor is valid and positionable on the + // Nth number, then we update the text field and display a toast indicating the + // caller name. + if ((c != null) && (text != null) && (c.moveToPosition(sc.contactNum))) { + String name = c.getString(c.getColumnIndexOrThrow(ADN_NAME_COLUMN_NAME)); + String number = c.getString(c.getColumnIndexOrThrow(ADN_PHONE_NUMBER_COLUMN_NAME)); + + // fill the text in. + text.getText().replace(0, 0, number); + + // display the name as a toast + Context context = sc.progressDialog.getContext(); + CharSequence msg = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + context.getResources(), R.string.menu_callNumber, name); + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); + } + } finally { + MoreCloseables.closeQuietly(c); + } + } + + public void cancel() { + mCanceled = true; + // Ask AsyncQueryHandler to cancel the whole request. This will fail when the query is + // already started. + cancelOperation(ADN_QUERY_TOKEN); + } + } +} diff --git a/java/com/android/dialer/app/alert/AlertManager.java b/java/com/android/dialer/app/alert/AlertManager.java new file mode 100644 index 000000000..ec6180262 --- /dev/null +++ b/java/com/android/dialer/app/alert/AlertManager.java @@ -0,0 +1,30 @@ +/* + * 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.dialer.app.alert; + +import android.view.View; + +/** Manages "alerts" to gain the user's attention. */ +public interface AlertManager { + + /** Inflates layoutId into a view that is ready to be inserted as an alert. */ + View inflate(int layoutId); + + void add(View view); + + void clear(); +} diff --git a/java/com/android/dialer/app/bindings/DialerBindings.java b/java/com/android/dialer/app/bindings/DialerBindings.java new file mode 100644 index 000000000..e1f517860 --- /dev/null +++ b/java/com/android/dialer/app/bindings/DialerBindings.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.dialer.app.bindings; + +import com.android.dialer.common.ConfigProvider; + +/** This interface allows the container application to customize the dialer. */ +public interface DialerBindings { + + ConfigProvider getConfigProvider(); +} diff --git a/java/com/android/dialer/app/bindings/DialerBindingsFactory.java b/java/com/android/dialer/app/bindings/DialerBindingsFactory.java new file mode 100644 index 000000000..9f209f99e --- /dev/null +++ b/java/com/android/dialer/app/bindings/DialerBindingsFactory.java @@ -0,0 +1,26 @@ +/* + * 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.dialer.app.bindings; + +/** + * This interface should be implementated by the Application subclass. It allows the dialer module + * to get references to the DialerBindings. + */ +public interface DialerBindingsFactory { + + DialerBindings newDialerBindings(); +} diff --git a/java/com/android/dialer/app/bindings/DialerBindingsStub.java b/java/com/android/dialer/app/bindings/DialerBindingsStub.java new file mode 100644 index 000000000..f56743fa5 --- /dev/null +++ b/java/com/android/dialer/app/bindings/DialerBindingsStub.java @@ -0,0 +1,48 @@ +/* + * 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.dialer.app.bindings; + +import com.android.dialer.common.ConfigProvider; + +/** Default implementation for dialer bindings. */ +public class DialerBindingsStub implements DialerBindings { + private ConfigProvider configProvider; + + @Override + public ConfigProvider getConfigProvider() { + if (configProvider == null) { + configProvider = + new ConfigProvider() { + @Override + public String getString(String key, String defaultValue) { + return defaultValue; + } + + @Override + public long getLong(String key, long defaultValue) { + return defaultValue; + } + + @Override + public boolean getBoolean(String key, boolean defaultValue) { + return defaultValue; + } + }; + } + return configProvider; + } +} diff --git a/java/com/android/dialer/app/calllog/BlockReportSpamListener.java b/java/com/android/dialer/app/calllog/BlockReportSpamListener.java new file mode 100644 index 000000000..66f40bcd7 --- /dev/null +++ b/java/com/android/dialer/app/calllog/BlockReportSpamListener.java @@ -0,0 +1,212 @@ +/* + * 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.dialer.app.calllog; + +import android.app.FragmentManager; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.support.v7.widget.RecyclerView; +import com.android.dialer.blocking.BlockReportSpamDialogs; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ReportingLocation; +import com.android.dialer.spam.Spam; + +/** Listener to show dialogs for block and report spam actions. */ +public class BlockReportSpamListener implements CallLogListItemViewHolder.OnClickListener { + + private final Context mContext; + private final FragmentManager mFragmentManager; + private final RecyclerView.Adapter mAdapter; + private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + + public BlockReportSpamListener( + Context context, + FragmentManager fragmentManager, + RecyclerView.Adapter adapter, + FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { + mContext = context; + mFragmentManager = fragmentManager; + mAdapter = adapter; + mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; + } + + @Override + public void onBlockReportSpam( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType) { + BlockReportSpamDialogs.BlockReportSpamDialogFragment.newInstance( + displayNumber, + Spam.get(mContext).isDialogReportSpamCheckedByDefault(), + new BlockReportSpamDialogs.OnSpamDialogClickListener() { + @Override + public void onClick(boolean isSpamChecked) { + LogUtil.i("BlockReportSpamListener.onBlockReportSpam", "onClick"); + if (isSpamChecked && Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression( + DialerImpression.Type + .REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG); + Spam.get(mContext) + .reportSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mFilteredNumberAsyncQueryHandler.blockNumber( + new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() { + @Override + public void onBlockComplete(Uri uri) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER); + mAdapter.notifyDataSetChanged(); + } + }, + number, + countryIso); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.BLOCK_REPORT_SPAM_DIALOG_TAG); + } + + @Override + public void onBlock( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType) { + BlockReportSpamDialogs.BlockDialogFragment.newInstance( + displayNumber, + Spam.get(mContext).isSpamEnabled(), + new BlockReportSpamDialogs.OnConfirmListener() { + @Override + public void onClick() { + LogUtil.i("BlockReportSpamListener.onBlock", "onClick"); + if (Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression( + DialerImpression.Type + .DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER); + Spam.get(mContext) + .reportSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mFilteredNumberAsyncQueryHandler.blockNumber( + new FilteredNumberAsyncQueryHandler.OnBlockNumberListener() { + @Override + public void onBlockComplete(Uri uri) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.USER_ACTION_BLOCKED_NUMBER); + mAdapter.notifyDataSetChanged(); + } + }, + number, + countryIso); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.BLOCK_DIALOG_TAG); + } + + @Override + public void onUnblock( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType, + final boolean isSpam, + final Integer blockId) { + BlockReportSpamDialogs.UnblockDialogFragment.newInstance( + displayNumber, + isSpam, + new BlockReportSpamDialogs.OnConfirmListener() { + @Override + public void onClick() { + LogUtil.i("BlockReportSpamListener.onUnblock", "onClick"); + if (isSpam && Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER); + Spam.get(mContext) + .reportNotSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mFilteredNumberAsyncQueryHandler.unblock( + new FilteredNumberAsyncQueryHandler.OnUnblockNumberListener() { + @Override + public void onUnblockComplete(int rows, ContentValues values) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.USER_ACTION_UNBLOCKED_NUMBER); + mAdapter.notifyDataSetChanged(); + } + }, + blockId); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.UNBLOCK_DIALOG_TAG); + } + + @Override + public void onReportNotSpam( + String displayNumber, + final String number, + final String countryIso, + final int callType, + final int contactSourceType) { + BlockReportSpamDialogs.ReportNotSpamDialogFragment.newInstance( + displayNumber, + new BlockReportSpamDialogs.OnConfirmListener() { + @Override + public void onClick() { + LogUtil.i("BlockReportSpamListener.onReportNotSpam", "onClick"); + if (Spam.get(mContext).isSpamEnabled()) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM); + Spam.get(mContext) + .reportNotSpamFromCallHistory( + number, + countryIso, + callType, + ReportingLocation.Type.CALL_LOG_HISTORY, + contactSourceType); + } + mAdapter.notifyDataSetChanged(); + } + }, + null) + .show(mFragmentManager, BlockReportSpamDialogs.NOT_SPAM_DIALOG_TAG); + } +} diff --git a/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java new file mode 100644 index 000000000..ab6ef7362 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java @@ -0,0 +1,214 @@ +/* + * 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.dialer.app.calllog; + +import android.content.Context; +import android.icu.lang.UCharacter; +import android.icu.text.BreakIterator; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.CallLog.Calls; +import android.text.format.DateUtils; +import android.text.format.Formatter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import java.util.ArrayList; +import java.util.Locale; + +/** Adapter for a ListView containing history items from the details of a call. */ +public class CallDetailHistoryAdapter extends BaseAdapter { + + /** Each history item shows the detail of a call. */ + private static final int VIEW_TYPE_HISTORY_ITEM = 1; + + private final Context mContext; + private final LayoutInflater mLayoutInflater; + private final CallTypeHelper mCallTypeHelper; + private final PhoneCallDetails[] mPhoneCallDetails; + + /** List of items to be concatenated together for duration strings. */ + private ArrayList mDurationItems = new ArrayList<>(); + + public CallDetailHistoryAdapter( + Context context, + LayoutInflater layoutInflater, + CallTypeHelper callTypeHelper, + PhoneCallDetails[] phoneCallDetails) { + mContext = context; + mLayoutInflater = layoutInflater; + mCallTypeHelper = callTypeHelper; + mPhoneCallDetails = phoneCallDetails; + } + + @Override + public boolean isEnabled(int position) { + // None of history will be clickable. + return false; + } + + @Override + public int getCount() { + return mPhoneCallDetails.length; + } + + @Override + public Object getItem(int position) { + return mPhoneCallDetails[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public int getItemViewType(int position) { + return VIEW_TYPE_HISTORY_ITEM; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // Make sure we have a valid convertView to start with + final View result = + convertView == null + ? mLayoutInflater.inflate(R.layout.call_detail_history_item, parent, false) + : convertView; + + PhoneCallDetails details = mPhoneCallDetails[position]; + CallTypeIconsView callTypeIconView = + (CallTypeIconsView) result.findViewById(R.id.call_type_icon); + TextView callTypeTextView = (TextView) result.findViewById(R.id.call_type_text); + TextView dateView = (TextView) result.findViewById(R.id.date); + TextView durationView = (TextView) result.findViewById(R.id.duration); + + int callType = details.callTypes[0]; + boolean isVideoCall = + (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO + && CallUtil.isVideoEnabled(mContext); + boolean isPulledCall = + (details.features & Calls.FEATURES_PULLED_EXTERNALLY) == Calls.FEATURES_PULLED_EXTERNALLY; + + callTypeIconView.clear(); + callTypeIconView.add(callType); + callTypeIconView.setShowVideo(isVideoCall); + callTypeTextView.setText(mCallTypeHelper.getCallTypeText(callType, isVideoCall, isPulledCall)); + // Set the date. + dateView.setText(formatDate(details.date)); + // Set the duration + if (Calls.VOICEMAIL_TYPE == callType || CallTypeHelper.isMissedCallType(callType)) { + durationView.setVisibility(View.GONE); + } else { + durationView.setVisibility(View.VISIBLE); + durationView.setText(formatDurationAndDataUsage(details.duration, details.dataUsage)); + } + + return result; + } + + /** + * Formats the provided date into a value suitable for display in the current locale. + * + *

For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016 + * may 25,20:02". + * + *

For pre-N devices, the returned value may not start with a capital if the local convention + * is to not capitalize day names. On N+ devices, the returned value is always capitalized. + */ + private CharSequence formatDate(long callDateMillis) { + CharSequence dateValue = + DateUtils.formatDateRange( + mContext, + callDateMillis /* startDate */, + callDateMillis /* endDate */, + DateUtils.FORMAT_SHOW_TIME + | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_SHOW_WEEKDAY + | DateUtils.FORMAT_SHOW_YEAR); + + // We want the beginning of the date string to be capitalized, even if the word at the beginning + // of the string is not usually capitalized. For example, "Wednesdsay" in Uzbek is "chorshanba” + // (not capitalized). To handle this issue we apply title casing to the start of the sentence so + // that "chorshanba, 2016 may 25,20:02" becomes "Chorshanba, 2016 may 25,20:02". + // + // The ICU library was not available in Android until N, so we can only do this in N+ devices. + // Pre-N devices will still see incorrect capitalization in some languages. + if (VERSION.SDK_INT < VERSION_CODES.N) { + return dateValue; + } + + // Using the ICU library is safer than just applying toUpperCase() on the first letter of the + // word because in some languages, there can be multiple starting characters which should be + // upper-cased together. For example in Dutch "ij" is a digraph in which both letters should be + // capitalized together. + + // TITLECASE_NO_LOWERCASE is necessary so that things that are already capitalized like the + // month ("May") are not lower-cased as part of the conversion. + return UCharacter.toTitleCase( + Locale.getDefault(), + dateValue.toString(), + BreakIterator.getSentenceInstance(), + UCharacter.TITLECASE_NO_LOWERCASE); + } + + private CharSequence formatDuration(long elapsedSeconds) { + long minutes = 0; + long seconds = 0; + + if (elapsedSeconds >= 60) { + minutes = elapsedSeconds / 60; + elapsedSeconds -= minutes * 60; + seconds = elapsedSeconds; + return mContext.getString(R.string.callDetailsDurationFormat, minutes, seconds); + } else { + seconds = elapsedSeconds; + return mContext.getString(R.string.callDetailsShortDurationFormat, seconds); + } + } + + /** + * Formats a string containing the call duration and the data usage (if specified). + * + * @param elapsedSeconds Total elapsed seconds. + * @param dataUsage Data usage in bytes, or null if not specified. + * @return String containing call duration and data usage. + */ + private CharSequence formatDurationAndDataUsage(long elapsedSeconds, Long dataUsage) { + CharSequence duration = formatDuration(elapsedSeconds); + + if (dataUsage != null) { + mDurationItems.clear(); + mDurationItems.add(duration); + mDurationItems.add(Formatter.formatShortFileSize(mContext, dataUsage)); + + return DialerUtils.join(mDurationItems); + } else { + return duration; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAdapter.java b/java/com/android/dialer/app/calllog/CallLogAdapter.java new file mode 100644 index 000000000..ea09a8c0a --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAdapter.java @@ -0,0 +1,915 @@ +/* + * 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.dialer.app.calllog; + +import android.app.Activity; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Trace; +import android.provider.CallLog; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.dialer.app.Bindings; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.common.LogUtil; +import com.android.dialer.enrichedcall.EnrichedCallCapabilities; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.enrichedcall.EnrichedCallManager.CapabilitiesListener; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.phonenumbercache.CallLogQuery; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.spam.Spam; +import com.android.dialer.util.PermissionsUtil; +import java.util.Map; +import java.util.Set; + +/** Adapter class to fill in data for the Call Log. */ +public class CallLogAdapter extends GroupingListAdapter + implements GroupCreator, OnVoicemailDeletedListener, CapabilitiesListener { + + // Types of activities the call log adapter is used for + public static final int ACTIVITY_TYPE_CALL_LOG = 1; + public static final int ACTIVITY_TYPE_DIALTACTS = 2; + private static final int NO_EXPANDED_LIST_ITEM = -1; + public static final int ALERT_POSITION = 0; + private static final int VIEW_TYPE_ALERT = 1; + private static final int VIEW_TYPE_CALLLOG = 2; + + private static final String KEY_EXPANDED_POSITION = "expanded_position"; + private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; + + public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data"; + + protected final Activity mActivity; + protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + /** Cache for repeated requests to Telecom/Telephony. */ + protected final CallLogCache mCallLogCache; + + private final CallFetcher mCallFetcher; + private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + private final int mActivityType; + + /** Instance of helper class for managing views. */ + private final CallLogListItemHelper mCallLogListItemHelper; + /** Helper to group call log entries. */ + private final CallLogGroupBuilder mCallLogGroupBuilder; + + private final AsyncTaskExecutor mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); + private ContactInfoCache mContactInfoCache; + // Tracks the position of the currently expanded list item. + private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + // Tracks the rowId of the currently expanded list item, so the position can be updated if there + // are any changes to the call log entries, such as additions or removals. + private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; + + private final CallLogAlertManager mCallLogAlertManager; + /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */ + private final View.OnClickListener mExpandCollapseListener = + new View.OnClickListener() { + @Override + public void onClick(View v) { + CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); + if (viewHolder == null) { + return; + } + + if (mVoicemailPlaybackPresenter != null) { + // Always reset the voicemail playback state on expand or collapse. + mVoicemailPlaybackPresenter.resetAll(); + } + + if (viewHolder.rowId == mCurrentlyExpandedRowId) { + // Hide actions, if the clicked item is the expanded item. + viewHolder.showActions(false); + + mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; + } else { + if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { + CallLogAsyncTaskUtil.markCallAsRead(mActivity, viewHolder.callIds); + if (mActivityType == ACTIVITY_TYPE_DIALTACTS) { + ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); + } + } + expandViewHolderActions(viewHolder); + } + } + }; + + /** + * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead + * if removing an item, it will be shown as an invisible view. This simplifies the calculation of + * item position. + */ + @NonNull private Set mHiddenRowIds = new ArraySet<>(); + /** + * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo + * timeout, all of the pending URIs will be deleted. + * + *

TODO: move this and OnVoicemailDeletedListener to somewhere like {@link + * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with + * hidden item or what to hide. + */ + @NonNull private final Set mHiddenItemUris = new ArraySet<>(); + + private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener; + /** + * Map, keyed by call Id, used to track the day group for a call. As call log entries are put into + * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are + * also assigned a secondary "day group". This map tracks the day group assigned to all calls in + * the call log. This information is used to trigger the display of a day group header above the + * call log entry at the start of a day group. Note: Multiple calls are grouped into a single + * primary "call group" in the call log, and the cursor used to bind rows includes all of these + * calls. When determining if a day group change has occurred it is necessary to look at the last + * entry in the call log to determine its day group. This map provides a means of determining the + * previous day group without having to reverse the cursor to the start of the previous day call + * log entry. + */ + private Map mDayGroups = new ArrayMap<>(); + + private boolean mLoading = true; + private ContactsPreferences mContactsPreferences; + + private boolean mIsSpamEnabled; + + @NonNull private final EnrichedCallManager mEnrichedCallManager; + + public CallLogAdapter( + Activity activity, + ViewGroup alertContainer, + CallFetcher callFetcher, + CallLogCache callLogCache, + ContactInfoCache contactInfoCache, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + int activityType) { + super(); + + mActivity = activity; + mCallFetcher = callFetcher; + mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; + if (mVoicemailPlaybackPresenter != null) { + mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); + } + + mActivityType = activityType; + + mContactInfoCache = contactInfoCache; + + if (!PermissionsUtil.hasContactsPermissions(activity)) { + mContactInfoCache.disableRequestProcessing(); + } + + Resources resources = mActivity.getResources(); + + mCallLogCache = callLogCache; + + PhoneCallDetailsHelper phoneCallDetailsHelper = + new PhoneCallDetailsHelper(mActivity, resources, mCallLogCache); + mCallLogListItemHelper = + new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache); + mCallLogGroupBuilder = new CallLogGroupBuilder(this); + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(mActivity); + + mContactsPreferences = new ContactsPreferences(mActivity); + + mBlockReportSpamListener = + new BlockReportSpamListener( + mActivity, + ((Activity) mActivity).getFragmentManager(), + this, + mFilteredNumberAsyncQueryHandler); + setHasStableIds(true); + + mCallLogAlertManager = + new CallLogAlertManager(this, LayoutInflater.from(mActivity), alertContainer); + mEnrichedCallManager = EnrichedCallManager.Accessor.getInstance(activity.getApplication()); + } + + private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { + if (!TextUtils.isEmpty(viewHolder.voicemailUri)) { + Logger.get(mActivity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY); + } + + int lastExpandedPosition = mCurrentlyExpandedPosition; + // Show the actions for the clicked list item. + viewHolder.showActions(true); + mCurrentlyExpandedPosition = viewHolder.getAdapterPosition(); + mCurrentlyExpandedRowId = viewHolder.rowId; + + // If another item is expanded, notify it that it has changed. Its actions will be + // hidden when it is re-binded because we change mCurrentlyExpandedRowId above. + if (lastExpandedPosition != RecyclerView.NO_POSITION) { + notifyItemChanged(lastExpandedPosition); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); + outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mCurrentlyExpandedPosition = + savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); + mCurrentlyExpandedRowId = + savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); + } + } + + /** Requery on background thread when {@link Cursor} changes. */ + @Override + protected void onContentChanged() { + mCallFetcher.fetchCalls(); + } + + public void setLoading(boolean loading) { + mLoading = loading; + } + + public boolean isEmpty() { + if (mLoading) { + // We don't want the empty state to show when loading. + return false; + } else { + return getItemCount() == 0; + } + } + + public void clearFilteredNumbersCache() { + mFilteredNumberAsyncQueryHandler.clearCache(); + } + + public void onResume() { + if (PermissionsUtil.hasPermission(mActivity, android.Manifest.permission.READ_CONTACTS)) { + mContactInfoCache.start(); + } + mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); + mIsSpamEnabled = Spam.get(mActivity).isSpamEnabled(); + mEnrichedCallManager.registerCapabilitiesListener(this); + notifyDataSetChanged(); + } + + public void onPause() { + pauseCache(); + for (Uri uri : mHiddenItemUris) { + CallLogAsyncTaskUtil.deleteVoicemail(mActivity, uri, null); + } + mEnrichedCallManager.unregisterCapabilitiesListener(this); + } + + public void onStop() { + mEnrichedCallManager.clearCachedData(); + } + + public CallLogAlertManager getAlertManager() { + return mCallLogAlertManager; + } + + @VisibleForTesting + /* package */ void pauseCache() { + mContactInfoCache.stop(); + mCallLogCache.reset(); + } + + @Override + protected void addGroups(Cursor cursor) { + mCallLogGroupBuilder.addGroups(cursor); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == VIEW_TYPE_ALERT) { + return mCallLogAlertManager.createViewHolder(parent); + } + return createCallLogEntryViewHolder(parent); + } + + /** + * Creates a new call log entry {@link ViewHolder}. + * + * @param parent the parent view. + * @return The {@link ViewHolder}. + */ + private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(mActivity); + View view = inflater.inflate(R.layout.call_log_list_item, parent, false); + CallLogListItemViewHolder viewHolder = + CallLogListItemViewHolder.create( + view, + mActivity, + mBlockReportSpamListener, + mExpandCollapseListener, + mCallLogCache, + mCallLogListItemHelper, + mVoicemailPlaybackPresenter); + + viewHolder.callLogEntryView.setTag(viewHolder); + + viewHolder.primaryActionView.setTag(viewHolder); + + return viewHolder; + } + + /** + * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times + * when Dialer starts up for a single call log entry and should not. It invokes cross-process + * methods and the repeat execution can get costly. + * + * @param viewHolder The view corresponding to this entry. + * @param position The position of the entry. + */ + @Override + public void onBindViewHolder(ViewHolder viewHolder, int position) { + Trace.beginSection("onBindViewHolder: " + position); + switch (getItemViewType(position)) { + case VIEW_TYPE_ALERT: + //Do nothing + break; + default: + bindCallLogListViewHolder(viewHolder, position); + break; + } + Trace.endSection(); + } + + @Override + public void onViewRecycled(ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { + CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; + if (views.asyncTask != null) { + views.asyncTask.cancel(true); + } + } + } + + @Override + public void onViewAttachedToWindow(ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { + ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true; + } + } + + @Override + public void onViewDetachedFromWindow(ViewHolder viewHolder) { + if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { + ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false; + } + } + + /** + * Binds the view holder for the call log list item view. + * + * @param viewHolder The call log list item view holder. + * @param position The position of the list item. + */ + private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) { + Cursor c = (Cursor) getItem(position); + if (c == null) { + return; + } + CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; + views.isLoaded = false; + PhoneCallDetails details = createPhoneCallDetails(c, getGroupSize(position), views); + if (mHiddenRowIds.contains(c.getLong(CallLogQuery.ID))) { + views.callLogEntryView.setVisibility(View.GONE); + views.dayGroupHeader.setVisibility(View.GONE); + return; + } else { + views.callLogEntryView.setVisibility(View.VISIBLE); + // dayGroupHeader will be restored after loadAndRender() if it is needed. + } + if (mCurrentlyExpandedRowId == views.rowId) { + views.inflateActionViewStub(); + } + loadAndRender(views, views.rowId, details); + } + + private void loadAndRender( + final CallLogListItemViewHolder views, final long rowId, final PhoneCallDetails details) { + // Reset block and spam information since this view could be reused which may contain + // outdated data. + views.isSpam = false; + views.blockId = null; + views.isSpamFeatureEnabled = false; + views.isCallComposerCapable = + isCallComposerCapable(PhoneNumberUtils.formatNumberToE164(views.number, views.countryIso)); + final AsyncTask loadDataTask = + new AsyncTask() { + @Override + protected Boolean doInBackground(Void... params) { + views.blockId = + mFilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly( + views.number, views.countryIso); + details.isBlocked = views.blockId != null; + if (isCancelled()) { + return false; + } + if (mIsSpamEnabled) { + views.isSpamFeatureEnabled = true; + // Only display the call as a spam call if there are incoming calls in the list. + // Call log cards with only outgoing calls should never be displayed as spam. + views.isSpam = + details.hasIncomingCalls() + && Spam.get(mActivity) + .checkSpamStatusSynchronous(views.number, views.countryIso); + details.isSpam = views.isSpam; + if (isCancelled()) { + return false; + } + return loadData(views, rowId, details); + } else { + return loadData(views, rowId, details); + } + } + + @Override + protected void onPostExecute(Boolean success) { + views.isLoaded = true; + if (success) { + int currentGroup = getDayGroupForCall(views.rowId); + if (currentGroup != details.previousGroup) { + views.dayGroupHeaderVisibility = View.VISIBLE; + views.dayGroupHeaderText = getGroupDescription(currentGroup); + } else { + views.dayGroupHeaderVisibility = View.GONE; + } + render(views, details, rowId); + } + } + }; + + views.asyncTask = loadDataTask; + mAsyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask); + } + + @MainThread + private boolean isCallComposerCapable(@Nullable String e164Number) { + if (e164Number == null) { + return false; + } + + EnrichedCallCapabilities capabilities = mEnrichedCallManager.getCapabilities(e164Number); + if (capabilities == null) { + mEnrichedCallManager.requestCapabilities(e164Number); + return false; + } + return capabilities.supportsCallComposer(); + } + + /** + * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main + * thread since cursor is not thread safe. + */ + @MainThread + private PhoneCallDetails createPhoneCallDetails( + Cursor cursor, int count, final CallLogListItemViewHolder views) { + Assert.isMainThread(); + final String number = cursor.getString(CallLogQuery.NUMBER); + final String postDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; + final String viaNumber = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; + final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); + final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor); + final PhoneCallDetails details = + new PhoneCallDetails(number, numberPresentation, postDialDigits); + details.viaNumber = viaNumber; + details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); + details.date = cursor.getLong(CallLogQuery.DATE); + details.duration = cursor.getLong(CallLogQuery.DURATION); + details.features = getCallFeatures(cursor, count); + details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION); + details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION); + details.callTypes = getCallTypes(cursor, count); + + details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); + details.cachedContactInfo = cachedContactInfo; + + if (!cursor.isNull(CallLogQuery.DATA_USAGE)) { + details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE); + } + + views.rowId = cursor.getLong(CallLogQuery.ID); + // Stash away the Ids of the calls so that we can support deleting a row in the call log. + views.callIds = getCallIds(cursor, count); + details.previousGroup = getPreviousDayGroup(cursor); + + // Store values used when the actions ViewStub is inflated on expansion. + views.number = number; + views.countryIso = details.countryIso; + views.postDialDigits = details.postDialDigits; + views.numberPresentation = numberPresentation; + + if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE + || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) { + details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1; + } + views.callType = cursor.getInt(CallLogQuery.CALL_TYPE); + views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI); + + return details; + } + + /** + * Load data for call log. Any expensive operation should be put here to avoid blocking main + * thread. Do NOT put any cursor operation here since it's not thread safe. + */ + @WorkerThread + private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) { + Assert.isWorkerThread(); + if (rowId != views.rowId) { + LogUtil.i( + "CallLogAdapter.loadData", + "rowId of viewHolder changed after load task is issued, aborting load"); + return false; + } + + final PhoneAccountHandle accountHandle = + PhoneAccountUtils.getAccount(details.accountComponentName, details.accountId); + + final boolean isVoicemailNumber = + mCallLogCache.isVoicemailNumber(accountHandle, details.number); + + // Note: Binding of the action buttons is done as required in configureActionViews when the + // user expands the actions ViewStub. + + ContactInfo info = ContactInfo.EMPTY; + if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation) + && !isVoicemailNumber) { + // Lookup contacts with this number + // Only do remote lookup in first 5 rows. + info = + mContactInfoCache.getValue( + details.number + details.postDialDigits, + details.countryIso, + details.cachedContactInfo, + rowId + < Bindings.get(mActivity) + .getConfigProvider() + .getLong("number_of_call_to_do_remote_lookup", 5L)); + } + CharSequence formattedNumber = + info.formattedNumber == null + ? null + : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber); + details.updateDisplayNumber(mActivity, formattedNumber, isVoicemailNumber); + + views.displayNumber = details.displayNumber; + views.accountHandle = accountHandle; + details.accountHandle = accountHandle; + + if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) { + details.contactUri = info.lookupUri; + details.namePrimary = info.name; + details.nameAlternative = info.nameAlternative; + details.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); + details.numberType = info.type; + details.numberLabel = info.label; + details.photoUri = info.photoUri; + details.sourceType = info.sourceType; + details.objectId = info.objectId; + details.contactUserType = info.userType; + } + + views.info = info; + views.numberType = + (String) + Phone.getTypeLabel(mActivity.getResources(), details.numberType, details.numberLabel); + + mCallLogListItemHelper.updatePhoneCallDetails(details); + return true; + } + + /** + * Render item view given position. This is running on UI thread so DO NOT put any expensive + * operation into it. + */ + @MainThread + private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) { + Assert.isMainThread(); + if (rowId != views.rowId) { + LogUtil.i( + "CallLogAdapter.render", + "rowId of viewHolder changed after load task is issued, aborting render"); + return; + } + + // Default case: an item in the call log. + views.primaryActionView.setVisibility(View.VISIBLE); + views.workIconView.setVisibility( + details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE); + + mCallLogListItemHelper.setPhoneCallDetails(views, details); + if (mCurrentlyExpandedRowId == views.rowId) { + // In case ViewHolders were added/removed, update the expanded position if the rowIds + // match so that we can restore the correct expanded state on rebind. + mCurrentlyExpandedPosition = views.getAdapterPosition(); + views.showActions(true); + } else { + views.showActions(false); + } + views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility); + views.dayGroupHeader.setText(views.dayGroupHeaderText); + } + + @Override + public int getItemCount() { + return super.getItemCount() + (mCallLogAlertManager.isEmpty() ? 0 : 1); + } + + @Override + public int getItemViewType(int position) { + if (position == ALERT_POSITION && !mCallLogAlertManager.isEmpty()) { + return VIEW_TYPE_ALERT; + } + return VIEW_TYPE_CALLLOG; + } + + /** + * Retrieves an item at the specified position, taking into account the presence of a promo card. + * + * @param position The position to retrieve. + * @return The item at that position. + */ + @Override + public Object getItem(int position) { + return super.getItem(position - (mCallLogAlertManager.isEmpty() ? 0 : 1)); + } + + @Override + public long getItemId(int position) { + Cursor cursor = (Cursor) getItem(position); + if (cursor != null) { + return cursor.getLong(CallLogQuery.ID); + } else { + return 0; + } + } + + @Override + public int getGroupSize(int position) { + return super.getGroupSize(position - (mCallLogAlertManager.isEmpty() ? 0 : 1)); + } + + protected boolean isCallLogActivity() { + return mActivityType == ACTIVITY_TYPE_CALL_LOG; + } + + /** + * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user + * clicks the delete button, the deleted item is temporarily hidden from the list. If a user + * clicks delete on a second item before the first item's undo option has expired, the first item + * is immediately deleted so that only one item can be "undoed" at a time. + */ + @Override + public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) { + mHiddenRowIds.add(viewHolder.rowId); + // Save the new hidden item uri in case the activity is suspend before the undo has timed out. + mHiddenItemUris.add(uri); + + collapseExpandedCard(); + notifyItemChanged(viewHolder.getAdapterPosition()); + // The next item might have to update its day group label + notifyItemChanged(viewHolder.getAdapterPosition() + 1); + } + + private void collapseExpandedCard() { + mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; + mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + } + + /** When the list is changing all stored position is no longer valid. */ + public void invalidatePositions() { + mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; + } + + /** When the user clicks "undo", the hidden item is unhidden. */ + @Override + public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) { + mHiddenItemUris.remove(uri); + mHiddenRowIds.remove(rowId); + notifyItemChanged(adapterPosition); + // The next item might have to update its day group label + notifyItemChanged(adapterPosition + 1); + } + + /** This callback signifies that a database deletion has completed. */ + @Override + public void onVoicemailDeletedInDatabase(long rowId, Uri uri) { + mHiddenItemUris.remove(uri); + } + + /** + * Retrieves the day group of the previous call in the call log. Used to determine if the day + * group has changed and to trigger display of the day group text. + * + * @param cursor The call log cursor. + * @return The previous day group, or DAY_GROUP_NONE if this is the first call. + */ + private int getPreviousDayGroup(Cursor cursor) { + // We want to restore the position in the cursor at the end. + int startingPosition = cursor.getPosition(); + moveToPreviousNonHiddenRow(cursor); + if (cursor.isBeforeFirst()) { + cursor.moveToPosition(startingPosition); + return CallLogGroupBuilder.DAY_GROUP_NONE; + } + int result = getDayGroupForCall(cursor.getLong(CallLogQuery.ID)); + cursor.moveToPosition(startingPosition); + return result; + } + + private void moveToPreviousNonHiddenRow(Cursor cursor) { + while (cursor.moveToPrevious() && mHiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {} + } + + /** + * Given a call Id, look up the day group that the call belongs to. The day group data is + * populated in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}. + * + * @param callId The call to retrieve the day group for. + * @return The day group for the call. + */ + @MainThread + private int getDayGroupForCall(long callId) { + Integer result = mDayGroups.get(callId); + if (result != null) { + return result; + } + return CallLogGroupBuilder.DAY_GROUP_NONE; + } + + /** + * Returns the call types for the given number of items in the cursor. + * + *

It uses the next {@code count} rows in the cursor to extract the types. + * + *

It position in the cursor is unchanged by this function. + */ + private static int[] getCallTypes(Cursor cursor, int count) { + int position = cursor.getPosition(); + int[] callTypes = new int[count]; + for (int index = 0; index < count; ++index) { + callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); + cursor.moveToNext(); + } + cursor.moveToPosition(position); + return callTypes; + } + + /** + * Determine the features which were enabled for any of the calls that make up a call log entry. + * + * @param cursor The cursor. + * @param count The number of calls for the current call log entry. + * @return The features. + */ + private int getCallFeatures(Cursor cursor, int count) { + int features = 0; + int position = cursor.getPosition(); + for (int index = 0; index < count; ++index) { + features |= cursor.getInt(CallLogQuery.FEATURES); + cursor.moveToNext(); + } + cursor.moveToPosition(position); + return features; + } + + /** + * Sets whether processing of requests for contact details should be enabled. + * + *

This method should be called in tests to disable such processing of requests when not + * needed. + */ + @VisibleForTesting + void disableRequestProcessingForTest() { + // TODO: Remove this and test the cache directly. + mContactInfoCache.disableRequestProcessing(); + } + + @VisibleForTesting + void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { + // TODO: Remove this and test the cache directly. + mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); + } + + /** + * Stores the day group associated with a call in the call log. + * + * @param rowId The row Id of the current call. + * @param dayGroup The day group the call belongs in. + */ + @Override + @MainThread + public void setDayGroup(long rowId, int dayGroup) { + if (!mDayGroups.containsKey(rowId)) { + mDayGroups.put(rowId, dayGroup); + } + } + + /** Clears the day group associations on re-bind of the call log. */ + @Override + @MainThread + public void clearDayGroups() { + mDayGroups.clear(); + } + + /** + * Retrieves the call Ids represented by the current call log row. + * + * @param cursor Call log cursor to retrieve call Ids from. + * @param groupSize Number of calls associated with the current call log row. + * @return Array of call Ids. + */ + private long[] getCallIds(final Cursor cursor, final int groupSize) { + // We want to restore the position in the cursor at the end. + int startingPosition = cursor.getPosition(); + long[] ids = new long[groupSize]; + // Copy the ids of the rows in the group. + for (int index = 0; index < groupSize; ++index) { + ids[index] = cursor.getLong(CallLogQuery.ID); + cursor.moveToNext(); + } + cursor.moveToPosition(startingPosition); + return ids; + } + + /** + * Determines the description for a day group. + * + * @param group The day group to retrieve the description for. + * @return The day group description. + */ + private CharSequence getGroupDescription(int group) { + if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { + return mActivity.getResources().getString(R.string.call_log_header_today); + } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { + return mActivity.getResources().getString(R.string.call_log_header_yesterday); + } else { + return mActivity.getResources().getString(R.string.call_log_header_other); + } + } + + @Override + public void onCapabilitiesUpdated() { + notifyDataSetChanged(); + } + + /** Interface used to initiate a refresh of the content. */ + public interface CallFetcher { + + void fetchCalls(); + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAlertManager.java b/java/com/android/dialer/app/calllog/CallLogAlertManager.java new file mode 100644 index 000000000..40b30f001 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAlertManager.java @@ -0,0 +1,90 @@ +/* + * 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.dialer.app.calllog; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.app.alert.AlertManager; +import com.android.dialer.common.Assert; + +/** Manages "alerts" to be shown at the top of an call log to gain the user's attention. */ +public class CallLogAlertManager implements AlertManager { + + private final CallLogAdapter adapter; + private final View view; + private final LayoutInflater inflater; + private final ViewGroup parent; + private final ViewGroup container; + + public CallLogAlertManager(CallLogAdapter adapter, LayoutInflater inflater, ViewGroup parent) { + this.adapter = adapter; + this.inflater = inflater; + this.parent = parent; + view = inflater.inflate(R.layout.call_log_alert_item, parent, false); + container = (ViewGroup) view.findViewById(R.id.container); + } + + @Override + public View inflate(int layoutId) { + return inflater.inflate(layoutId, container, false); + } + + public RecyclerView.ViewHolder createViewHolder(ViewGroup parent) { + Assert.checkArgument( + parent == this.parent, + "createViewHolder should be called with the same parent in constructor"); + return new AlertViewHolder(view); + } + + public boolean isEmpty() { + return container.getChildCount() == 0; + } + + public boolean contains(View view) { + return container.indexOfChild(view) != -1; + } + + @Override + public void clear() { + container.removeAllViews(); + adapter.notifyItemRemoved(CallLogAdapter.ALERT_POSITION); + } + + @Override + public void add(View view) { + if (contains(view)) { + return; + } + container.addView(view); + if (container.getChildCount() == 1) { + // Was empty before + adapter.notifyItemInserted(CallLogAdapter.ALERT_POSITION); + } + } + + /** + * Does nothing. The view this ViewHolder show is directly managed by {@link CallLogAlertManager} + */ + private static class AlertViewHolder extends RecyclerView.ViewHolder { + private AlertViewHolder(View view) { + super(view); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAsync.java b/java/com/android/dialer/app/calllog/CallLogAsync.java new file mode 100644 index 000000000..f62deca89 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAsync.java @@ -0,0 +1,96 @@ +/* + * 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.dialer.app.calllog; + +import android.content.Context; +import android.os.AsyncTask; +import android.provider.CallLog.Calls; +import com.android.dialer.common.Assert; + +/** + * Class to access the call log asynchronously to avoid carrying out database operations on the UI + * thread, using an {@link AsyncTask}. + * + *

 Typical usage: ==============
+ *
+ * // From an activity... String mLastNumber = "";
+ *
+ * CallLogAsync log = new CallLogAsync();
+ *
+ * CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = new CallLogAsync.GetLastOutgoingCallArgs(
+ * this, new CallLogAsync.OnLastOutgoingCallComplete() { public void lastOutgoingCall(String number)
+ * { mLastNumber = number; } }); log.getLastOutgoingCall(lastCallArgs); 
+ */ +public class CallLogAsync { + + /** CallLog.getLastOutgoingCall(...) */ + public AsyncTask getLastOutgoingCall(GetLastOutgoingCallArgs args) { + Assert.isMainThread(); + return new GetLastOutgoingCallTask(args.callback).execute(args); + } + + /** Interface to retrieve the last dialed number asynchronously. */ + public interface OnLastOutgoingCallComplete { + + /** @param number The last dialed number or an empty string if none exists yet. */ + void lastOutgoingCall(String number); + } + + /** Parameter object to hold the args to get the last outgoing call from the call log DB. */ + public static class GetLastOutgoingCallArgs { + + public final Context context; + public final OnLastOutgoingCallComplete callback; + + public GetLastOutgoingCallArgs(Context context, OnLastOutgoingCallComplete callback) { + this.context = context; + this.callback = callback; + } + } + + /** AsyncTask to get the last outgoing call from the DB. */ + private class GetLastOutgoingCallTask extends AsyncTask { + + private final OnLastOutgoingCallComplete mCallback; + + public GetLastOutgoingCallTask(OnLastOutgoingCallComplete callback) { + mCallback = callback; + } + + // Happens on a background thread. We cannot run the callback + // here because only the UI thread can modify the view + // hierarchy (e.g enable/disable the dial button). The + // callback is ran rom the post execute method. + @Override + protected String doInBackground(GetLastOutgoingCallArgs... list) { + String number = ""; + for (GetLastOutgoingCallArgs args : list) { + // May block. Select only the last one. + number = Calls.getLastOutgoingCall(args.context); + } + return number; // passed to the onPostExecute method. + } + + // Happens on the UI thread, it is safe to run the callback + // that may do some work on the views. + @Override + protected void onPostExecute(String number) { + Assert.isMainThread(); + mCallback.lastOutgoingCall(number); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java new file mode 100644 index 000000000..b4e6fc5ad --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java @@ -0,0 +1,376 @@ +/* + * 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.dialer.app.calllog; + +import android.Manifest.permission; +import android.annotation.TargetApi; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.provider.CallLog; +import android.provider.VoicemailContract.Voicemails; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; +import java.util.Arrays; + +@TargetApi(VERSION_CODES.M) +public class CallLogAsyncTaskUtil { + + private static final String TAG = "CallLogAsyncTaskUtil"; + private static AsyncTaskExecutor sAsyncTaskExecutor; + + private static void initTaskExecutor() { + sAsyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor(); + } + + public static void getCallDetails( + @NonNull final Context context, + @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener, + @NonNull final Uri... callUris) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.GET_CALL_DETAILS, + new AsyncTask() { + @Override + public PhoneCallDetails[] doInBackground(Void... params) { + if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG) + != PackageManager.PERMISSION_GRANTED) { + LogUtil.w("CallLogAsyncTaskUtil.getCallDetails", "missing READ_CALL_LOG permission"); + return null; + } + // TODO: All calls correspond to the same person, so make a single lookup. + final int numCalls = callUris.length; + PhoneCallDetails[] details = new PhoneCallDetails[numCalls]; + try { + for (int index = 0; index < numCalls; ++index) { + details[index] = getPhoneCallDetailsForUri(context, callUris[index]); + } + return details; + } catch (IllegalArgumentException e) { + // Something went wrong reading in our primary data. + LogUtil.e( + "CallLogAsyncTaskUtil.getCallDetails", "invalid URI starting call details", e); + return null; + } + } + + @Override + public void onPostExecute(PhoneCallDetails[] phoneCallDetails) { + if (callLogAsyncTaskListener != null) { + callLogAsyncTaskListener.onGetCallDetails(phoneCallDetails); + } + } + }); + } + + /** Return the phone call details for a given call log URI. */ + private static PhoneCallDetails getPhoneCallDetailsForUri( + @NonNull Context context, @NonNull Uri callUri) { + Cursor cursor = + context + .getContentResolver() + .query(callUri, CallDetailQuery.CALL_LOG_PROJECTION, null, null, null); + + try { + if (cursor == null || !cursor.moveToFirst()) { + throw new IllegalArgumentException("Cannot find content: " + callUri); + } + + // Read call log. + final String countryIso = cursor.getString(CallDetailQuery.COUNTRY_ISO_COLUMN_INDEX); + final String number = cursor.getString(CallDetailQuery.NUMBER_COLUMN_INDEX); + final String postDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) + ? cursor.getString(CallDetailQuery.POST_DIAL_DIGITS) + : ""; + final String viaNumber = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallDetailQuery.VIA_NUMBER) : ""; + final int numberPresentation = + cursor.getInt(CallDetailQuery.NUMBER_PRESENTATION_COLUMN_INDEX); + + final PhoneAccountHandle accountHandle = + PhoneAccountUtils.getAccount( + cursor.getString(CallDetailQuery.ACCOUNT_COMPONENT_NAME), + cursor.getString(CallDetailQuery.ACCOUNT_ID)); + + // If this is not a regular number, there is no point in looking it up in the contacts. + ContactInfoHelper contactInfoHelper = + new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)); + boolean isVoicemail = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number); + boolean shouldLookupNumber = + PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) && !isVoicemail; + ContactInfo info = ContactInfo.EMPTY; + + if (shouldLookupNumber) { + ContactInfo lookupInfo = contactInfoHelper.lookupNumber(number, countryIso); + info = lookupInfo != null ? lookupInfo : ContactInfo.EMPTY; + } + + PhoneCallDetails details = new PhoneCallDetails(number, numberPresentation, postDialDigits); + details.updateDisplayNumber(context, info.formattedNumber, isVoicemail); + + details.viaNumber = viaNumber; + details.accountHandle = accountHandle; + details.contactUri = info.lookupUri; + details.namePrimary = info.name; + details.nameAlternative = info.nameAlternative; + details.numberType = info.type; + details.numberLabel = info.label; + details.photoUri = info.photoUri; + details.sourceType = info.sourceType; + details.objectId = info.objectId; + + details.callTypes = new int[] {cursor.getInt(CallDetailQuery.CALL_TYPE_COLUMN_INDEX)}; + details.date = cursor.getLong(CallDetailQuery.DATE_COLUMN_INDEX); + details.duration = cursor.getLong(CallDetailQuery.DURATION_COLUMN_INDEX); + details.features = cursor.getInt(CallDetailQuery.FEATURES); + details.geocode = cursor.getString(CallDetailQuery.GEOCODED_LOCATION_COLUMN_INDEX); + details.transcription = cursor.getString(CallDetailQuery.TRANSCRIPTION_COLUMN_INDEX); + + details.countryIso = + !TextUtils.isEmpty(countryIso) ? countryIso : GeoUtil.getCurrentCountryIso(context); + + if (!cursor.isNull(CallDetailQuery.DATA_USAGE)) { + details.dataUsage = cursor.getLong(CallDetailQuery.DATA_USAGE); + } + + return details; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Delete specified calls from the call log. + * + * @param context The context. + * @param callIds String of the callIds to delete from the call log, delimited by commas (","). + * @param callLogAsyncTaskListener The listener to invoke after the entries have been deleted. + */ + public static void deleteCalls( + @NonNull final Context context, + final String callIds, + @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.DELETE_CALL, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + context + .getContentResolver() + .delete( + TelecomUtil.getCallLogUri(context), + CallLog.Calls._ID + " IN (" + callIds + ")", + null); + return null; + } + + @Override + public void onPostExecute(Void result) { + if (callLogAsyncTaskListener != null) { + callLogAsyncTaskListener.onDeleteCall(); + } + } + }); + } + + public static void markVoicemailAsRead( + @NonNull final Context context, @NonNull final Uri voicemailUri) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.MARK_VOICEMAIL_READ, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + ContentValues values = new ContentValues(); + values.put(Voicemails.IS_READ, true); + context + .getContentResolver() + .update(voicemailUri, values, Voicemails.IS_READ + " = 0", null); + + Intent intent = new Intent(context, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + context.startService(intent); + return null; + } + }); + } + + public static void deleteVoicemail( + @NonNull final Context context, + final Uri voicemailUri, + @Nullable final CallLogAsyncTaskListener callLogAsyncTaskListener) { + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.DELETE_VOICEMAIL, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + context.getContentResolver().delete(voicemailUri, null, null); + return null; + } + + @Override + public void onPostExecute(Void result) { + if (callLogAsyncTaskListener != null) { + callLogAsyncTaskListener.onDeleteVoicemail(); + } + } + }); + } + + public static void markCallAsRead(@NonNull final Context context, @NonNull final long[] callIds) { + if (!PermissionsUtil.hasPhonePermissions(context)) { + return; + } + if (sAsyncTaskExecutor == null) { + initTaskExecutor(); + } + + sAsyncTaskExecutor.submit( + Tasks.MARK_CALL_READ, + new AsyncTask() { + @Override + public Void doInBackground(Void... params) { + + StringBuilder where = new StringBuilder(); + where.append(CallLog.Calls.TYPE).append(" = ").append(CallLog.Calls.MISSED_TYPE); + where.append(" AND "); + + Long[] callIdLongs = new Long[callIds.length]; + for (int i = 0; i < callIds.length; i++) { + callIdLongs[i] = callIds[i]; + } + where + .append(CallLog.Calls._ID) + .append(" IN (" + TextUtils.join(",", callIdLongs) + ")"); + + ContentValues values = new ContentValues(1); + values.put(CallLog.Calls.IS_READ, "1"); + context + .getContentResolver() + .update(CallLog.Calls.CONTENT_URI, values, where.toString(), null); + return null; + } + }); + } + + @VisibleForTesting + public static void resetForTest() { + sAsyncTaskExecutor = null; + } + + /** The enumeration of {@link AsyncTask} objects used in this class. */ + public enum Tasks { + DELETE_VOICEMAIL, + DELETE_CALL, + MARK_VOICEMAIL_READ, + MARK_CALL_READ, + GET_CALL_DETAILS, + UPDATE_DURATION, + } + + public interface CallLogAsyncTaskListener { + + void onDeleteCall(); + + void onDeleteVoicemail(); + + void onGetCallDetails(PhoneCallDetails[] details); + } + + private static final class CallDetailQuery { + + public static final String[] CALL_LOG_PROJECTION; + static final int DATE_COLUMN_INDEX = 0; + static final int DURATION_COLUMN_INDEX = 1; + static final int NUMBER_COLUMN_INDEX = 2; + static final int CALL_TYPE_COLUMN_INDEX = 3; + static final int COUNTRY_ISO_COLUMN_INDEX = 4; + static final int GEOCODED_LOCATION_COLUMN_INDEX = 5; + static final int NUMBER_PRESENTATION_COLUMN_INDEX = 6; + static final int ACCOUNT_COMPONENT_NAME = 7; + static final int ACCOUNT_ID = 8; + static final int FEATURES = 9; + static final int DATA_USAGE = 10; + static final int TRANSCRIPTION_COLUMN_INDEX = 11; + static final int POST_DIAL_DIGITS = 12; + static final int VIA_NUMBER = 13; + private static final String[] CALL_LOG_PROJECTION_INTERNAL = + new String[] { + CallLog.Calls.DATE, + CallLog.Calls.DURATION, + CallLog.Calls.NUMBER, + CallLog.Calls.TYPE, + CallLog.Calls.COUNTRY_ISO, + CallLog.Calls.GEOCODED_LOCATION, + CallLog.Calls.NUMBER_PRESENTATION, + CallLog.Calls.PHONE_ACCOUNT_COMPONENT_NAME, + CallLog.Calls.PHONE_ACCOUNT_ID, + CallLog.Calls.FEATURES, + CallLog.Calls.DATA_USAGE, + CallLog.Calls.TRANSCRIPTION + }; + + static { + ArrayList projectionList = new ArrayList<>(); + projectionList.addAll(Arrays.asList(CALL_LOG_PROJECTION_INTERNAL)); + if (VERSION.SDK_INT >= VERSION_CODES.N) { + projectionList.add(CallLog.Calls.POST_DIAL_DIGITS); + projectionList.add(CallLog.Calls.VIA_NUMBER); + } + projectionList.trimToSize(); + CALL_LOG_PROJECTION = projectionList.toArray(new String[projectionList.size()]); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogFragment.java b/java/com/android/dialer/app/calllog/CallLogFragment.java new file mode 100644 index 000000000..1ae68cd65 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogFragment.java @@ -0,0 +1,528 @@ +/* + * 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.dialer.app.calllog; + +import static android.Manifest.permission.READ_CALL_LOG; + +import android.app.Activity; +import android.app.Fragment; +import android.app.KeyguardManager; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.Bindings; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.contactinfo.ContactInfoCache.OnContactInfoChangedListener; +import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.list.ListsFragment.ListsPage; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.util.PermissionsUtil; + +/** + * Displays a list of call log entries. To filter for a particular kind of call (all, missed or + * voicemails), specify it in the constructor. + */ +public class CallLogFragment extends Fragment + implements ListsPage, + CallLogQueryHandler.Listener, + CallLogAdapter.CallFetcher, + OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback, + CallLogModalAlertManager.Listener { + private static final String KEY_FILTER_TYPE = "filter_type"; + private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission"; + private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required"; + + private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1; + + private static final int EVENT_UPDATE_DISPLAY = 1; + + private static final long MILLIS_IN_MINUTE = 60 * 1000; + private final Handler mHandler = new Handler(); + // See issue 6363009 + private final ContentObserver mCallLogObserver = new CustomContentObserver(); + private final ContentObserver mContactsObserver = new CustomContentObserver(); + private RecyclerView mRecyclerView; + private LinearLayoutManager mLayoutManager; + private CallLogAdapter mAdapter; + private CallLogQueryHandler mCallLogQueryHandler; + private boolean mScrollToTop; + private EmptyContentView mEmptyListView; + private KeyguardManager mKeyguardManager; + private ContactInfoCache mContactInfoCache; + private final OnContactInfoChangedListener mOnContactInfoChangedListener = + new OnContactInfoChangedListener() { + @Override + public void onContactInfoChanged() { + if (mAdapter != null) { + mAdapter.notifyDataSetChanged(); + } + } + }; + private boolean mRefreshDataRequired; + private boolean mHasReadCallLogPermission; + // Exactly same variable is in Fragment as a package private. + private boolean mMenuVisible = true; + // Default to all calls. + protected int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL; + + private final Handler mDisplayUpdateHandler = + new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case EVENT_UPDATE_DISPLAY: + refreshData(); + rescheduleDisplayUpdate(); + break; + } + } + }; + protected CallLogModalAlertManager mModalAlertManager; + private ViewGroup mModalAlertView; + + @Override + public void onCreate(Bundle state) { + LogUtil.d("CallLogFragment.onCreate", toString()); + super.onCreate(state); + mRefreshDataRequired = true; + if (state != null) { + mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter); + mHasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false); + mRefreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); + } + + final Activity activity = getActivity(); + final ContentResolver resolver = activity.getContentResolver(); + mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this); + mKeyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE); + resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver); + resolver.registerContentObserver( + ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver); + setHasOptionsMenu(true); + } + + /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */ + @Override + public boolean onCallsFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing()) { + // Return false; we did not take ownership of the cursor + return false; + } + mAdapter.invalidatePositions(); + mAdapter.setLoading(false); + mAdapter.changeCursor(cursor); + // This will update the state of the "Clear call log" menu item. + getActivity().invalidateOptionsMenu(); + + if (cursor != null && cursor.getCount() > 0) { + mRecyclerView.setPaddingRelative( + mRecyclerView.getPaddingStart(), + 0, + mRecyclerView.getPaddingEnd(), + getResources().getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding)); + mEmptyListView.setVisibility(View.GONE); + } else { + mRecyclerView.setPaddingRelative( + mRecyclerView.getPaddingStart(), 0, mRecyclerView.getPaddingEnd(), 0); + mEmptyListView.setVisibility(View.VISIBLE); + } + if (mScrollToTop) { + // The smooth-scroll animation happens over a fixed time period. + // As a result, if it scrolls through a large portion of the list, + // each frame will jump so far from the previous one that the user + // will not experience the illusion of downward motion. Instead, + // if we're not already near the top of the list, we instantly jump + // near the top, and animate from there. + if (mLayoutManager.findFirstVisibleItemPosition() > 5) { + // TODO: Jump to near the top, then begin smooth scroll. + mRecyclerView.smoothScrollToPosition(0); + } + // Workaround for framework issue: the smooth-scroll doesn't + // occur if setSelection() is called immediately before. + mHandler.post( + new Runnable() { + @Override + public void run() { + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + mRecyclerView.smoothScrollToPosition(0); + } + }); + + mScrollToTop = false; + } + return true; + } + + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) {} + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) {} + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) {} + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + View view = inflater.inflate(R.layout.call_log_fragment, container, false); + setupView(view); + return view; + } + + protected void setupView(View view) { + mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); + mRecyclerView.setHasFixedSize(true); + mLayoutManager = new LinearLayoutManager(getActivity()); + mRecyclerView.setLayoutManager(mLayoutManager); + mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); + mEmptyListView.setImage(R.drawable.empty_call_log); + mEmptyListView.setActionClickedListener(this); + mModalAlertView = (ViewGroup) view.findViewById(R.id.modal_message_container); + mModalAlertManager = + new CallLogModalAlertManager(LayoutInflater.from(getContext()), mModalAlertView, this); + } + + protected void setupData() { + int activityType = CallLogAdapter.ACTIVITY_TYPE_DIALTACTS; + String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); + + mContactInfoCache = + new ContactInfoCache( + ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity()) + .getRetainedCache(), + new ContactInfoHelper(getActivity(), currentCountryIso), + mOnContactInfoChangedListener); + mAdapter = + Bindings.getLegacy(getActivity()) + .newCallLogAdapter( + getActivity(), + mRecyclerView, + this, + CallLogCache.getCallLogCache(getActivity()), + mContactInfoCache, + getVoicemailPlaybackPresenter(), + activityType); + mRecyclerView.setAdapter(mAdapter); + fetchCalls(); + } + + @Nullable + protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() { + return null; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + setupData(); + mAdapter.onRestoreInstanceState(savedInstanceState); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + updateEmptyMessage(mCallTypeFilter); + } + + @Override + public void onResume() { + LogUtil.d("CallLogFragment.onResume", toString()); + super.onResume(); + final boolean hasReadCallLogPermission = + PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG); + if (!mHasReadCallLogPermission && hasReadCallLogPermission) { + // We didn't have the permission before, and now we do. Force a refresh of the call log. + // Note that this code path always happens on a fresh start, but mRefreshDataRequired + // is already true in that case anyway. + mRefreshDataRequired = true; + updateEmptyMessage(mCallTypeFilter); + } + + mHasReadCallLogPermission = hasReadCallLogPermission; + + /* + * Always clear the filtered numbers cache since users could have blocked/unblocked numbers + * from the settings page + */ + mAdapter.clearFilteredNumbersCache(); + refreshData(); + mAdapter.onResume(); + + rescheduleDisplayUpdate(); + } + + @Override + public void onPause() { + LogUtil.d("CallLogFragment.onPause", toString()); + cancelDisplayUpdate(); + mAdapter.onPause(); + super.onPause(); + } + + @Override + public void onStop() { + updateOnTransition(); + + super.onStop(); + mAdapter.onStop(); + } + + @Override + public void onDestroy() { + LogUtil.d("CallLogFragment.onDestroy", toString()); + mAdapter.changeCursor(null); + + getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver); + getActivity().getContentResolver().unregisterContentObserver(mContactsObserver); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter); + outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, mHasReadCallLogPermission); + outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, mRefreshDataRequired); + + mContactInfoCache.stop(); + + mAdapter.onSaveInstanceState(outState); + } + + @Override + public void fetchCalls() { + mCallLogQueryHandler.fetchCalls(mCallTypeFilter); + ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + } + + private void updateEmptyMessage(int filterType) { + final Context context = getActivity(); + if (context == null) { + return; + } + + if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) { + mEmptyListView.setDescription(R.string.permission_no_calllog); + mEmptyListView.setActionLabel(R.string.permission_single_turn_on); + return; + } + + final int messageId; + switch (filterType) { + case Calls.MISSED_TYPE: + messageId = R.string.call_log_missed_empty; + break; + case Calls.VOICEMAIL_TYPE: + messageId = R.string.call_log_voicemail_empty; + break; + case CallLogQueryHandler.CALL_TYPE_ALL: + messageId = R.string.call_log_all_empty; + break; + default: + throw new IllegalArgumentException( + "Unexpected filter type in CallLogFragment: " + filterType); + } + mEmptyListView.setDescription(messageId); + if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { + mEmptyListView.setActionLabel(R.string.call_log_all_empty_action); + } + } + + public CallLogAdapter getAdapter() { + return mAdapter; + } + + @Override + public void setMenuVisibility(boolean menuVisible) { + super.setMenuVisibility(menuVisible); + if (mMenuVisible != menuVisible) { + mMenuVisible = menuVisible; + if (!menuVisible) { + updateOnTransition(); + } else if (isResumed()) { + refreshData(); + } + } + } + + /** Requests updates to the data to be shown. */ + private void refreshData() { + // Prevent unnecessary refresh. + if (mRefreshDataRequired) { + // Mark all entries in the contact info cache as out of date, so they will be looked up + // again once being shown. + mContactInfoCache.invalidate(); + mAdapter.setLoading(true); + + fetchCalls(); + mCallLogQueryHandler.fetchVoicemailStatus(); + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + updateOnTransition(); + mRefreshDataRequired = false; + } else { + // Refresh the display of the existing data to update the timestamp text descriptions. + mAdapter.notifyDataSetChanged(); + } + } + + /** + * Updates the voicemail notification state. + * + *

TODO: Move to CallLogActivity + */ + private void updateOnTransition() { + // We don't want to update any call data when keyguard is on because the user has likely not + // seen the new calls yet. + // This might be called before onCreate() and thus we need to check null explicitly. + if (mKeyguardManager != null + && !mKeyguardManager.inKeyguardRestrictedInputMode() + && mCallTypeFilter == Calls.VOICEMAIL_TYPE) { + CallLogNotificationsHelper.updateVoicemailNotifications(getActivity()); + } + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) { + FragmentCompat.requestPermissions( + this, new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE); + } else { + ((HostInterface) activity).showDialpad(); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) { + if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + mRefreshDataRequired = true; + } + } + } + + /** Schedules an update to the relative call times (X mins ago). */ + private void rescheduleDisplayUpdate() { + if (!mDisplayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) { + long time = System.currentTimeMillis(); + // This value allows us to change the display relatively close to when the time changes + // from one minute to the next. + long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE); + mDisplayUpdateHandler.sendEmptyMessageDelayed(EVENT_UPDATE_DISPLAY, millisUtilNextMinute); + } + } + + /** Cancels any pending update requests to update the relative call times (X mins ago). */ + private void cancelDisplayUpdate() { + mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY); + } + + @Override + @CallSuper + public void onPageResume(@Nullable Activity activity) { + LogUtil.d("CallLogFragment.onPageResume", "frag: %s", this); + if (activity != null) { + ((HostInterface) activity) + .enableFloatingButton(mModalAlertManager == null || mModalAlertManager.isEmpty()); + } + } + + @Override + @CallSuper + public void onPagePause(@Nullable Activity activity) { + LogUtil.d("CallLogFragment.onPagePause", "frag: %s", this); + } + + @Override + public void onShowModalAlert(boolean show) { + LogUtil.d( + "CallLogFragment.onShowModalAlert", + "show: %b, fragment: %s, isVisible: %b", + show, + this, + getUserVisibleHint()); + getAdapter().notifyDataSetChanged(); + HostInterface hostInterface = (HostInterface) getActivity(); + if (show) { + mRecyclerView.setVisibility(View.GONE); + mModalAlertView.setVisibility(View.VISIBLE); + if (hostInterface != null && getUserVisibleHint()) { + hostInterface.enableFloatingButton(false); + } + } else { + mRecyclerView.setVisibility(View.VISIBLE); + mModalAlertView.setVisibility(View.GONE); + if (hostInterface != null && getUserVisibleHint()) { + hostInterface.enableFloatingButton(true); + } + } + } + + public interface HostInterface { + + void showDialpad(); + + void enableFloatingButton(boolean enabled); + } + + protected class CustomContentObserver extends ContentObserver { + + public CustomContentObserver() { + super(mHandler); + } + + @Override + public void onChange(boolean selfChange) { + mRefreshDataRequired = true; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java new file mode 100644 index 000000000..45ff3783d --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogGroupBuilder.java @@ -0,0 +1,274 @@ +/* + * 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.dialer.app.calllog; + +import android.database.Cursor; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.text.format.Time; +import com.android.contacts.common.util.DateUtils; +import com.android.dialer.compat.AppCompatConstants; +import com.android.dialer.phonenumbercache.CallLogQuery; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import java.util.Objects; + +/** + * Groups together calls in the call log. The primary grouping attempts to group together calls to + * and from the same number into a single row on the call log. A secondary grouping assigns calls, + * grouped via the primary grouping, to "day groups". The day groups provide a means of identifying + * the calls which occurred "Today", "Yesterday", "Last week", or "Other". + * + *

This class is meant to be used in conjunction with {@link GroupingListAdapter}. + */ +public class CallLogGroupBuilder { + + /** + * Day grouping for call log entries used to represent no associated day group. Used primarily + * when retrieving the previous day group, but there is no previous day group (i.e. we are at the + * start of the list). + */ + public static final int DAY_GROUP_NONE = -1; + /** Day grouping for calls which occurred today. */ + public static final int DAY_GROUP_TODAY = 0; + /** Day grouping for calls which occurred yesterday. */ + public static final int DAY_GROUP_YESTERDAY = 1; + /** Day grouping for calls which occurred before last week. */ + public static final int DAY_GROUP_OTHER = 2; + /** Instance of the time object used for time calculations. */ + private static final Time TIME = new Time(); + /** The object on which the groups are created. */ + private final GroupCreator mGroupCreator; + + public CallLogGroupBuilder(GroupCreator groupCreator) { + mGroupCreator = groupCreator; + } + + /** + * Finds all groups of adjacent entries in the call log which should be grouped together and calls + * {@link GroupCreator#addGroup(int, int)} on {@link #mGroupCreator} for each of them. + * + *

For entries that are not grouped with others, we do not need to create a group of size one. + * + *

It assumes that the cursor will not change during its execution. + * + * @see GroupingListAdapter#addGroups(Cursor) + */ + public void addGroups(Cursor cursor) { + final int count = cursor.getCount(); + if (count == 0) { + return; + } + + // Clear any previous day grouping information. + mGroupCreator.clearDayGroups(); + + // Get current system time, used for calculating which day group calls belong to. + long currentTime = System.currentTimeMillis(); + cursor.moveToFirst(); + + // Determine the day group for the first call in the cursor. + final long firstDate = cursor.getLong(CallLogQuery.DATE); + final long firstRowId = cursor.getLong(CallLogQuery.ID); + int groupDayGroup = getDayGroup(firstDate, currentTime); + mGroupCreator.setDayGroup(firstRowId, groupDayGroup); + + // Instantiate the group values to those of the first call in the cursor. + String groupNumber = cursor.getString(CallLogQuery.NUMBER); + String groupPostDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; + String groupViaNumbers = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; + int groupCallType = cursor.getInt(CallLogQuery.CALL_TYPE); + String groupAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + String groupAccountId = cursor.getString(CallLogQuery.ACCOUNT_ID); + int groupSize = 1; + + String number; + String numberPostDialDigits; + String numberViaNumbers; + int callType; + String accountComponentName; + String accountId; + + while (cursor.moveToNext()) { + // Obtain the values for the current call to group. + number = cursor.getString(CallLogQuery.NUMBER); + numberPostDialDigits = + (VERSION.SDK_INT >= VERSION_CODES.N) + ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) + : ""; + numberViaNumbers = + (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; + callType = cursor.getInt(CallLogQuery.CALL_TYPE); + accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); + accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); + + final boolean isSameNumber = equalNumbers(groupNumber, number); + final boolean isSamePostDialDigits = groupPostDialDigits.equals(numberPostDialDigits); + final boolean isSameViaNumbers = groupViaNumbers.equals(numberViaNumbers); + final boolean isSameAccount = + isSameAccount(groupAccountComponentName, accountComponentName, groupAccountId, accountId); + + // Group with the same number and account. Never group voicemails. Only group blocked + // calls with other blocked calls. + if (isSameNumber + && isSameAccount + && isSamePostDialDigits + && isSameViaNumbers + && areBothNotVoicemail(callType, groupCallType) + && (areBothNotBlocked(callType, groupCallType) + || areBothBlocked(callType, groupCallType))) { + // Increment the size of the group to include the current call, but do not create + // the group until finding a call that does not match. + groupSize++; + } else { + // The call group has changed. Determine the day group for the new call group. + final long date = cursor.getLong(CallLogQuery.DATE); + groupDayGroup = getDayGroup(date, currentTime); + + // Create a group for the previous group of calls, which does not include the + // current call. + mGroupCreator.addGroup(cursor.getPosition() - groupSize, groupSize); + + // Start a new group; it will include at least the current call. + groupSize = 1; + + // Update the group values to those of the current call. + groupNumber = number; + groupPostDialDigits = numberPostDialDigits; + groupViaNumbers = numberViaNumbers; + groupCallType = callType; + groupAccountComponentName = accountComponentName; + groupAccountId = accountId; + } + + // Save the day group associated with the current call. + final long currentCallId = cursor.getLong(CallLogQuery.ID); + mGroupCreator.setDayGroup(currentCallId, groupDayGroup); + } + + // Create a group for the last set of calls. + mGroupCreator.addGroup(count - groupSize, groupSize); + } + + @VisibleForTesting + boolean equalNumbers(@Nullable String number1, @Nullable String number2) { + if (PhoneNumberHelper.isUriNumber(number1) || PhoneNumberHelper.isUriNumber(number2)) { + return compareSipAddresses(number1, number2); + } else { + return PhoneNumberUtils.compare(number1, number2); + } + } + + private boolean isSameAccount(String name1, String name2, String id1, String id2) { + return TextUtils.equals(name1, name2) && TextUtils.equals(id1, id2); + } + + @VisibleForTesting + boolean compareSipAddresses(@Nullable String number1, @Nullable String number2) { + if (number1 == null || number2 == null) { + return Objects.equals(number1, number2); + } + + int index1 = number1.indexOf('@'); + final String userinfo1; + final String rest1; + if (index1 != -1) { + userinfo1 = number1.substring(0, index1); + rest1 = number1.substring(index1); + } else { + userinfo1 = number1; + rest1 = ""; + } + + int index2 = number2.indexOf('@'); + final String userinfo2; + final String rest2; + if (index2 != -1) { + userinfo2 = number2.substring(0, index2); + rest2 = number2.substring(index2); + } else { + userinfo2 = number2; + rest2 = ""; + } + + return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2); + } + + /** + * Given a call date and the current date, determine which date group the call belongs in. + * + * @param date The call date. + * @param now The current date. + * @return The date group the call belongs in. + */ + private int getDayGroup(long date, long now) { + int days = DateUtils.getDayDifference(TIME, date, now); + + if (days == 0) { + return DAY_GROUP_TODAY; + } else if (days == 1) { + return DAY_GROUP_YESTERDAY; + } else { + return DAY_GROUP_OTHER; + } + } + + private boolean areBothNotVoicemail(int callType, int groupCallType) { + return callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE + && groupCallType != AppCompatConstants.CALLS_VOICEMAIL_TYPE; + } + + private boolean areBothNotBlocked(int callType, int groupCallType) { + return callType != AppCompatConstants.CALLS_BLOCKED_TYPE + && groupCallType != AppCompatConstants.CALLS_BLOCKED_TYPE; + } + + private boolean areBothBlocked(int callType, int groupCallType) { + return callType == AppCompatConstants.CALLS_BLOCKED_TYPE + && groupCallType == AppCompatConstants.CALLS_BLOCKED_TYPE; + } + + public interface GroupCreator { + + /** + * Defines the interface for adding a group to the call log. The primary group for a call log + * groups the calls together based on the number which was dialed. + * + * @param cursorPosition The starting position of the group in the cursor. + * @param size The size of the group. + */ + void addGroup(int cursorPosition, int size); + + /** + * Defines the interface for tracking the day group each call belongs to. Calls in a call group + * are assigned the same day group as the first call in the group. The day group assigns calls + * to the buckets: Today, Yesterday, Last week, and Other + * + * @param rowId The row Id of the current call. + * @param dayGroup The day group the call belongs in. + */ + void setDayGroup(long rowId, int dayGroup); + + /** Defines the interface for clearing the day groupings information on rebind/regroup. */ + void clearDayGroups(); + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogListItemHelper.java b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java new file mode 100644 index 000000000..ea2119c83 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogListItemHelper.java @@ -0,0 +1,277 @@ +/* + * 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.dialer.app.calllog; + +import android.content.res.Resources; +import android.provider.CallLog.Calls; +import android.support.annotation.WorkerThread; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.util.Log; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.common.Assert; +import com.android.dialer.compat.AppCompatConstants; + +/** Helper class to fill in the views of a call log entry. */ +/* package */ class CallLogListItemHelper { + + private static final String TAG = "CallLogListItemHelper"; + + /** Helper for populating the details of a phone call. */ + private final PhoneCallDetailsHelper mPhoneCallDetailsHelper; + /** Resources to look up strings. */ + private final Resources mResources; + + private final CallLogCache mCallLogCache; + + /** + * Creates a new helper instance. + * + * @param phoneCallDetailsHelper used to set the details of a phone call + * @param resources The object from which resources can be retrieved + * @param callLogCache A cache for values retrieved from telecom/telephony + */ + public CallLogListItemHelper( + PhoneCallDetailsHelper phoneCallDetailsHelper, + Resources resources, + CallLogCache callLogCache) { + mPhoneCallDetailsHelper = phoneCallDetailsHelper; + mResources = resources; + mCallLogCache = callLogCache; + } + + /** + * Update phone call details. This is called before any drawing to avoid expensive operation on UI + * thread. + * + * @param details + */ + @WorkerThread + public void updatePhoneCallDetails(PhoneCallDetails details) { + Assert.isWorkerThread(); + details.callLocationAndDate = mPhoneCallDetailsHelper.getCallLocationAndDate(details); + details.callDescription = getCallDescription(details); + } + + /** + * Sets the name, label, and number for a contact. + * + * @param views the views to populate + * @param details the details of a phone call needed to fill in the data + */ + public void setPhoneCallDetails(CallLogListItemViewHolder views, PhoneCallDetails details) { + mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details); + + // Set the accessibility text for the contact badge + views.quickContactView.setContentDescription(getContactBadgeDescription(details)); + + // Set the primary action accessibility description + views.primaryActionView.setContentDescription(details.callDescription); + + // Cache name or number of caller. Used when setting the content descriptions of buttons + // when the actions ViewStub is inflated. + views.nameOrNumber = getNameOrNumber(details); + + // The call type or Location associated with the call. Use when setting text for a + // voicemail log's call button + views.callTypeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details); + + // Cache country iso. Used for number filtering. + views.countryIso = details.countryIso; + + views.updatePhoto(); + } + + /** + * Sets the accessibility descriptions for the action buttons in the action button ViewStub. + * + * @param views The views associated with the current call log entry. + */ + public void setActionContentDescriptions(CallLogListItemViewHolder views) { + if (views.nameOrNumber == null) { + Log.e(TAG, "setActionContentDescriptions; name or number is null."); + } + + // Calling expandTemplate with a null parameter will cause a NullPointerException. + // Although we don't expect a null name or number, it is best to protect against it. + CharSequence nameOrNumber = views.nameOrNumber == null ? "" : views.nameOrNumber; + + views.videoCallButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_video_call_action), nameOrNumber)); + + views.createNewContactButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_create_new_contact_action), nameOrNumber)); + + views.addToExistingContactButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_add_to_existing_contact_action), + nameOrNumber)); + + views.detailsButtonView.setContentDescription( + TextUtils.expandTemplate( + mResources.getString(R.string.description_details_action), nameOrNumber)); + } + + /** + * Returns the accessibility description for the contact badge for a call log entry. + * + * @param details Details of call. + * @return Accessibility description. + */ + private CharSequence getContactBadgeDescription(PhoneCallDetails details) { + if (details.isSpam) { + return mResources.getString( + R.string.description_spam_contact_details, getNameOrNumber(details)); + } + return mResources.getString(R.string.description_contact_details, getNameOrNumber(details)); + } + + /** + * Returns the accessibility description of the "return call/call" action for a call log entry. + * Accessibility text is a combination of: {Voicemail Prefix}. {Number of Calls}. {Caller + * information} {Phone Account}. If most recent call is a voicemail, {Voicemail Prefix} is "New + * Voicemail.", otherwise "". + * + *

If more than one call for the caller, {Number of Calls} is: "{number of calls} calls.", + * otherwise "". + * + *

The {Caller Information} references the most recent call associated with the caller. For + * incoming calls: If missed call: Missed call from {Name/Number} {Call Type} {Call Time}. If + * answered call: Answered call from {Name/Number} {Call Type} {Call Time}. + * + *

For outgoing calls: If outgoing: Call to {Name/Number] {Call Type} {Call Time}. + * + *

Where: {Name/Number} is the name or number of the caller (as shown in call log). {Call type} + * is the contact phone number type (eg mobile) or location. {Call Time} is the time since the + * last call for the contact occurred. + * + *

The {Phone Account} refers to the account/SIM through which the call was placed or received + * in multi-SIM devices. + * + *

Examples: 3 calls. New Voicemail. Missed call from Joe Smith mobile 2 hours ago on SIM 1. + * + *

2 calls. Answered call from John Doe mobile 1 hour ago. + * + * @param context The application context. + * @param details Details of call. + * @return Return call action description. + */ + public CharSequence getCallDescription(PhoneCallDetails details) { + // Get the name or number of the caller. + final CharSequence nameOrNumber = getNameOrNumber(details); + + // Get the call type or location of the caller; null if not applicable + final CharSequence typeOrLocation = mPhoneCallDetailsHelper.getCallTypeOrLocation(details); + + // Get the time/date of the call + final CharSequence timeOfCall = mPhoneCallDetailsHelper.getCallDate(details); + + SpannableStringBuilder callDescription = new SpannableStringBuilder(); + + // Add number of calls if more than one. + if (details.callTypes.length > 1) { + callDescription.append( + mResources.getString(R.string.description_num_calls, details.callTypes.length)); + } + + // If call had video capabilities, add the "Video Call" string. + if ((details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO) { + callDescription.append(mResources.getString(R.string.description_video_call)); + } + + String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle); + CharSequence onAccountLabel = + PhoneCallDetails.createAccountLabelDescription(mResources, details.viaNumber, accountLabel); + + int stringID = getCallDescriptionStringID(details.callTypes, details.isRead); + callDescription.append( + TextUtils.expandTemplate( + mResources.getString(stringID), + nameOrNumber, + typeOrLocation == null ? "" : typeOrLocation, + timeOfCall, + onAccountLabel)); + + return callDescription; + } + + /** + * Determine the appropriate string ID to describe a call for accessibility purposes. + * + * @param callTypes The type of call corresponding to this entry or multiple if this entry + * represents multiple calls grouped together. + * @param isRead If the entry is a voicemail, {@code true} if the voicemail is read. + * @return String resource ID to use. + */ + public int getCallDescriptionStringID(int[] callTypes, boolean isRead) { + int lastCallType = getLastCallType(callTypes); + int stringID; + + if (lastCallType == AppCompatConstants.CALLS_MISSED_TYPE) { + //Message: Missed call from , , , + //. + stringID = R.string.description_incoming_missed_call; + } else if (lastCallType == AppCompatConstants.CALLS_INCOMING_TYPE) { + //Message: Answered call from , , , + //. + stringID = R.string.description_incoming_answered_call; + } else if (lastCallType == AppCompatConstants.CALLS_VOICEMAIL_TYPE) { + //Message: (Unread) [V/v]oicemail from , , , + //. + stringID = + isRead ? R.string.description_read_voicemail : R.string.description_unread_voicemail; + } else { + //Message: Call to , , , . + stringID = R.string.description_outgoing_call; + } + return stringID; + } + + /** + * Determine the call type for the most recent call. + * + * @param callTypes Call types to check. + * @return Call type. + */ + private int getLastCallType(int[] callTypes) { + if (callTypes.length > 0) { + return callTypes[0]; + } else { + return Calls.MISSED_TYPE; + } + } + + /** + * Return the name or number of the caller specified by the details. + * + * @param details Call details + * @return the name (if known) of the caller, otherwise the formatted number. + */ + private CharSequence getNameOrNumber(PhoneCallDetails details) { + final CharSequence recipient; + if (!TextUtils.isEmpty(details.getPreferredName())) { + recipient = details.getPreferredName(); + } else { + recipient = details.displayNumber + details.postDialDigits; + } + return recipient; + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java new file mode 100644 index 000000000..6abd36078 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java @@ -0,0 +1,966 @@ +/* + * 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.dialer.app.calllog; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.provider.CallLog; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.widget.CardView; +import android.support.v7.widget.RecyclerView; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewStub; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.contacts.common.dialog.CallSubjectDialog; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.voicemail.VoicemailPlaybackLayout; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.blocking.BlockedNumbersMigrator; +import com.android.dialer.blocking.FilteredNumberCompat; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.callcomposer.CallComposerActivity; +import com.android.dialer.callcomposer.nano.CallComposerContact; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; + +/** + * This is an object containing references to views contained by the call log list item. This + * improves performance by reducing the frequency with which we need to find views by IDs. + * + *

This object also contains UI logic pertaining to the view, to isolate it from the + * CallLogAdapter. + */ +public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder + implements View.OnClickListener, + MenuItem.OnMenuItemClickListener, + View.OnCreateContextMenuListener { + private static final String CONFIG_SHARE_VOICEMAIL_ALLOWED = "share_voicemail_allowed"; + + /** The root view of the call log list item */ + public final View rootView; + /** The quick contact badge for the contact. */ + public final QuickContactBadge quickContactView; + /** The primary action view of the entry. */ + public final View primaryActionView; + /** The details of the phone call. */ + public final PhoneCallDetailsViews phoneCallDetailsViews; + /** The text of the header for a day grouping. */ + public final TextView dayGroupHeader; + /** The view containing the details for the call log row, including the action buttons. */ + public final CardView callLogEntryView; + /** The actionable view which places a call to the number corresponding to the call log row. */ + public final ImageView primaryActionButtonView; + + private final Context mContext; + private final CallLogCache mCallLogCache; + private final CallLogListItemHelper mCallLogListItemHelper; + private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + private final OnClickListener mBlockReportListener; + private final int mPhotoSize; + /** Whether the data fields are populated by the worker thread, ready to be shown. */ + public boolean isLoaded; + /** The view containing call log item actions. Null until the ViewStub is inflated. */ + public View actionsView; + /** The button views below are assigned only when the action section is expanded. */ + public VoicemailPlaybackLayout voicemailPlaybackView; + + public View callButtonView; + public View videoCallButtonView; + public View createNewContactButtonView; + public View addToExistingContactButtonView; + public View sendMessageView; + public View blockReportView; + public View blockView; + public View unblockView; + public View reportNotSpamView; + public View detailsButtonView; + public View callWithNoteButtonView; + public View callComposeButtonView; + public View sendVoicemailButtonView; + public ImageView workIconView; + /** + * The row Id for the first call associated with the call log entry. Used as a key for the map + * used to track which call log entries have the action button section expanded. + */ + public long rowId; + /** + * The call Ids for the calls represented by the current call log entry. Used when the user + * deletes a call log entry. + */ + public long[] callIds; + /** + * The callable phone number for the current call log entry. Cached here as the call back intent + * is set only when the actions ViewStub is inflated. + */ + public String number; + /** The post-dial numbers that are dialed following the phone number. */ + public String postDialDigits; + /** The formatted phone number to display. */ + public String displayNumber; + /** + * The phone number presentation for the current call log entry. Cached here as the call back + * intent is set only when the actions ViewStub is inflated. + */ + public int numberPresentation; + /** The type of the phone number (e.g. main, work, etc). */ + public String numberType; + /** + * The country iso for the call. Cached here as the call back intent is set only when the actions + * ViewStub is inflated. + */ + public String countryIso; + /** + * The type of call for the current call log entry. Cached here as the call back intent is set + * only when the actions ViewStub is inflated. + */ + public int callType; + /** + * ID for blocked numbers database. Set when context menu is created, if the number is blocked. + */ + public Integer blockId; + /** + * The account for the current call log entry. Cached here as the call back intent is set only + * when the actions ViewStub is inflated. + */ + public PhoneAccountHandle accountHandle; + /** + * If the call has an associated voicemail message, the URI of the voicemail message for playback. + * Cached here as the voicemail intent is only set when the actions ViewStub is inflated. + */ + public String voicemailUri; + /** + * The name or number associated with the call. Cached here for use when setting content + * descriptions on buttons in the actions ViewStub when it is inflated. + */ + public CharSequence nameOrNumber; + /** + * The call type or Location associated with the call. Cached here for use when setting text for a + * voicemail log's call button + */ + public CharSequence callTypeOrLocation; + /** Whether this row is for a business or not. */ + public boolean isBusiness; + /** The contact info for the contact displayed in this list item. */ + public volatile ContactInfo info; + /** Whether spam feature is enabled, which affects UI. */ + public boolean isSpamFeatureEnabled; + /** Whether the current log entry is a spam number or not. */ + public boolean isSpam; + + public boolean isCallComposerCapable; + + private View.OnClickListener mExpandCollapseListener; + private boolean mVoicemailPrimaryActionButtonClicked; + + public int dayGroupHeaderVisibility; + public CharSequence dayGroupHeaderText; + public boolean isAttachedToWindow; + + public AsyncTask asyncTask; + + private CallLogListItemViewHolder( + Context context, + OnClickListener blockReportListener, + View.OnClickListener expandCollapseListener, + CallLogCache callLogCache, + CallLogListItemHelper callLogListItemHelper, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + View rootView, + QuickContactBadge quickContactView, + View primaryActionView, + PhoneCallDetailsViews phoneCallDetailsViews, + CardView callLogEntryView, + TextView dayGroupHeader, + ImageView primaryActionButtonView) { + super(rootView); + + mContext = context; + mExpandCollapseListener = expandCollapseListener; + mCallLogCache = callLogCache; + mCallLogListItemHelper = callLogListItemHelper; + mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; + mBlockReportListener = blockReportListener; + + this.rootView = rootView; + this.quickContactView = quickContactView; + this.primaryActionView = primaryActionView; + this.phoneCallDetailsViews = phoneCallDetailsViews; + this.callLogEntryView = callLogEntryView; + this.dayGroupHeader = dayGroupHeader; + this.primaryActionButtonView = primaryActionButtonView; + this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon); + mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); + + // Set text height to false on the TextViews so they don't have extra padding. + phoneCallDetailsViews.nameView.setElegantTextHeight(false); + phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false); + + quickContactView.setOverlay(null); + if (CompatUtils.hasPrioritizedMimeType()) { + quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + primaryActionButtonView.setOnClickListener(this); + primaryActionView.setOnClickListener(mExpandCollapseListener); + primaryActionView.setOnCreateContextMenuListener(this); + } + + public static CallLogListItemViewHolder create( + View view, + Context context, + OnClickListener blockReportListener, + View.OnClickListener expandCollapseListener, + CallLogCache callLogCache, + CallLogListItemHelper callLogListItemHelper, + VoicemailPlaybackPresenter voicemailPlaybackPresenter) { + + return new CallLogListItemViewHolder( + context, + blockReportListener, + expandCollapseListener, + callLogCache, + callLogListItemHelper, + voicemailPlaybackPresenter, + view, + (QuickContactBadge) view.findViewById(R.id.quick_contact_photo), + view.findViewById(R.id.primary_action_view), + PhoneCallDetailsViews.fromView(view), + (CardView) view.findViewById(R.id.call_log_row), + (TextView) view.findViewById(R.id.call_log_day_group_label), + (ImageView) view.findViewById(R.id.primary_action_button)); + } + + public static CallLogListItemViewHolder createForTest(Context context) { + Resources resources = context.getResources(); + CallLogCache callLogCache = CallLogCache.getCallLogCache(context); + PhoneCallDetailsHelper phoneCallDetailsHelper = + new PhoneCallDetailsHelper(context, resources, callLogCache); + + CallLogListItemViewHolder viewHolder = + new CallLogListItemViewHolder( + context, + null, + null /* expandCollapseListener */, + callLogCache, + new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache), + null /* voicemailPlaybackPresenter */, + new View(context), + new QuickContactBadge(context), + new View(context), + PhoneCallDetailsViews.createForTest(context), + new CardView(context), + new TextView(context), + new ImageView(context)); + viewHolder.detailsButtonView = new TextView(context); + viewHolder.actionsView = new View(context); + viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context); + viewHolder.workIconView = new ImageButton(context); + return viewHolder; + } + + @Override + public void onCreateContextMenu( + final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + if (TextUtils.isEmpty(number)) { + return; + } + + if (callType == CallLog.Calls.VOICEMAIL_TYPE) { + menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail)); + } else { + menu.setHeaderTitle( + PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR))); + } + + menu.add( + ContextMenu.NONE, + R.id.context_menu_copy_to_clipboard, + ContextMenu.NONE, + R.string.action_copy_number_text) + .setOnMenuItemClickListener(this); + + // The edit number before call does not show up if any of the conditions apply: + // 1) Number cannot be called + // 2) Number is the voicemail number + // 3) Number is a SIP address + + if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) + && !mCallLogCache.isVoicemailNumber(accountHandle, number) + && !PhoneNumberHelper.isSipNumber(number)) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_edit_before_call, + ContextMenu.NONE, + R.string.action_edit_number_before_call) + .setOnMenuItemClickListener(this); + } + + if (callType == CallLog.Calls.VOICEMAIL_TYPE + && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_copy_transcript_to_clipboard, + ContextMenu.NONE, + R.string.copy_transcript_text) + .setOnMenuItemClickListener(this); + } + + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + if (!isVoicemailNumber + && FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number) + && FilteredNumberCompat.canAttemptBlockOperations(mContext)) { + boolean isBlocked = blockId != null; + if (isBlocked) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_unblock, + ContextMenu.NONE, + R.string.call_log_action_unblock_number) + .setOnMenuItemClickListener(this); + } else { + if (isSpamFeatureEnabled) { + if (isSpam) { + menu.add( + ContextMenu.NONE, + R.id.context_menu_report_not_spam, + ContextMenu.NONE, + R.string.call_log_action_remove_spam) + .setOnMenuItemClickListener(this); + menu.add( + ContextMenu.NONE, + R.id.context_menu_block, + ContextMenu.NONE, + R.string.call_log_action_block_number) + .setOnMenuItemClickListener(this); + } else { + menu.add( + ContextMenu.NONE, + R.id.context_menu_block_report_spam, + ContextMenu.NONE, + R.string.call_log_action_block_report_number) + .setOnMenuItemClickListener(this); + } + } else { + menu.add( + ContextMenu.NONE, + R.id.context_menu_block, + ContextMenu.NONE, + R.string.call_log_action_block_number) + .setOnMenuItemClickListener(this); + } + } + } + + Logger.get(mContext).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) mContext); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + int resId = item.getItemId(); + if (resId == R.id.context_menu_copy_to_clipboard) { + ClipboardUtils.copyText(mContext, null, number, true); + return true; + } else if (resId == R.id.context_menu_copy_transcript_to_clipboard) { + ClipboardUtils.copyText( + mContext, null, phoneCallDetailsViews.voicemailTranscriptionView.getText(), true); + return true; + } else if (resId == R.id.context_menu_edit_before_call) { + final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number)); + intent.setClass(mContext, DialtactsActivity.class); + DialerUtils.startActivityWithErrorToast(mContext, intent); + return true; + } else if (resId == R.id.context_menu_block_report_spam) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlockReportSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (resId == R.id.context_menu_block) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlock( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (resId == R.id.context_menu_unblock) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER); + mBlockReportListener.onUnblock( + displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId); + } else if (resId == R.id.context_menu_report_not_spam) { + Logger.get(mContext) + .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM); + mBlockReportListener.onReportNotSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } + return false; + } + + /** + * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not inflated + * during initial binding, so click handlers, tags and accessibility text must be set here, if + * necessary. + */ + public void inflateActionViewStub() { + ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub); + if (stub != null) { + actionsView = stub.inflate(); + + voicemailPlaybackView = + (VoicemailPlaybackLayout) actionsView.findViewById(R.id.voicemail_playback_layout); + voicemailPlaybackView.setViewHolder(this); + + callButtonView = actionsView.findViewById(R.id.call_action); + callButtonView.setOnClickListener(this); + + videoCallButtonView = actionsView.findViewById(R.id.video_call_action); + videoCallButtonView.setOnClickListener(this); + + createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action); + createNewContactButtonView.setOnClickListener(this); + + addToExistingContactButtonView = + actionsView.findViewById(R.id.add_to_existing_contact_action); + addToExistingContactButtonView.setOnClickListener(this); + + sendMessageView = actionsView.findViewById(R.id.send_message_action); + sendMessageView.setOnClickListener(this); + + blockReportView = actionsView.findViewById(R.id.block_report_action); + blockReportView.setOnClickListener(this); + + blockView = actionsView.findViewById(R.id.block_action); + blockView.setOnClickListener(this); + + unblockView = actionsView.findViewById(R.id.unblock_action); + unblockView.setOnClickListener(this); + + reportNotSpamView = actionsView.findViewById(R.id.report_not_spam_action); + reportNotSpamView.setOnClickListener(this); + + detailsButtonView = actionsView.findViewById(R.id.details_action); + detailsButtonView.setOnClickListener(this); + + callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action); + callWithNoteButtonView.setOnClickListener(this); + + callComposeButtonView = actionsView.findViewById(R.id.call_compose_action); + callComposeButtonView.setOnClickListener(this); + + sendVoicemailButtonView = actionsView.findViewById(R.id.share_voicemail); + sendVoicemailButtonView.setOnClickListener(this); + } + } + + private void updatePrimaryActionButton(boolean isExpanded) { + + if (nameOrNumber == null) { + LogUtil.e("CallLogListItemViewHolder.updatePrimaryActionButton", "name or number is null"); + } + + // Calling expandTemplate with a null parameter will cause a NullPointerException. + CharSequence validNameOrNumber = nameOrNumber == null ? "" : nameOrNumber; + + if (!TextUtils.isEmpty(voicemailUri)) { + // Treat as voicemail list item; show play button if not expanded. + if (!isExpanded) { + primaryActionButtonView.setImageResource(R.drawable.ic_play_arrow_24dp); + primaryActionButtonView.setContentDescription( + TextUtils.expandTemplate( + mContext.getString(R.string.description_voicemail_action), validNameOrNumber)); + primaryActionButtonView.setVisibility(View.VISIBLE); + } else { + primaryActionButtonView.setVisibility(View.GONE); + } + } else { + // Treat as normal list item; show call button, if possible. + if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) { + boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + if (isVoicemailNumber) { + // Call to generic voicemail number, in case there are multiple accounts. + primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider()); + } else { + primaryActionButtonView.setTag( + IntentProvider.getReturnCallIntentProvider(number + postDialDigits)); + } + + primaryActionButtonView.setContentDescription( + TextUtils.expandTemplate( + mContext.getString(R.string.description_call_action), validNameOrNumber)); + primaryActionButtonView.setImageResource(R.drawable.ic_call_24dp); + primaryActionButtonView.setVisibility(View.VISIBLE); + } else { + primaryActionButtonView.setTag(null); + primaryActionButtonView.setVisibility(View.GONE); + } + } + } + + private static boolean isShareVoicemailAllowed(Context context) { + return ConfigProviderBindings.get(context).getBoolean(CONFIG_SHARE_VOICEMAIL_ALLOWED, true); + } + + /** + * Binds text titles, click handlers and intents to the voicemail, details and callback action + * buttons. + */ + private void bindActionButtons() { + boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation); + + if (isFullyUndialableVoicemail()) { + // Sometimes the voicemail server will report the message is from some non phone number + // source. If the number does not contains any dialable digit treat it as it is from a unknown + // number, remove all action buttons but still show the voicemail playback layout. + callButtonView.setVisibility(View.GONE); + videoCallButtonView.setVisibility(View.GONE); + detailsButtonView.setVisibility(View.GONE); + createNewContactButtonView.setVisibility(View.GONE); + addToExistingContactButtonView.setVisibility(View.GONE); + sendMessageView.setVisibility(View.GONE); + callWithNoteButtonView.setVisibility(View.GONE); + callComposeButtonView.setVisibility(View.GONE); + blockReportView.setVisibility(View.GONE); + blockView.setVisibility(View.GONE); + unblockView.setVisibility(View.GONE); + reportNotSpamView.setVisibility(View.GONE); + + if (isShareVoicemailAllowed(mContext)) { + sendVoicemailButtonView.setVisibility(View.VISIBLE); + } + voicemailPlaybackView.setVisibility(View.VISIBLE); + Uri uri = Uri.parse(voicemailUri); + mVoicemailPlaybackPresenter.setPlaybackView( + voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked); + mVoicemailPrimaryActionButtonClicked = false; + CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); + return; + } + + if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) { + callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number)); + ((TextView) callButtonView.findViewById(R.id.call_action_text)) + .setText( + TextUtils.expandTemplate( + mContext.getString(R.string.call_log_action_call), + nameOrNumber == null ? "" : nameOrNumber)); + TextView callTypeOrLocationView = + ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text)); + if (callType == Calls.VOICEMAIL_TYPE && !TextUtils.isEmpty(callTypeOrLocation)) { + callTypeOrLocationView.setText(callTypeOrLocation); + callTypeOrLocationView.setVisibility(View.VISIBLE); + } else { + callTypeOrLocationView.setVisibility(View.GONE); + } + callButtonView.setVisibility(View.VISIBLE); + } else { + callButtonView.setVisibility(View.GONE); + } + + if (shouldShowVideoCallActionButton(canPlaceCallToNumber)) { + videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); + videoCallButtonView.setVisibility(View.VISIBLE); + } else { + videoCallButtonView.setVisibility(View.GONE); + } + + // For voicemail calls, show the voicemail playback layout; hide otherwise. + if (callType == Calls.VOICEMAIL_TYPE + && mVoicemailPlaybackPresenter != null + && !TextUtils.isEmpty(voicemailUri)) { + voicemailPlaybackView.setVisibility(View.VISIBLE); + if (isShareVoicemailAllowed(mContext)) { + Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_VISIBLE); + sendVoicemailButtonView.setVisibility(View.VISIBLE); + } + + Uri uri = Uri.parse(voicemailUri); + mVoicemailPlaybackPresenter.setPlaybackView( + voicemailPlaybackView, rowId, uri, mVoicemailPrimaryActionButtonClicked); + mVoicemailPrimaryActionButtonClicked = false; + CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); + } else { + voicemailPlaybackView.setVisibility(View.GONE); + sendVoicemailButtonView.setVisibility(View.GONE); + } + + if (callType == Calls.VOICEMAIL_TYPE) { + detailsButtonView.setVisibility(View.GONE); + } else { + detailsButtonView.setVisibility(View.VISIBLE); + detailsButtonView.setTag(IntentProvider.getCallDetailIntentProvider(rowId, callIds, null)); + } + + boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam); + + if (!isBlockedOrSpam && info != null && UriUtils.isEncodedContactUri(info.lookupUri)) { + createNewContactButtonView.setTag( + IntentProvider.getAddContactIntentProvider( + info.lookupUri, info.name, info.number, info.type, true /* isNewContact */)); + createNewContactButtonView.setVisibility(View.VISIBLE); + + addToExistingContactButtonView.setTag( + IntentProvider.getAddContactIntentProvider( + info.lookupUri, info.name, info.number, info.type, false /* isNewContact */)); + addToExistingContactButtonView.setVisibility(View.VISIBLE); + } else { + createNewContactButtonView.setVisibility(View.GONE); + addToExistingContactButtonView.setVisibility(View.GONE); + } + + if (canPlaceCallToNumber && !isBlockedOrSpam) { + sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number)); + sendMessageView.setVisibility(View.VISIBLE); + } else { + sendMessageView.setVisibility(View.GONE); + } + + mCallLogListItemHelper.setActionContentDescriptions(this); + + boolean supportsCallSubject = mCallLogCache.doesAccountSupportCallSubject(accountHandle); + boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number); + callWithNoteButtonView.setVisibility( + supportsCallSubject && !isVoicemailNumber && info != null ? View.VISIBLE : View.GONE); + + callComposeButtonView.setVisibility(isCallComposerCapable ? View.VISIBLE : View.GONE); + + updateBlockReportActions(isVoicemailNumber); + } + + private boolean isFullyUndialableVoicemail() { + if (callType == Calls.VOICEMAIL_TYPE) { + if (!hasDialableChar(number)) { + return true; + } + } + return false; + } + + private static boolean hasDialableChar(CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + for (char c : number.toString().toCharArray()) { + if (PhoneNumberUtils.isDialable(c)) { + return true; + } + } + return false; + } + + private boolean shouldShowVideoCallActionButton(boolean canPlaceCallToNumber) { + return canPlaceCallToNumber && (hasPlacedVideoCall() || canSupportVideoCall()); + } + + private boolean hasPlacedVideoCall() { + return phoneCallDetailsViews.callTypeIcons.isVideoShown(); + } + + private boolean canSupportVideoCall() { + return mCallLogCache.canRelyOnVideoPresence() + && info != null + && (info.carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; + } + + /** + * Show or hide the action views, such as voicemail, details, and add contact. + * + *

If the action views have never been shown yet for this view, inflate the view stub. + */ + public void showActions(boolean show) { + showOrHideVoicemailTranscriptionView(show); + + if (show) { + if (!isLoaded) { + // b/31268128 for some unidentified reason showActions() can be called before the item is + // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will + // be called again once the item is loaded. + LogUtil.e( + "CallLogListItemViewHolder.showActions", + "called before item is loaded", + new Exception()); + return; + } + + // Inflate the view stub if necessary, and wire up the event handlers. + inflateActionViewStub(); + bindActionButtons(); + actionsView.setVisibility(View.VISIBLE); + actionsView.setAlpha(1.0f); + } else { + // When recycling a view, it is possible the actionsView ViewStub was previously + // inflated so we should hide it in this case. + if (actionsView != null) { + actionsView.setVisibility(View.GONE); + } + } + + updatePrimaryActionButton(show); + } + + public void showOrHideVoicemailTranscriptionView(boolean isExpanded) { + if (callType != Calls.VOICEMAIL_TYPE) { + return; + } + + final TextView view = phoneCallDetailsViews.voicemailTranscriptionView; + if (!isExpanded || TextUtils.isEmpty(view.getText())) { + view.setVisibility(View.GONE); + return; + } + view.setVisibility(View.VISIBLE); + } + + public void updatePhoto() { + quickContactView.assignContactUri(info.lookupUri); + + if (isSpamFeatureEnabled && isSpam) { + quickContactView.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); + return; + } + final boolean isVoicemail = mCallLogCache.isVoicemailNumber(accountHandle, number); + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemail) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + final String lookupKey = + info.lookupUri != null ? UriUtils.getLookupKeyFromUri(info.lookupUri) : null; + final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name; + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + + if (info.photoId == 0 && info.photoUri != null) { + ContactPhotoManager.getInstance(mContext) + .loadPhoto( + quickContactView, + info.photoUri, + mPhotoSize, + false /* darkTheme */, + true /* isCircular */, + request); + } else { + ContactPhotoManager.getInstance(mContext) + .loadThumbnail( + quickContactView, + info.photoId, + false /* darkTheme */, + true /* isCircular */, + request); + } + } + + @Override + public void onClick(View view) { + if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) { + Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_DIRECTLY); + mVoicemailPrimaryActionButtonClicked = true; + mExpandCollapseListener.onClick(primaryActionView); + } else if (view.getId() == R.id.call_with_note_action) { + CallSubjectDialog.start( + (Activity) mContext, + info.photoId, + info.photoUri, + info.lookupUri, + (String) nameOrNumber /* top line of contact view in call subject dialog */, + isBusiness, + number, + TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact + view in dialog. */ + numberType, /* phone number type (e.g. mobile) in second line of contact view */ + accountHandle); + } else if (view.getId() == R.id.block_report_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlockReportSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (view.getId() == R.id.block_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_NUMBER); + maybeShowBlockNumberMigrationDialog( + new BlockedNumbersMigrator.Listener() { + @Override + public void onComplete() { + mBlockReportListener.onBlock( + displayNumber, number, countryIso, callType, info.sourceType); + } + }); + } else if (view.getId() == R.id.unblock_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER); + mBlockReportListener.onUnblock( + displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId); + } else if (view.getId() == R.id.report_not_spam_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM); + mBlockReportListener.onReportNotSpam( + displayNumber, number, countryIso, callType, info.sourceType); + } else if (view.getId() == R.id.call_compose_action) { + LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed"); + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL); + CallComposerContact contact = new CallComposerContact(); + contact.photoId = info.photoId; + contact.photoUri = info.photoUri == null ? null : info.photoUri.toString(); + contact.contactUri = info.lookupUri == null ? null : info.lookupUri.toString(); + contact.nameOrNumber = (String) nameOrNumber; + contact.isBusiness = isBusiness; + contact.number = number; + /* second line of contact view. */ + contact.displayNumber = TextUtils.isEmpty(info.name) ? null : displayNumber; + /* phone number type (e.g. mobile) in second line of contact view */ + contact.numberLabel = numberType; + Activity activity = (Activity) mContext; + activity.startActivityForResult( + CallComposerActivity.newIntent(activity, contact), + DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE); + } else if (view.getId() == R.id.share_voicemail) { + Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED); + mVoicemailPlaybackPresenter.shareVoicemail(); + } else { + logCallLogAction(view.getId()); + final IntentProvider intentProvider = (IntentProvider) view.getTag(); + if (intentProvider != null) { + final Intent intent = intentProvider.getIntent(mContext); + // See IntentProvider.getCallDetailIntentProvider() for why this may be null. + if (intent != null) { + DialerUtils.startActivityWithErrorToast(mContext, intent); + } + } + } + } + + private void logCallLogAction(int id) { + if (id == R.id.send_message_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE); + } else if (id == R.id.add_to_existing_contact_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT); + } else if (id == R.id.create_new_contact_action) { + Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT); + } + } + + private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) { + if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog( + mContext, ((Activity) mContext).getFragmentManager(), listener)) { + listener.onComplete(); + } + } + + private void updateBlockReportActions(boolean isVoicemailNumber) { + // Set block/spam actions. + blockReportView.setVisibility(View.GONE); + blockView.setVisibility(View.GONE); + unblockView.setVisibility(View.GONE); + reportNotSpamView.setVisibility(View.GONE); + String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); + if (isVoicemailNumber + || !FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number) + || !FilteredNumberCompat.canAttemptBlockOperations(mContext)) { + return; + } + boolean isBlocked = blockId != null; + if (isBlocked) { + unblockView.setVisibility(View.VISIBLE); + } else { + if (isSpamFeatureEnabled) { + if (isSpam) { + blockView.setVisibility(View.VISIBLE); + reportNotSpamView.setVisibility(View.VISIBLE); + } else { + blockReportView.setVisibility(View.VISIBLE); + } + } else { + blockView.setVisibility(View.VISIBLE); + } + } + } + + public interface OnClickListener { + + void onBlockReportSpam( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType); + + void onBlock( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType); + + void onUnblock( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType, + boolean isSpam, + Integer blockId); + + void onReportNotSpam( + String displayNumber, + String number, + String countryIso, + int callType, + int contactSourceType); + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java b/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java new file mode 100644 index 000000000..9de260a0a --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogModalAlertManager.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.app.alert.AlertManager; + +/** + * Alert manager controls modal view to show message in call log. When modal view is shown, regular + * call log will be hidden. + */ +public class CallLogModalAlertManager implements AlertManager { + + interface Listener { + void onShowModalAlert(boolean show); + } + + private final Listener listener; + private final ViewGroup parent; + private final ViewGroup container; + private final LayoutInflater inflater; + + public CallLogModalAlertManager(LayoutInflater inflater, ViewGroup parent, Listener listener) { + this.inflater = inflater; + this.parent = parent; + this.listener = listener; + container = (ViewGroup) parent.findViewById(R.id.modal_message_container); + } + + @Override + public View inflate(int layoutId) { + return inflater.inflate(layoutId, parent, false); + } + + @Override + public void add(View view) { + if (contains(view)) { + return; + } + container.addView(view); + listener.onShowModalAlert(true); + } + + @Override + public void clear() { + container.removeAllViews(); + listener.onShowModalAlert(false); + } + + public boolean isEmpty() { + return container.getChildCount() == 0; + } + + public boolean contains(View view) { + return container.indexOfChild(view) != -1; + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java b/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java new file mode 100644 index 000000000..8f664d1a4 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java @@ -0,0 +1,299 @@ +/* + * 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.dialer.app.calllog; + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.R; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; +import java.util.List; + +/** Helper class operating on call log notifications. */ +public class CallLogNotificationsHelper { + + private static final String TAG = "CallLogNotifHelper"; + private static CallLogNotificationsHelper sInstance; + private final Context mContext; + private final NewCallsQuery mNewCallsQuery; + private final ContactInfoHelper mContactInfoHelper; + private final String mCurrentCountryIso; + + CallLogNotificationsHelper( + Context context, + NewCallsQuery newCallsQuery, + ContactInfoHelper contactInfoHelper, + String countryIso) { + mContext = context; + mNewCallsQuery = newCallsQuery; + mContactInfoHelper = contactInfoHelper; + mCurrentCountryIso = countryIso; + } + + /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */ + public static CallLogNotificationsHelper getInstance(Context context) { + if (sInstance == null) { + ContentResolver contentResolver = context.getContentResolver(); + String countryIso = GeoUtil.getCurrentCountryIso(context); + sInstance = + new CallLogNotificationsHelper( + context, + createNewCallsQuery(context, contentResolver), + new ContactInfoHelper(context, countryIso), + countryIso); + } + return sInstance; + } + + /** Removes the missed call notifications. */ + public static void removeMissedCallNotifications(Context context) { + TelecomUtil.cancelMissedCallsNotification(context); + } + + /** Update the voice mail notifications. */ + public static void updateVoicemailNotifications(Context context) { + CallLogNotificationsService.updateVoicemailNotifications(context, null); + } + + /** Create a new instance of {@link NewCallsQuery}. */ + public static NewCallsQuery createNewCallsQuery( + Context context, ContentResolver contentResolver) { + + return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver); + } + + /** + * Get all voicemails with the "new" flag set to 1. + * + * @return A list of NewCall objects where each object represents a new voicemail. + */ + @Nullable + public List getNewVoicemails() { + return mNewCallsQuery.query(Calls.VOICEMAIL_TYPE); + } + + /** + * Get all missed calls with the "new" flag set to 1. + * + * @return A list of NewCall objects where each object represents a new missed call. + */ + @Nullable + public List getNewMissedCalls() { + return mNewCallsQuery.query(Calls.MISSED_TYPE); + } + + /** + * Given a number and number information (presentation and country ISO), get the best name for + * display. If the name is empty but we have a special presentation, display that. Otherwise + * attempt to look it up in the database or the cache. If that fails, fall back to displaying the + * number. + */ + public String getName( + @Nullable String number, int numberPresentation, @Nullable String countryIso) { + return getContactInfo(number, numberPresentation, countryIso).name; + } + + /** + * Given a number and number information (presentation and country ISO), get {@link ContactInfo}. + * If the name is empty but we have a special presentation, display that. Otherwise attempt to + * look it up in the cache. If that fails, fall back to displaying the number. + */ + public ContactInfo getContactInfo( + @Nullable String number, int numberPresentation, @Nullable String countryIso) { + if (countryIso == null) { + countryIso = mCurrentCountryIso; + } + + number = (number == null) ? "" : number; + ContactInfo contactInfo = new ContactInfo(); + contactInfo.number = number; + contactInfo.formattedNumber = PhoneNumberUtils.formatNumber(number, countryIso); + // contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo. + contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); + + // 1. Special number representation. + contactInfo.name = + PhoneNumberDisplayUtil.getDisplayName(mContext, number, numberPresentation, false) + .toString(); + if (!TextUtils.isEmpty(contactInfo.name)) { + return contactInfo; + } + + // 2. Look it up in the cache. + ContactInfo cachedContactInfo = mContactInfoHelper.lookupNumber(number, countryIso); + + if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) { + return cachedContactInfo; + } + + if (!TextUtils.isEmpty(contactInfo.formattedNumber)) { + // 3. If we cannot lookup the contact, use the formatted number instead. + contactInfo.name = contactInfo.formattedNumber; + } else if (!TextUtils.isEmpty(number)) { + // 4. If number can't be formatted, use number. + contactInfo.name = number; + } else { + // 5. Otherwise, it's unknown number. + contactInfo.name = mContext.getResources().getString(R.string.unknown); + } + return contactInfo; + } + + /** Allows determining the new calls for which a notification should be generated. */ + public interface NewCallsQuery { + + /** Returns the new calls of a certain type for which a notification should be generated. */ + @Nullable + List query(int type); + } + + /** Information about a new voicemail. */ + public static final class NewCall { + + public final Uri callsUri; + public final Uri voicemailUri; + public final String number; + public final int numberPresentation; + public final String accountComponentName; + public final String accountId; + public final String transcription; + public final String countryIso; + public final long dateMs; + + public NewCall( + Uri callsUri, + Uri voicemailUri, + String number, + int numberPresentation, + String accountComponentName, + String accountId, + String transcription, + String countryIso, + long dateMs) { + this.callsUri = callsUri; + this.voicemailUri = voicemailUri; + this.number = number; + this.numberPresentation = numberPresentation; + this.accountComponentName = accountComponentName; + this.accountId = accountId; + this.transcription = transcription; + this.countryIso = countryIso; + this.dateMs = dateMs; + } + } + + /** + * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to notify + * about in the call log. + */ + private static final class DefaultNewCallsQuery implements NewCallsQuery { + + private static final String[] PROJECTION = { + Calls._ID, + Calls.NUMBER, + Calls.VOICEMAIL_URI, + Calls.NUMBER_PRESENTATION, + Calls.PHONE_ACCOUNT_COMPONENT_NAME, + Calls.PHONE_ACCOUNT_ID, + Calls.TRANSCRIPTION, + Calls.COUNTRY_ISO, + Calls.DATE + }; + private static final int ID_COLUMN_INDEX = 0; + private static final int NUMBER_COLUMN_INDEX = 1; + private static final int VOICEMAIL_URI_COLUMN_INDEX = 2; + private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3; + private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4; + private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5; + private static final int TRANSCRIPTION_COLUMN_INDEX = 6; + private static final int COUNTRY_ISO_COLUMN_INDEX = 7; + private static final int DATE_COLUMN_INDEX = 8; + + private final ContentResolver mContentResolver; + private final Context mContext; + + private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) { + mContext = context; + mContentResolver = contentResolver; + } + + @Override + @Nullable + @TargetApi(VERSION_CODES.M) + public List query(int type) { + if (!PermissionsUtil.hasPermission(mContext, Manifest.permission.READ_CALL_LOG)) { + Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup."); + return null; + } + final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); + final String[] selectionArgs = new String[] {Integer.toString(type)}; + try (Cursor cursor = + mContentResolver.query( + Calls.CONTENT_URI_WITH_VOICEMAIL, + PROJECTION, + selection, + selectionArgs, + Calls.DEFAULT_SORT_ORDER)) { + if (cursor == null) { + return null; + } + List newCalls = new ArrayList<>(); + while (cursor.moveToNext()) { + newCalls.add(createNewCallsFromCursor(cursor)); + } + return newCalls; + } catch (RuntimeException e) { + Log.w(TAG, "Exception when querying Contacts Provider for calls lookup"); + return null; + } + } + + /** Returns an instance of {@link NewCall} created by using the values of the cursor. */ + private NewCall createNewCallsFromCursor(Cursor cursor) { + String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX); + Uri callsUri = + ContentUris.withAppendedId( + Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX)); + Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString); + return new NewCall( + callsUri, + voicemailUri, + cursor.getString(NUMBER_COLUMN_INDEX), + cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX), + cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX), + cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX), + cursor.getString(TRANSCRIPTION_COLUMN_INDEX), + cursor.getString(COUNTRY_ISO_COLUMN_INDEX), + cursor.getLong(DATE_COLUMN_INDEX)); + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogNotificationsService.java b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java new file mode 100644 index 000000000..820528126 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogNotificationsService.java @@ -0,0 +1,203 @@ +/* + * 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.dialer.app.calllog; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import com.android.dialer.common.LogUtil; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.PermissionsUtil; +import me.leolin.shortcutbadger.ShortcutBadger; + +/** + * Provides operations for managing call-related notifications. + * + *

It handles the following actions: + * + *

    + *
  • Updating voicemail notifications + *
  • Marking new voicemails as old + *
  • Updating missed call notifications + *
  • Marking new missed calls as old + *
  • Calling back from a missed call + *
  • Sending an SMS from a missed call + *
+ */ +public class CallLogNotificationsService extends IntentService { + + /** Action to mark all the new voicemails as old. */ + public static final String ACTION_MARK_NEW_VOICEMAILS_AS_OLD = + "com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD"; + /** + * Action to update voicemail notifications. + * + *

May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}. + */ + public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS = + "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS"; + /** + * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new + * voicemail that triggered an update. + * + *

It must be a {@link Uri}. + */ + public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI"; + /** + * Action to update the missed call notifications. + * + *

Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and {@link + * #EXTRA_MISSED_CALL_COUNT}. + */ + public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS = + "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS"; + /** Action to mark all the new missed calls as old. */ + public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD = + "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD"; + /** Action to call back a missed call. */ + public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION = + "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION"; + + public static final String ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION = + "com.android.dialer.calllog.SEND_SMS_FROM_MISSED_CALL_NOTIFICATION"; + /** + * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS}, {@link + * #ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION} and {@link + * #ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION} to identify the number to display, call or + * text back. + * + *

It must be a {@link String}. + */ + public static final String EXTRA_MISSED_CALL_NUMBER = "MISSED_CALL_NUMBER"; + /** + * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS} to represent the + * number of missed calls. + * + *

It must be a {@link Integer} + */ + public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT"; + + public static final int UNKNOWN_MISSED_CALL_COUNT = -1; + private VoicemailQueryHandler mVoicemailQueryHandler; + + public CallLogNotificationsService() { + super("CallLogNotificationsService"); + } + + /** + * Updates notifications for any new voicemails. + * + * @param context a valid context. + * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code + * null}, then notifications for all new voicemails will be updated. + */ + public static void updateVoicemailNotifications(Context context, Uri voicemailUri) { + if (!TelecomUtil.isDefaultDialer(context)) { + LogUtil.i( + "CallLogNotificationsService.updateVoicemailNotifications", + "not default dialer, ignoring voicemail notifications"); + return; + } + if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); + // If voicemailUri is null, then notifications for all voicemails will be updated. + if (voicemailUri != null) { + serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri); + } + context.startService(serviceIntent); + } + } + + /** + * Updates notifications for any new missed calls. + * + * @param context A valid context. + * @param count The number of new missed calls. + * @param number The phone number of the newest missed call. + */ + public static void updateMissedCallNotifications(Context context, int count, String number) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS); + serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count); + serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number); + context.startService(serviceIntent); + } + + public static void markNewVoicemailsAsOld(Context context) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + context.startService(serviceIntent); + } + + public static boolean updateBadgeCount(Context context, int count) { + boolean success = ShortcutBadger.applyCount(context, count); + LogUtil.i( + "CallLogNotificationsService.updateBadgeCount", + "update badge count: %d success: %b", + count, + success); + return success; + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle null intent"); + return; + } + + if (!PermissionsUtil.hasPermission(this, android.Manifest.permission.READ_CALL_LOG)) { + return; + } + + String action = intent.getAction(); + switch (action) { + case ACTION_MARK_NEW_VOICEMAILS_AS_OLD: + if (mVoicemailQueryHandler == null) { + mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver()); + } + mVoicemailQueryHandler.markNewVoicemailsAsOld(); + break; + case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS: + Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI); + DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri); + break; + case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS: + int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT); + String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER); + MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number); + updateBadgeCount(this, count); + break; + case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD: + CallLogNotificationsHelper.removeMissedCallNotifications(this); + break; + case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION: + MissedCallNotifier.getInstance(this) + .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + break; + case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION: + MissedCallNotifier.getInstance(this) + .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + break; + default: + LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent); + break; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java new file mode 100644 index 000000000..a781b0887 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java @@ -0,0 +1,77 @@ +/* + * 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.dialer.app.calllog; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.provider.VoicemailContract; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; + +/** + * Receiver for call log events. + * + *

It is currently used to handle {@link VoicemailContract#ACTION_NEW_VOICEMAIL} and {@link + * Intent#ACTION_BOOT_COMPLETED}. + */ +public class CallLogReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) { + checkVoicemailStatus(context); + CallLogNotificationsService.updateVoicemailNotifications(context, intent.getData()); + } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) { + CallLogNotificationsService.updateVoicemailNotifications(context, null); + } else { + LogUtil.w("CallLogReceiver.onReceive", "could not handle: " + intent); + } + } + + private static void checkVoicemailStatus(Context context) { + new CallLogQueryHandler( + context, + context.getContentResolver(), + new CallLogQueryHandler.Listener() { + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { + VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus( + context, statusCursor, Source.Notification); + } + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + // Do nothing + } + + @Override + public boolean onCallsFetched(Cursor combinedCursor) { + return false; + } + }) + .fetchVoicemailStatus(); + } +} diff --git a/java/com/android/dialer/app/calllog/CallTypeHelper.java b/java/com/android/dialer/app/calllog/CallTypeHelper.java new file mode 100644 index 000000000..f3c27a1ac --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallTypeHelper.java @@ -0,0 +1,136 @@ +/* + * 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.dialer.app.calllog; + +import android.content.res.Resources; +import com.android.dialer.app.R; +import com.android.dialer.compat.AppCompatConstants; + +/** Helper class to perform operations related to call types. */ +public class CallTypeHelper { + + /** Name used to identify incoming calls. */ + private final CharSequence mIncomingName; + /** Name used to identify incoming calls which were transferred to another device. */ + private final CharSequence mIncomingPulledName; + /** Name used to identify outgoing calls. */ + private final CharSequence mOutgoingName; + /** Name used to identify outgoing calls which were transferred to another device. */ + private final CharSequence mOutgoingPulledName; + /** Name used to identify missed calls. */ + private final CharSequence mMissedName; + /** Name used to identify incoming video calls. */ + private final CharSequence mIncomingVideoName; + /** Name used to identify incoming video calls which were transferred to another device. */ + private final CharSequence mIncomingVideoPulledName; + /** Name used to identify outgoing video calls. */ + private final CharSequence mOutgoingVideoName; + /** Name used to identify outgoing video calls which were transferred to another device. */ + private final CharSequence mOutgoingVideoPulledName; + /** Name used to identify missed video calls. */ + private final CharSequence mMissedVideoName; + /** Name used to identify voicemail calls. */ + private final CharSequence mVoicemailName; + /** Name used to identify rejected calls. */ + private final CharSequence mRejectedName; + /** Name used to identify blocked calls. */ + private final CharSequence mBlockedName; + /** Name used to identify calls which were answered on another device. */ + private final CharSequence mAnsweredElsewhereName; + + public CallTypeHelper(Resources resources) { + // Cache these values so that we do not need to look them up each time. + mIncomingName = resources.getString(R.string.type_incoming); + mIncomingPulledName = resources.getString(R.string.type_incoming_pulled); + mOutgoingName = resources.getString(R.string.type_outgoing); + mOutgoingPulledName = resources.getString(R.string.type_outgoing_pulled); + mMissedName = resources.getString(R.string.type_missed); + mIncomingVideoName = resources.getString(R.string.type_incoming_video); + mIncomingVideoPulledName = resources.getString(R.string.type_incoming_video_pulled); + mOutgoingVideoName = resources.getString(R.string.type_outgoing_video); + mOutgoingVideoPulledName = resources.getString(R.string.type_outgoing_video_pulled); + mMissedVideoName = resources.getString(R.string.type_missed_video); + mVoicemailName = resources.getString(R.string.type_voicemail); + mRejectedName = resources.getString(R.string.type_rejected); + mBlockedName = resources.getString(R.string.type_blocked); + mAnsweredElsewhereName = resources.getString(R.string.type_answered_elsewhere); + } + + public static boolean isMissedCallType(int callType) { + return (callType != AppCompatConstants.CALLS_INCOMING_TYPE + && callType != AppCompatConstants.CALLS_OUTGOING_TYPE + && callType != AppCompatConstants.CALLS_VOICEMAIL_TYPE + && callType != AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE); + } + + /** Returns the text used to represent the given call type. */ + public CharSequence getCallTypeText(int callType, boolean isVideoCall, boolean isPulledCall) { + switch (callType) { + case AppCompatConstants.CALLS_INCOMING_TYPE: + if (isVideoCall) { + if (isPulledCall) { + return mIncomingVideoPulledName; + } else { + return mIncomingVideoName; + } + } else { + if (isPulledCall) { + return mIncomingPulledName; + } else { + return mIncomingName; + } + } + + case AppCompatConstants.CALLS_OUTGOING_TYPE: + if (isVideoCall) { + if (isPulledCall) { + return mOutgoingVideoPulledName; + } else { + return mOutgoingVideoName; + } + } else { + if (isPulledCall) { + return mOutgoingPulledName; + } else { + return mOutgoingName; + } + } + + case AppCompatConstants.CALLS_MISSED_TYPE: + if (isVideoCall) { + return mMissedVideoName; + } else { + return mMissedName; + } + + case AppCompatConstants.CALLS_VOICEMAIL_TYPE: + return mVoicemailName; + + case AppCompatConstants.CALLS_REJECTED_TYPE: + return mRejectedName; + + case AppCompatConstants.CALLS_BLOCKED_TYPE: + return mBlockedName; + + case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: + return mAnsweredElsewhereName; + + default: + return mMissedName; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallTypeIconsView.java b/java/com/android/dialer/app/calllog/CallTypeIconsView.java new file mode 100644 index 000000000..cd5c5460c --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallTypeIconsView.java @@ -0,0 +1,221 @@ +/* + * 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.dialer.app.calllog; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.PorterDuff; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import com.android.contacts.common.util.BitmapUtil; +import com.android.dialer.app.R; +import com.android.dialer.compat.AppCompatConstants; +import java.util.ArrayList; +import java.util.List; + +/** + * View that draws one or more symbols for different types of calls (missed calls, outgoing etc). + * The symbols are set up horizontally. As this view doesn't create subviews, it is better suited + * for ListView-recycling that a regular LinearLayout using ImageViews. + */ +public class CallTypeIconsView extends View { + + private static Resources sResources; + private List mCallTypes = new ArrayList<>(3); + private boolean mShowVideo = false; + private int mWidth; + private int mHeight; + + public CallTypeIconsView(Context context) { + this(context, null); + } + + public CallTypeIconsView(Context context, AttributeSet attrs) { + super(context, attrs); + if (sResources == null) { + sResources = new Resources(context); + } + } + + public void clear() { + mCallTypes.clear(); + mWidth = 0; + mHeight = 0; + invalidate(); + } + + public void add(int callType) { + mCallTypes.add(callType); + + final Drawable drawable = getCallTypeDrawable(callType); + mWidth += drawable.getIntrinsicWidth() + sResources.iconMargin; + mHeight = Math.max(mHeight, drawable.getIntrinsicHeight()); + invalidate(); + } + + /** + * Determines whether the video call icon will be shown. + * + * @param showVideo True where the video icon should be shown. + */ + public void setShowVideo(boolean showVideo) { + mShowVideo = showVideo; + if (showVideo) { + mWidth += sResources.videoCall.getIntrinsicWidth(); + mHeight = Math.max(mHeight, sResources.videoCall.getIntrinsicHeight()); + invalidate(); + } + } + + /** + * Determines if the video icon should be shown. + * + * @return True if the video icon should be shown. + */ + public boolean isVideoShown() { + return mShowVideo; + } + + public int getCount() { + return mCallTypes.size(); + } + + public int getCallType(int index) { + return mCallTypes.get(index); + } + + private Drawable getCallTypeDrawable(int callType) { + switch (callType) { + case AppCompatConstants.CALLS_INCOMING_TYPE: + case AppCompatConstants.CALLS_ANSWERED_EXTERNALLY_TYPE: + return sResources.incoming; + case AppCompatConstants.CALLS_OUTGOING_TYPE: + return sResources.outgoing; + case AppCompatConstants.CALLS_MISSED_TYPE: + return sResources.missed; + case AppCompatConstants.CALLS_VOICEMAIL_TYPE: + return sResources.voicemail; + case AppCompatConstants.CALLS_BLOCKED_TYPE: + return sResources.blocked; + default: + // It is possible for users to end up with calls with unknown call types in their + // call history, possibly due to 3rd party call log implementations (e.g. to + // distinguish between rejected and missed calls). Instead of crashing, just + // assume that all unknown call types are missed calls. + return sResources.missed; + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mWidth, mHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + int left = 0; + for (Integer callType : mCallTypes) { + final Drawable drawable = getCallTypeDrawable(callType); + final int right = left + drawable.getIntrinsicWidth(); + drawable.setBounds(left, 0, right, drawable.getIntrinsicHeight()); + drawable.draw(canvas); + left = right + sResources.iconMargin; + } + + // If showing the video call icon, draw it scaled appropriately. + if (mShowVideo) { + final Drawable drawable = sResources.videoCall; + final int right = left + sResources.videoCall.getIntrinsicWidth(); + drawable.setBounds(left, 0, right, sResources.videoCall.getIntrinsicHeight()); + drawable.draw(canvas); + } + } + + private static class Resources { + + // Drawable representing an incoming answered call. + public final Drawable incoming; + + // Drawable respresenting an outgoing call. + public final Drawable outgoing; + + // Drawable representing an incoming missed call. + public final Drawable missed; + + // Drawable representing a voicemail. + public final Drawable voicemail; + + // Drawable representing a blocked call. + public final Drawable blocked; + + // Drawable repesenting a video call. + public final Drawable videoCall; + + /** The margin to use for icons. */ + public final int iconMargin; + + /** + * Configures the call icon drawables. A single white call arrow which points down and left is + * used as a basis for all of the call arrow icons, applying rotation and colors as needed. + * + * @param context The current context. + */ + public Resources(Context context) { + final android.content.res.Resources r = context.getResources(); + + incoming = r.getDrawable(R.drawable.ic_call_arrow); + incoming.setColorFilter(r.getColor(R.color.answered_call), PorterDuff.Mode.MULTIPLY); + + // Create a rotated instance of the call arrow for outgoing calls. + outgoing = BitmapUtil.getRotatedDrawable(r, R.drawable.ic_call_arrow, 180f); + outgoing.setColorFilter(r.getColor(R.color.answered_call), PorterDuff.Mode.MULTIPLY); + + // Need to make a copy of the arrow drawable, otherwise the same instance colored + // above will be recolored here. + missed = r.getDrawable(R.drawable.ic_call_arrow).mutate(); + missed.setColorFilter(r.getColor(R.color.missed_call), PorterDuff.Mode.MULTIPLY); + + voicemail = r.getDrawable(R.drawable.quantum_ic_voicemail_white_18); + voicemail.setColorFilter( + r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY); + + blocked = getScaledBitmap(context, R.drawable.ic_block_24dp); + blocked.setColorFilter(r.getColor(R.color.blocked_call), PorterDuff.Mode.MULTIPLY); + + videoCall = getScaledBitmap(context, R.drawable.quantum_ic_videocam_white_24); + videoCall.setColorFilter( + r.getColor(R.color.dialer_secondary_text_color), PorterDuff.Mode.MULTIPLY); + + iconMargin = r.getDimensionPixelSize(R.dimen.call_log_icon_margin); + } + + // Gets the icon, scaled to the height of the call type icons. This helps display all the + // icons to be the same height, while preserving their width aspect ratio. + private Drawable getScaledBitmap(Context context, int resourceId) { + Bitmap icon = BitmapFactory.decodeResource(context.getResources(), resourceId); + int scaledHeight = context.getResources().getDimensionPixelSize(R.dimen.call_type_icon_size); + int scaledWidth = + (int) ((float) icon.getWidth() * ((float) scaledHeight / (float) icon.getHeight())); + Bitmap scaledIcon = Bitmap.createScaledBitmap(icon, scaledWidth, scaledHeight, false); + return new BitmapDrawable(context.getResources(), scaledIcon); + } + } +} diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java new file mode 100644 index 000000000..0c9bd4b35 --- /dev/null +++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java @@ -0,0 +1,98 @@ +/* + * 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.dialer.app.calllog; + +import android.app.Activity; +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.CallLog.Calls; +import com.android.dialer.app.R; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.PhoneNumberCache; + +/** Dialog that clears the call log after confirming with the user */ +public class ClearCallLogDialog extends DialogFragment { + + /** Preferred way to show this dialog */ + public static void show(FragmentManager fragmentManager) { + ClearCallLogDialog dialog = new ClearCallLogDialog(); + dialog.show(fragmentManager, "deleteCallLog"); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final ContentResolver resolver = getActivity().getContentResolver(); + final Context context = getActivity().getApplicationContext(); + final OnClickListener okListener = + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final ProgressDialog progressDialog = + ProgressDialog.show( + getActivity(), getString(R.string.clearCallLogProgress_title), "", true, false); + progressDialog.setOwnerActivity(getActivity()); + final AsyncTask task = + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + resolver.delete(Calls.CONTENT_URI, null, null); + CachedNumberLookupService cachedNumberLookupService = + PhoneNumberCache.get(context).getCachedNumberLookupService(); + if (cachedNumberLookupService != null) { + cachedNumberLookupService.clearAllCacheEntries(context); + } + return null; + } + + @Override + protected void onPostExecute(Void result) { + final Activity activity = progressDialog.getOwnerActivity(); + + if (activity == null || activity.isDestroyed() || activity.isFinishing()) { + return; + } + + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); + } + } + }; + // TODO: Once we have the API, we should configure this ProgressDialog + // to only show up after a certain time (e.g. 150ms) + progressDialog.show(); + task.execute(); + } + }; + return new AlertDialog.Builder(getActivity()) + .setTitle(R.string.clearCallLogConfirmation_title) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setMessage(R.string.clearCallLogConfirmation) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, okListener) + .setCancelable(true) + .create(); + } +} diff --git a/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java new file mode 100644 index 000000000..651a0ccb8 --- /dev/null +++ b/java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java @@ -0,0 +1,273 @@ +/* + * 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.dialer.app.calllog; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.v4.util.Pair; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import com.android.contacts.common.compat.TelephonyManagerCompat; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.telecom.TelecomUtil; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** Shows a voicemail notification in the status bar. */ +public class DefaultVoicemailNotifier { + + public static final String TAG = "VoicemailNotifier"; + + /** The tag used to identify notifications from this class. */ + private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier"; + /** The identifier of the notification of new voicemails. */ + private static final int NOTIFICATION_ID = 1; + + /** The singleton instance of {@link DefaultVoicemailNotifier}. */ + private static DefaultVoicemailNotifier sInstance; + + private final Context mContext; + + private DefaultVoicemailNotifier(Context context) { + mContext = context; + } + + /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */ + public static DefaultVoicemailNotifier getInstance(Context context) { + if (sInstance == null) { + ContentResolver contentResolver = context.getContentResolver(); + sInstance = new DefaultVoicemailNotifier(context); + } + return sInstance; + } + + /** + * Updates the notification and notifies of the call with the given URI. + * + *

Clears the notification if there are no new voicemails, and notifies if the given URI + * corresponds to a new voicemail. + * + *

It is not safe to call this method from the main thread. + */ + public void updateNotification(Uri newCallUri) { + // Lookup the list of new voicemails to include in the notification. + // TODO: Move this into a service, to avoid holding the receiver up. + final List newCalls = + CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails(); + + if (newCalls == null) { + // Query failed, just return. + return; + } + + if (newCalls.isEmpty()) { + // No voicemails to notify about: clear the notification. + getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID); + return; + } + + Resources resources = mContext.getResources(); + + // This represents a list of names to include in the notification. + String callers = null; + + // Maps each number into a name: if a number is in the map, it has already left a more + // recent voicemail. + final Map names = new ArrayMap<>(); + + // Determine the call corresponding to the new voicemail we have to notify about. + NewCall callToNotify = null; + + // Iterate over the new voicemails to determine all the information above. + Iterator itr = newCalls.iterator(); + while (itr.hasNext()) { + NewCall newCall = itr.next(); + + // Skip notifying for numbers which are blocked. + if (FilteredNumbersUtil.shouldBlockVoicemail( + mContext, newCall.number, newCall.countryIso, newCall.dateMs)) { + itr.remove(); + + // Delete the voicemail. + mContext.getContentResolver().delete(newCall.voicemailUri, null, null); + continue; + } + + // Check if we already know the name associated with this number. + String name = names.get(newCall.number); + if (name == null) { + name = + CallLogNotificationsHelper.getInstance(mContext) + .getName(newCall.number, newCall.numberPresentation, newCall.countryIso); + names.put(newCall.number, name); + // This is a new caller. Add it to the back of the list of callers. + if (TextUtils.isEmpty(callers)) { + callers = name; + } else { + callers = + resources.getString(R.string.notification_voicemail_callers_list, callers, name); + } + } + // Check if this is the new call we need to notify about. + if (newCallUri != null + && newCall.voicemailUri != null + && ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) { + callToNotify = newCall; + } + } + + // All the potential new voicemails have been removed, e.g. if they were spam. + if (newCalls.isEmpty()) { + return; + } + + // If there is only one voicemail, set its transcription as the "long text". + String transcription = null; + if (newCalls.size() == 1) { + transcription = newCalls.get(0).transcription; + } + + if (newCallUri != null && callToNotify == null) { + Log.e(TAG, "The new call could not be found in the call log: " + newCallUri); + } + + // Determine the title of the notification and the icon for it. + final String title = + resources.getQuantityString( + R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()); + // TODO: Use the photo of contact if all calls are from the same person. + final int icon = android.R.drawable.stat_notify_voicemail; + + Pair info = getNotificationInfo(callToNotify); + + Notification.Builder notificationBuilder = + new Notification.Builder(mContext) + .setSmallIcon(icon) + .setContentTitle(title) + .setContentText(callers) + .setColor(resources.getColor(R.color.dialer_theme_color)) + .setSound(info.first) + .setDefaults(info.second) + .setDeleteIntent(createMarkNewVoicemailsAsOldIntent()) + .setAutoCancel(true); + + if (!TextUtils.isEmpty(transcription)) { + notificationBuilder.setStyle(new Notification.BigTextStyle().bigText(transcription)); + } + + // Determine the intent to fire when the notification is clicked on. + final Intent contentIntent; + // Open the call log. + contentIntent = DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_VOICEMAIL); + contentIntent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); + notificationBuilder.setContentIntent( + PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT)); + + // The text to show in the ticker, describing the new event. + if (callToNotify != null) { + CharSequence msg = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + resources, + R.string.notification_new_voicemail_ticker, + names.get(callToNotify.number)); + notificationBuilder.setTicker(msg); + } + Log.i(TAG, "Creating voicemail notification"); + getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build()); + } + + /** + * Determines which ringtone Uri and Notification defaults to use when updating the notification + * for the given call. + */ + private Pair getNotificationInfo(@Nullable NewCall callToNotify) { + Log.v(TAG, "getNotificationInfo"); + if (callToNotify == null) { + Log.i(TAG, "callToNotify == null"); + return new Pair<>(null, 0); + } + PhoneAccountHandle accountHandle; + if (callToNotify.accountComponentName == null || callToNotify.accountId == null) { + Log.v(TAG, "accountComponentName == null || callToNotify.accountId == null"); + accountHandle = TelecomUtil.getDefaultOutgoingPhoneAccount(mContext, PhoneAccount.SCHEME_TEL); + if (accountHandle == null) { + Log.i(TAG, "No default phone account found, using default notification ringtone"); + return new Pair<>(null, Notification.DEFAULT_ALL); + } + + } else { + accountHandle = + new PhoneAccountHandle( + ComponentName.unflattenFromString(callToNotify.accountComponentName), + callToNotify.accountId); + } + if (accountHandle.getComponentName() != null) { + Log.v(TAG, "PhoneAccountHandle.ComponentInfo:" + accountHandle.getComponentName()); + } else { + Log.i(TAG, "PhoneAccountHandle.ComponentInfo: null"); + } + return new Pair<>( + TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle), + getNotificationDefaults(accountHandle)); + } + + private int getNotificationDefaults(PhoneAccountHandle accountHandle) { + if (VERSION.SDK_INT >= VERSION_CODES.N) { + return TelephonyManagerCompat.isVoicemailVibrationEnabled( + getTelephonyManager(), accountHandle) + ? Notification.DEFAULT_VIBRATE + : 0; + } + return Notification.DEFAULT_ALL; + } + + /** Creates a pending intent that marks all new voicemails as old. */ + private PendingIntent createMarkNewVoicemailsAsOldIntent() { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + return PendingIntent.getService(mContext, 0, intent, 0); + } + + private NotificationManager getNotificationManager() { + return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } + + private TelephonyManager getTelephonyManager() { + return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + } +} diff --git a/java/com/android/dialer/app/calllog/GroupingListAdapter.java b/java/com/android/dialer/app/calllog/GroupingListAdapter.java new file mode 100644 index 000000000..d1157206f --- /dev/null +++ b/java/com/android/dialer/app/calllog/GroupingListAdapter.java @@ -0,0 +1,153 @@ +/* + * 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.dialer.app.calllog; + +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.support.v7.widget.RecyclerView; +import android.util.SparseIntArray; + +/** + * Maintains a list that groups items into groups of consecutive elements which are disjoint, that + * is, an item can only belong to one group. This is leveraged for grouping calls in the call log + * received from or made to the same phone number. + * + *

There are two integers stored as metadata for every list item in the adapter. + */ +abstract class GroupingListAdapter extends RecyclerView.Adapter { + + protected ContentObserver mChangeObserver = + new ContentObserver(new Handler()) { + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + }; + protected DataSetObserver mDataSetObserver = + new DataSetObserver() { + @Override + public void onChanged() { + notifyDataSetChanged(); + } + }; + private Cursor mCursor; + /** + * SparseIntArray, which maps the cursor position of the first element of a group to the size of + * the group. The index of a key in this map corresponds to the list position of that group. + */ + private SparseIntArray mGroupMetadata; + + private int mItemCount; + + public GroupingListAdapter() { + reset(); + } + + /** + * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for each of them. + */ + protected abstract void addGroups(Cursor cursor); + + protected abstract void onContentChanged(); + + public void changeCursor(Cursor cursor) { + if (cursor == mCursor) { + return; + } + + if (mCursor != null) { + mCursor.unregisterContentObserver(mChangeObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + + // Reset whenever the cursor is changed. + reset(); + mCursor = cursor; + + if (cursor != null) { + addGroups(mCursor); + + // Calculate the item count by subtracting group child counts from the cursor count. + mItemCount = mGroupMetadata.size(); + + cursor.registerContentObserver(mChangeObserver); + cursor.registerDataSetObserver(mDataSetObserver); + notifyDataSetChanged(); + } + } + + /** + * Records information about grouping in the list. Should be called by the overridden {@link + * #addGroups} method. + */ + public void addGroup(int cursorPosition, int groupSize) { + int lastIndex = mGroupMetadata.size() - 1; + if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) { + mGroupMetadata.put(cursorPosition, groupSize); + } else { + // Optimization to avoid binary search if adding groups in ascending cursor position. + mGroupMetadata.append(cursorPosition, groupSize); + } + } + + @Override + public int getItemCount() { + return mItemCount; + } + + /** + * Given the position of a list item, returns the size of the group of items corresponding to that + * position. + */ + public int getGroupSize(int listPosition) { + if (listPosition < 0 || listPosition >= mGroupMetadata.size()) { + return 0; + } + + return mGroupMetadata.valueAt(listPosition); + } + + /** + * Given the position of a list item, returns the the first item in the group of items + * corresponding to that position. + */ + public Object getItem(int listPosition) { + if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) { + return null; + } + + int cursorPosition = mGroupMetadata.keyAt(listPosition); + if (mCursor.moveToPosition(cursorPosition)) { + return mCursor; + } else { + return null; + } + } + + private void reset() { + mItemCount = 0; + mGroupMetadata = new SparseIntArray(); + } +} diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java new file mode 100644 index 000000000..879ac353d --- /dev/null +++ b/java/com/android/dialer/app/calllog/IntentProvider.java @@ -0,0 +1,198 @@ +/* + * 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.dialer.app.calllog; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.ContactsContract; +import android.telecom.PhoneAccountHandle; +import com.android.contacts.common.model.Contact; +import com.android.contacts.common.model.ContactLoader; +import com.android.dialer.app.CallDetailActivity; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.IntentUtil; +import java.util.ArrayList; + +/** + * Used to create an intent to attach to an action in the call log. + * + *

The intent is constructed lazily with the given information. + */ +public abstract class IntentProvider { + + private static final String TAG = IntentProvider.class.getSimpleName(); + + public static IntentProvider getReturnCallIntentProvider(final String number) { + return getReturnCallIntentProvider(number, null); + } + + public static IntentProvider getReturnCallIntentProvider( + final String number, final PhoneAccountHandle accountHandle) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG) + .setPhoneAccountHandle(accountHandle) + .build(); + } + }; + } + + public static IntentProvider getReturnVideoCallIntentProvider(final String number) { + return getReturnVideoCallIntentProvider(number, null); + } + + public static IntentProvider getReturnVideoCallIntentProvider( + final String number, final PhoneAccountHandle accountHandle) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG) + .setPhoneAccountHandle(accountHandle) + .setIsVideoCall(true) + .build(); + } + }; + } + + public static IntentProvider getReturnVoicemailCallIntentProvider() { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return new CallIntentBuilder(CallUtil.getVoicemailUri(), CallInitiationType.Type.CALL_LOG) + .build(); + } + }; + } + + public static IntentProvider getSendSmsIntentProvider(final String number) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + return IntentUtil.getSendSmsIntent(number); + } + }; + } + + /** + * Retrieves the call details intent provider for an entry in the call log. + * + * @param id The call ID of the first call in the call group. + * @param extraIds The call ID of the other calls grouped together with the call. + * @param voicemailUri If call log entry is for a voicemail, the voicemail URI. + * @return The call details intent provider. + */ + public static IntentProvider getCallDetailIntentProvider( + final long id, final long[] extraIds, final String voicemailUri) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + Intent intent = new Intent(context, CallDetailActivity.class); + // Check if the first item is a voicemail. + if (voicemailUri != null) { + intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri)); + } + + if (extraIds != null && extraIds.length > 0) { + intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, extraIds); + } else { + // If there is a single item, use the direct URI for it. + intent.setData(ContentUris.withAppendedId(TelecomUtil.getCallLogUri(context), id)); + } + return intent; + } + }; + } + + /** Retrieves an add contact intent for the given contact and phone call details. */ + public static IntentProvider getAddContactIntentProvider( + final Uri lookupUri, + final CharSequence name, + final CharSequence number, + final int numberType, + final boolean isNewContact) { + return new IntentProvider() { + @Override + public Intent getIntent(Context context) { + Contact contactToSave = null; + + if (lookupUri != null) { + contactToSave = ContactLoader.parseEncodedContactEntity(lookupUri); + } + + if (contactToSave != null) { + // Populate the intent with contact information stored in the lookup URI. + // Note: This code mirrors code in Contacts/QuickContactsActivity. + final Intent intent; + if (isNewContact) { + intent = IntentUtil.getNewContactIntent(); + } else { + intent = IntentUtil.getAddToExistingContactIntent(); + } + + ArrayList values = contactToSave.getContentValues(); + // Only pre-fill the name field if the provided display name is an nickname + // or better (e.g. structured name, nickname) + if (contactToSave.getDisplayNameSource() + >= ContactsContract.DisplayNameSources.NICKNAME) { + intent.putExtra(ContactsContract.Intents.Insert.NAME, contactToSave.getDisplayName()); + } else if (contactToSave.getDisplayNameSource() + == ContactsContract.DisplayNameSources.ORGANIZATION) { + // This is probably an organization. Instead of copying the organization + // name into a name entry, copy it into the organization entry. This + // way we will still consider the contact an organization. + final ContentValues organization = new ContentValues(); + organization.put( + ContactsContract.CommonDataKinds.Organization.COMPANY, + contactToSave.getDisplayName()); + organization.put( + ContactsContract.Data.MIMETYPE, + ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE); + values.add(organization); + } + + // Last time used and times used are aggregated values from the usage stat + // table. They need to be removed from data values so the SQL table can insert + // properly + for (ContentValues value : values) { + value.remove(ContactsContract.Data.LAST_TIME_USED); + value.remove(ContactsContract.Data.TIMES_USED); + } + + intent.putExtra(ContactsContract.Intents.Insert.DATA, values); + + return intent; + } else { + // If no lookup uri is provided, rely on the available phone number and name. + if (isNewContact) { + return IntentUtil.getNewContactIntent(name, number, numberType); + } else { + return IntentUtil.getAddToExistingContactIntent(name, number, numberType); + } + } + } + }; + } + + public abstract Intent getIntent(Context context); +} diff --git a/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java b/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java new file mode 100644 index 000000000..3a202034e --- /dev/null +++ b/java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java @@ -0,0 +1,50 @@ +/* + * 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.dialer.app.calllog; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * Receives broadcasts that should trigger a refresh of the missed call notification. This includes + * both an explicit broadcast from Telecom and a reboot. + */ +public class MissedCallNotificationReceiver extends BroadcastReceiver { + + //TODO: Use compat class for these methods. + public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION = + "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"; + + public static final String EXTRA_NOTIFICATION_COUNT = "android.telecom.extra.NOTIFICATION_COUNT"; + + public static final String EXTRA_NOTIFICATION_PHONE_NUMBER = + "android.telecom.extra.NOTIFICATION_PHONE_NUMBER"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (!ACTION_SHOW_MISSED_CALLS_NOTIFICATION.equals(action)) { + return; + } + + int count = + intent.getIntExtra( + EXTRA_NOTIFICATION_COUNT, CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT); + String number = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER); + CallLogNotificationsService.updateMissedCallNotifications(context, count, number); + } +} diff --git a/java/com/android/dialer/app/calllog/MissedCallNotifier.java b/java/com/android/dialer/app/calllog/MissedCallNotifier.java new file mode 100644 index 000000000..2fa3dae65 --- /dev/null +++ b/java/com/android/dialer/app/calllog/MissedCallNotifier.java @@ -0,0 +1,330 @@ +/* + * 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.dialer.app.calllog; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.os.AsyncTask; +import android.provider.CallLog.Calls; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.os.UserManagerCompat; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogNotificationsHelper.NewCall; +import com.android.dialer.app.contactinfo.ContactPhotoLoader; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.ConfigProviderBindings; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import java.util.List; + +/** Creates a notification for calls that the user missed (neither answered nor rejected). */ +public class MissedCallNotifier { + + /** The tag used to identify notifications from this class. */ + private static final String NOTIFICATION_TAG = "MissedCallNotifier"; + /** The identifier of the notification of new missed calls. */ + private static final int NOTIFICATION_ID = 1; + + private static MissedCallNotifier sInstance; + private Context mContext; + private CallLogNotificationsHelper mCalllogNotificationsHelper; + + @VisibleForTesting + MissedCallNotifier(Context context, CallLogNotificationsHelper callLogNotificationsHelper) { + mContext = context; + mCalllogNotificationsHelper = callLogNotificationsHelper; + } + + /** Returns the singleton instance of the {@link MissedCallNotifier}. */ + public static MissedCallNotifier getInstance(Context context) { + if (sInstance == null) { + CallLogNotificationsHelper callLogNotificationsHelper = + CallLogNotificationsHelper.getInstance(context); + sInstance = new MissedCallNotifier(context, callLogNotificationsHelper); + } + return sInstance; + } + + /** + * Creates a missed call notification with a post call message if there are no existing missed + * calls. + */ + public void createPostCallMessageNotification(String number, String message) { + int count = CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT; + if (ConfigProviderBindings.get(mContext).getBoolean("enable_call_compose", false)) { + updateMissedCallNotification(count, number, message); + } else { + updateMissedCallNotification(count, number, null); + } + } + + /** Creates a missed call notification. */ + public void updateMissedCallNotification(int count, String number) { + updateMissedCallNotification(count, number, null); + } + + private void updateMissedCallNotification( + int count, String number, @Nullable String postCallMessage) { + final int titleResId; + CharSequence expandedText; // The text in the notification's line 1 and 2. + + final List newCalls = mCalllogNotificationsHelper.getNewMissedCalls(); + + if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) { + if (newCalls == null) { + // If the intent did not contain a count, and we are unable to get a count from the + // call log, then no notification can be shown. + return; + } + count = newCalls.size(); + } + + if (count == 0) { + // No voicemails to notify about: clear the notification. + clearMissedCalls(); + return; + } + + // The call log has been updated, use that information preferentially. + boolean useCallLog = newCalls != null && newCalls.size() == count; + NewCall newestCall = useCallLog ? newCalls.get(0) : null; + long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis(); + String missedNumber = useCallLog ? newestCall.number : number; + + Notification.Builder builder = new Notification.Builder(mContext); + // Display the first line of the notification: + // 1 missed call: + // More than 1 missed call: + "missed calls" + if (count == 1) { + //TODO: look up caller ID that is not in contacts. + ContactInfo contactInfo = + mCalllogNotificationsHelper.getContactInfo( + missedNumber, + useCallLog ? newestCall.numberPresentation : Calls.PRESENTATION_ALLOWED, + useCallLog ? newestCall.countryIso : null); + + titleResId = + contactInfo.userType == ContactsUtils.USER_TYPE_WORK + ? R.string.notification_missedWorkCallTitle + : R.string.notification_missedCallTitle; + if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber) + || TextUtils.equals(contactInfo.name, contactInfo.number)) { + expandedText = + PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance() + .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR)); + } else { + expandedText = contactInfo.name; + } + + if (!TextUtils.isEmpty(postCallMessage)) { + // Ex. "John Doe: Hey dude" + expandedText = + mContext.getString( + R.string.post_call_notification_message, expandedText, postCallMessage); + } + ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo); + Bitmap photoIcon = loader.loadPhotoIcon(); + if (photoIcon != null) { + builder.setLargeIcon(photoIcon); + } + } else { + titleResId = R.string.notification_missedCallsTitle; + expandedText = mContext.getString(R.string.notification_missedCallsMsg, count); + } + + // Create a public viewable version of the notification, suitable for display when sensitive + // notification content is hidden. + Notification.Builder publicBuilder = new Notification.Builder(mContext); + publicBuilder + .setSmallIcon(android.R.drawable.stat_notify_missed_call) + .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) + // Show "Phone" for notification title. + .setContentTitle(mContext.getText(R.string.userCallActivityLabel)) + // Notification details shows that there are missed call(s), but does not reveal + // the missed caller information. + .setContentText(mContext.getText(titleResId)) + .setContentIntent(createCallLogPendingIntent()) + .setAutoCancel(true) + .setWhen(timeMs) + .setShowWhen(true) + .setDeleteIntent(createClearMissedCallsPendingIntent()); + + // Create the notification suitable for display when sensitive information is showing. + builder + .setSmallIcon(android.R.drawable.stat_notify_missed_call) + .setColor(mContext.getResources().getColor(R.color.dialer_theme_color)) + .setContentTitle(mContext.getText(titleResId)) + .setContentText(expandedText) + .setContentIntent(createCallLogPendingIntent()) + .setAutoCancel(true) + .setWhen(timeMs) + .setShowWhen(true) + .setDefaults(Notification.DEFAULT_VIBRATE) + .setDeleteIntent(createClearMissedCallsPendingIntent()) + // Include a public version of the notification to be shown when the missed call + // notification is shown on the user's lock screen and they have chosen to hide + // sensitive notification information. + .setPublicVersion(publicBuilder.build()); + + // Add additional actions when there is only 1 missed call and the user isn't locked + if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) { + if (!TextUtils.isEmpty(missedNumber) + && !TextUtils.equals(missedNumber, mContext.getString(R.string.handle_restricted))) { + builder.addAction( + R.drawable.ic_phone_24dp, + mContext.getString(R.string.notification_missedCall_call_back), + createCallBackPendingIntent(missedNumber)); + + if (!PhoneNumberHelper.isUriNumber(missedNumber)) { + builder.addAction( + R.drawable.ic_message_24dp, + mContext.getString(R.string.notification_missedCall_message), + createSendSmsFromNotificationPendingIntent(missedNumber)); + } + } + } + + Notification notification = builder.build(); + configureLedOnNotification(notification); + + LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification"); + getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification); + } + + private void clearMissedCalls() { + AsyncTask.execute( + new Runnable() { + @Override + public void run() { + // Call log is only accessible when unlocked. If that's the case, clear the list of + // new missed calls from the call log. + if (UserManagerCompat.isUserUnlocked(mContext)) { + ContentValues values = new ContentValues(); + values.put(Calls.NEW, 0); + values.put(Calls.IS_READ, 1); + StringBuilder where = new StringBuilder(); + where.append(Calls.NEW); + where.append(" = 1 AND "); + where.append(Calls.TYPE); + where.append(" = ?"); + try { + mContext + .getContentResolver() + .update( + Calls.CONTENT_URI, + values, + where.toString(), + new String[] {Integer.toString(Calls.MISSED_TYPE)}); + } catch (IllegalArgumentException e) { + LogUtil.e( + "MissedCallNotifier.clearMissedCalls", + "contacts provider update command failed", + e); + } + } + getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID); + } + }); + } + + /** Trigger an intent to make a call from a missed call number. */ + public void callBackFromMissedCall(String number) { + closeSystemDialogs(mContext); + CallLogNotificationsHelper.removeMissedCallNotifications(mContext); + DialerUtils.startActivityWithErrorToast( + mContext, + new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION) + .build() + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** Trigger an intent to send an sms from a missed call number. */ + public void sendSmsFromMissedCall(String number) { + closeSystemDialogs(mContext); + CallLogNotificationsHelper.removeMissedCallNotifications(mContext); + DialerUtils.startActivityWithErrorToast( + mContext, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + /** + * Creates a new pending intent that sends the user to the call log. + * + * @return The pending intent. + */ + private PendingIntent createCallLogPendingIntent() { + Intent contentIntent = + DialtactsActivity.getShowTabIntent(mContext, ListsFragment.TAB_INDEX_HISTORY); + return PendingIntent.getActivity(mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** Creates a pending intent that marks all new missed calls as old. */ + private PendingIntent createClearMissedCallsPendingIntent() { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD); + return PendingIntent.getService(mContext, 0, intent, 0); + } + + private PendingIntent createCallBackPendingIntent(String number) { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION); + intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number); + // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new + // extra. + return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent createSendSmsFromNotificationPendingIntent(String number) { + Intent intent = new Intent(mContext, CallLogNotificationsService.class); + intent.setAction(CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION); + intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number); + // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new + // extra. + return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** Configures a notification to emit the blinky notification light. */ + private void configureLedOnNotification(Notification notification) { + notification.flags |= Notification.FLAG_SHOW_LIGHTS; + notification.defaults |= Notification.DEFAULT_LIGHTS; + } + + /** Closes open system dialogs and the notification shade. */ + private void closeSystemDialogs(Context context) { + context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + + private NotificationManager getNotificationMgr() { + return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneAccountUtils.java b/java/com/android/dialer/app/calllog/PhoneAccountUtils.java new file mode 100644 index 000000000..c6d94d341 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneAccountUtils.java @@ -0,0 +1,104 @@ +/* + * 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.dialer.app.calllog; + +import android.content.ComponentName; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.dialer.telecom.TelecomUtil; +import java.util.ArrayList; +import java.util.List; + +/** Methods to help extract {@code PhoneAccount} information from database and Telecomm sources. */ +public class PhoneAccountUtils { + + /** Return a list of phone accounts that are subscription/SIM accounts. */ + public static List getSubscriptionPhoneAccounts(Context context) { + List subscriptionAccountHandles = new ArrayList(); + final List accountHandles = + TelecomUtil.getCallCapablePhoneAccounts(context); + for (PhoneAccountHandle accountHandle : accountHandles) { + PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); + if (account.hasCapabilities(PhoneAccount.CAPABILITY_SIM_SUBSCRIPTION)) { + subscriptionAccountHandles.add(accountHandle); + } + } + return subscriptionAccountHandles; + } + + /** Compose PhoneAccount object from component name and account id. */ + @Nullable + public static PhoneAccountHandle getAccount( + @Nullable String componentString, @Nullable String accountId) { + if (TextUtils.isEmpty(componentString) || TextUtils.isEmpty(accountId)) { + return null; + } + final ComponentName componentName = ComponentName.unflattenFromString(componentString); + if (componentName == null) { + return null; + } + return new PhoneAccountHandle(componentName, accountId); + } + + /** Extract account label from PhoneAccount object. */ + @Nullable + public static String getAccountLabel( + Context context, @Nullable PhoneAccountHandle accountHandle) { + PhoneAccount account = getAccountOrNull(context, accountHandle); + if (account != null && account.getLabel() != null) { + return account.getLabel().toString(); + } + return null; + } + + /** Extract account color from PhoneAccount object. */ + public static int getAccountColor(Context context, @Nullable PhoneAccountHandle accountHandle) { + final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); + + // For single-sim devices the PhoneAccount will be NO_HIGHLIGHT_COLOR by default, so it is + // safe to always use the account highlight color. + return account == null ? PhoneAccount.NO_HIGHLIGHT_COLOR : account.getHighlightColor(); + } + + /** + * Determine whether a phone account supports call subjects. + * + * @return {@code true} if call subjects are supported, {@code false} otherwise. + */ + public static boolean getAccountSupportsCallSubject( + Context context, @Nullable PhoneAccountHandle accountHandle) { + final PhoneAccount account = TelecomUtil.getPhoneAccount(context, accountHandle); + + return account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT); + } + + /** + * Retrieve the account metadata, but if the account does not exist or the device has only a + * single registered and enabled account, return null. + */ + @Nullable + private static PhoneAccount getAccountOrNull( + Context context, @Nullable PhoneAccountHandle accountHandle) { + if (TelecomUtil.getCallCapablePhoneAccounts(context).size() <= 1) { + return null; + } + return TelecomUtil.getPhoneAccount(context, accountHandle); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java new file mode 100644 index 000000000..b18270bb3 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java @@ -0,0 +1,352 @@ +/* + * 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.dialer.app.calllog; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Typeface; +import android.provider.CallLog.Calls; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v4.content.ContextCompat; +import android.telecom.PhoneAccount; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.view.View; +import android.widget.TextView; +import com.android.dialer.app.PhoneCallDetails; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.DialerUtils; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.concurrent.TimeUnit; + +/** Helper class to fill in the views in {@link PhoneCallDetailsViews}. */ +public class PhoneCallDetailsHelper { + + /** The maximum number of icons will be shown to represent the call types in a group. */ + private static final int MAX_CALL_TYPE_ICONS = 3; + + private final Context mContext; + private final Resources mResources; + private final CallLogCache mCallLogCache; + /** Calendar used to construct dates */ + private final Calendar mCalendar; + /** The injected current time in milliseconds since the epoch. Used only by tests. */ + private Long mCurrentTimeMillisForTest; + + private CharSequence mPhoneTypeLabelForTest; + /** List of items to be concatenated together for accessibility descriptions */ + private ArrayList mDescriptionItems = new ArrayList<>(); + + /** + * Creates a new instance of the helper. + * + *

Generally you should have a single instance of this helper in any context. + * + * @param resources used to look up strings + */ + public PhoneCallDetailsHelper(Context context, Resources resources, CallLogCache callLogCache) { + mContext = context; + mResources = resources; + mCallLogCache = callLogCache; + mCalendar = Calendar.getInstance(); + } + + /** Fills the call details views with content. */ + public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details) { + // Display up to a given number of icons. + views.callTypeIcons.clear(); + int count = details.callTypes.length; + boolean isVoicemail = false; + for (int index = 0; index < count && index < MAX_CALL_TYPE_ICONS; ++index) { + views.callTypeIcons.add(details.callTypes[index]); + if (index == 0) { + isVoicemail = details.callTypes[index] == Calls.VOICEMAIL_TYPE; + } + } + + // Show the video icon if the call had video enabled. + views.callTypeIcons.setShowVideo( + (details.features & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO); + views.callTypeIcons.requestLayout(); + views.callTypeIcons.setVisibility(View.VISIBLE); + + // Show the total call count only if there are more than the maximum number of icons. + final Integer callCount; + if (count > MAX_CALL_TYPE_ICONS) { + callCount = count; + } else { + callCount = null; + } + + // Set the call count, location, date and if voicemail, set the duration. + setDetailText(views, callCount, details); + + // Set the account label if it exists. + String accountLabel = mCallLogCache.getAccountLabel(details.accountHandle); + if (!TextUtils.isEmpty(details.viaNumber)) { + if (!TextUtils.isEmpty(accountLabel)) { + accountLabel = + mResources.getString( + R.string.call_log_via_number_phone_account, accountLabel, details.viaNumber); + } else { + accountLabel = mResources.getString(R.string.call_log_via_number, details.viaNumber); + } + } + if (!TextUtils.isEmpty(accountLabel)) { + views.callAccountLabel.setVisibility(View.VISIBLE); + views.callAccountLabel.setText(accountLabel); + int color = mCallLogCache.getAccountColor(details.accountHandle); + if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) { + int defaultColor = R.color.dialer_secondary_text_color; + views.callAccountLabel.setTextColor(mContext.getResources().getColor(defaultColor)); + } else { + views.callAccountLabel.setTextColor(color); + } + } else { + views.callAccountLabel.setVisibility(View.GONE); + } + + final CharSequence nameText; + final CharSequence displayNumber = details.displayNumber; + if (TextUtils.isEmpty(details.getPreferredName())) { + nameText = displayNumber; + // We have a real phone number as "nameView" so make it always LTR + views.nameView.setTextDirection(View.TEXT_DIRECTION_LTR); + } else { + nameText = details.getPreferredName(); + } + + views.nameView.setText(nameText); + + if (isVoicemail) { + views.voicemailTranscriptionView.setText( + TextUtils.isEmpty(details.transcription) ? null : details.transcription); + } + + // Bold if not read + Typeface typeface = details.isRead ? Typeface.SANS_SERIF : Typeface.DEFAULT_BOLD; + views.nameView.setTypeface(typeface); + views.voicemailTranscriptionView.setTypeface(typeface); + views.callLocationAndDate.setTypeface(typeface); + views.callLocationAndDate.setTextColor( + ContextCompat.getColor( + mContext, + details.isRead ? R.color.call_log_detail_color : R.color.call_log_unread_text_color)); + } + + /** + * Builds a string containing the call location and date. For voicemail logs only the call date is + * returned because location information is displayed in the call action button + * + * @param details The call details. + * @return The call location and date string. + */ + public CharSequence getCallLocationAndDate(PhoneCallDetails details) { + mDescriptionItems.clear(); + + if (details.callTypes[0] != Calls.VOICEMAIL_TYPE) { + // Get type of call (ie mobile, home, etc) if known, or the caller's location. + CharSequence callTypeOrLocation = getCallTypeOrLocation(details); + + // Only add the call type or location if its not empty. It will be empty for unknown + // callers. + if (!TextUtils.isEmpty(callTypeOrLocation)) { + mDescriptionItems.add(callTypeOrLocation); + } + } + + // The date of this call + mDescriptionItems.add(getCallDate(details)); + + // Create a comma separated list from the call type or location, and call date. + return DialerUtils.join(mDescriptionItems); + } + + /** + * For a call, if there is an associated contact for the caller, return the known call type (e.g. + * mobile, home, work). If there is no associated contact, attempt to use the caller's location if + * known. + * + * @param details Call details to use. + * @return Type of call (mobile/home) if known, or the location of the caller (if known). + */ + public CharSequence getCallTypeOrLocation(PhoneCallDetails details) { + if (details.isSpam) { + return mResources.getString(R.string.spam_number_call_log_label); + } else if (details.isBlocked) { + return mResources.getString(R.string.blocked_number_call_log_label); + } + + CharSequence numberFormattedLabel = null; + // Only show a label if the number is shown and it is not a SIP address. + if (!TextUtils.isEmpty(details.number) + && !PhoneNumberHelper.isUriNumber(details.number.toString()) + && !mCallLogCache.isVoicemailNumber(details.accountHandle, details.number)) { + + if (TextUtils.isEmpty(details.namePrimary) && !TextUtils.isEmpty(details.geocode)) { + numberFormattedLabel = details.geocode; + } else if (!(details.numberType == Phone.TYPE_CUSTOM + && TextUtils.isEmpty(details.numberLabel))) { + // Get type label only if it will not be "Custom" because of an empty number label. + numberFormattedLabel = + mPhoneTypeLabelForTest != null + ? mPhoneTypeLabelForTest + : Phone.getTypeLabel(mResources, details.numberType, details.numberLabel); + } + } + + if (!TextUtils.isEmpty(details.namePrimary) && TextUtils.isEmpty(numberFormattedLabel)) { + numberFormattedLabel = details.displayNumber; + } + return numberFormattedLabel; + } + + public void setPhoneTypeLabelForTest(CharSequence phoneTypeLabel) { + this.mPhoneTypeLabelForTest = phoneTypeLabel; + } + + /** + * Get the call date/time of the call. For the call log this is relative to the current time. e.g. + * 3 minutes ago. For voicemail, see {@link #getGranularDateTime(PhoneCallDetails)} + * + * @param details Call details to use. + * @return String representing when the call occurred. + */ + public CharSequence getCallDate(PhoneCallDetails details) { + if (details.callTypes[0] == Calls.VOICEMAIL_TYPE) { + return getGranularDateTime(details); + } + + return DateUtils.getRelativeTimeSpanString( + details.date, + getCurrentTimeMillis(), + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE); + } + + /** + * Get the granular version of the call date/time of the call. The result is always in the form + * 'DATE at TIME'. The date value changes based on when the call was created. + * + *

If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is + * 'MMM dd, yyyy' + * + *

TIME is the localized time format, e.g. 'hh:mm a' or 'HH:mm' + * + * @param details Call details to use + * @return String representing when the call occurred + */ + public CharSequence getGranularDateTime(PhoneCallDetails details) { + return mResources.getString( + R.string.voicemailCallLogDateTimeFormat, + getGranularDate(details.date), + DateUtils.formatDateTime(mContext, details.date, DateUtils.FORMAT_SHOW_TIME)); + } + + /** + * Get the granular version of the call date. See {@link #getGranularDateTime(PhoneCallDetails)} + */ + private String getGranularDate(long date) { + if (DateUtils.isToday(date)) { + return mResources.getString(R.string.voicemailCallLogToday); + } + return DateUtils.formatDateTime( + mContext, + date, + DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH + | (shouldShowYear(date) ? DateUtils.FORMAT_SHOW_YEAR : DateUtils.FORMAT_NO_YEAR)); + } + + /** + * Determines whether the year should be shown for the given date + * + * @return {@code true} if date is within the current year, {@code false} otherwise + */ + private boolean shouldShowYear(long date) { + mCalendar.setTimeInMillis(getCurrentTimeMillis()); + int currentYear = mCalendar.get(Calendar.YEAR); + mCalendar.setTimeInMillis(date); + return currentYear != mCalendar.get(Calendar.YEAR); + } + + /** Sets the text of the header view for the details page of a phone call. */ + public void setCallDetailsHeader(TextView nameView, PhoneCallDetails details) { + final CharSequence nameText; + if (!TextUtils.isEmpty(details.namePrimary)) { + nameText = details.namePrimary; + } else if (!TextUtils.isEmpty(details.displayNumber)) { + nameText = details.displayNumber; + } else { + nameText = mResources.getString(R.string.unknown); + } + + nameView.setText(nameText); + } + + public void setCurrentTimeForTest(long currentTimeMillis) { + mCurrentTimeMillisForTest = currentTimeMillis; + } + + /** + * Returns the current time in milliseconds since the epoch. + * + *

It can be injected in tests using {@link #setCurrentTimeForTest(long)}. + */ + private long getCurrentTimeMillis() { + if (mCurrentTimeMillisForTest == null) { + return System.currentTimeMillis(); + } else { + return mCurrentTimeMillisForTest; + } + } + + /** Sets the call count, date, and if it is a voicemail, sets the duration. */ + private void setDetailText( + PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) { + // Combine the count (if present) and the date. + CharSequence dateText = details.callLocationAndDate; + final CharSequence text; + if (callCount != null) { + text = mResources.getString(R.string.call_log_item_count_and_date, callCount, dateText); + } else { + text = dateText; + } + + if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) { + views.callLocationAndDate.setText( + mResources.getString( + R.string.voicemailCallLogDateTimeFormatWithDuration, + text, + getVoicemailDuration(details))); + } else { + views.callLocationAndDate.setText(text); + } + } + + private String getVoicemailDuration(PhoneCallDetails details) { + long minutes = TimeUnit.SECONDS.toMinutes(details.duration); + long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes); + if (minutes > 99) { + minutes = 99; + } + return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java new file mode 100644 index 000000000..476996826 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java @@ -0,0 +1,75 @@ +/* + * 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.dialer.app.calllog; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; +import com.android.dialer.app.R; + +/** Encapsulates the views that are used to display the details of a phone call in the call log. */ +public final class PhoneCallDetailsViews { + + public final TextView nameView; + public final View callTypeView; + public final CallTypeIconsView callTypeIcons; + public final TextView callLocationAndDate; + public final TextView voicemailTranscriptionView; + public final TextView callAccountLabel; + + private PhoneCallDetailsViews( + TextView nameView, + View callTypeView, + CallTypeIconsView callTypeIcons, + TextView callLocationAndDate, + TextView voicemailTranscriptionView, + TextView callAccountLabel) { + this.nameView = nameView; + this.callTypeView = callTypeView; + this.callTypeIcons = callTypeIcons; + this.callLocationAndDate = callLocationAndDate; + this.voicemailTranscriptionView = voicemailTranscriptionView; + this.callAccountLabel = callAccountLabel; + } + + /** + * Create a new instance by extracting the elements from the given view. + * + *

The view should contain three text views with identifiers {@code R.id.name}, {@code + * R.id.date}, and {@code R.id.number}, and a linear layout with identifier {@code + * R.id.call_types}. + */ + public static PhoneCallDetailsViews fromView(View view) { + return new PhoneCallDetailsViews( + (TextView) view.findViewById(R.id.name), + view.findViewById(R.id.call_type), + (CallTypeIconsView) view.findViewById(R.id.call_type_icons), + (TextView) view.findViewById(R.id.call_location_and_date), + (TextView) view.findViewById(R.id.voicemail_transcription), + (TextView) view.findViewById(R.id.call_account_label)); + } + + public static PhoneCallDetailsViews createForTest(Context context) { + return new PhoneCallDetailsViews( + new TextView(context), + new View(context), + new CallTypeIconsView(context), + new TextView(context), + new TextView(context), + new TextView(context)); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java b/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java new file mode 100644 index 000000000..410d4cc37 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.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.dialer.app.calllog; + +import android.content.Context; +import android.provider.CallLog.Calls; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import com.android.contacts.common.compat.PhoneNumberUtilsCompat; +import com.android.dialer.app.R; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; + +/** Helper for formatting and managing the display of phone numbers. */ +public class PhoneNumberDisplayUtil { + + /** Returns the string to display for the given phone number if there is no matching contact. */ + /* package */ + static CharSequence getDisplayName( + Context context, CharSequence number, int presentation, boolean isVoicemail) { + if (presentation == Calls.PRESENTATION_UNKNOWN) { + return context.getResources().getString(R.string.unknown); + } + if (presentation == Calls.PRESENTATION_RESTRICTED) { + return PhoneNumberHelper.getDisplayNameForRestrictedNumber(context); + } + if (presentation == Calls.PRESENTATION_PAYPHONE) { + return context.getResources().getString(R.string.payphone); + } + if (isVoicemail) { + return context.getResources().getString(R.string.voicemail); + } + if (PhoneNumberHelper.isLegacyUnknownNumbers(number)) { + return context.getResources().getString(R.string.unknown); + } + return ""; + } + + /** + * Returns the string to display for the given phone number. + * + * @param number the number to display + * @param formattedNumber the formatted number if available, may be null + */ + public static CharSequence getDisplayNumber( + Context context, + CharSequence number, + int presentation, + CharSequence formattedNumber, + CharSequence postDialDigits, + boolean isVoicemail) { + final CharSequence displayName = getDisplayName(context, number, presentation, isVoicemail); + if (!TextUtils.isEmpty(displayName)) { + return getTtsSpannableLtrNumber(displayName); + } + + if (!TextUtils.isEmpty(formattedNumber)) { + return getTtsSpannableLtrNumber(formattedNumber); + } else if (!TextUtils.isEmpty(number)) { + return getTtsSpannableLtrNumber(number.toString() + postDialDigits); + } else { + return context.getResources().getString(R.string.unknown); + } + } + + /** Returns number annotated as phone number in LTR direction. */ + public static CharSequence getTtsSpannableLtrNumber(CharSequence number) { + return PhoneNumberUtilsCompat.createTtsSpannable( + BidiFormatter.getInstance().unicodeWrap(number.toString(), TextDirectionHeuristics.LTR)); + } +} diff --git a/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java new file mode 100644 index 000000000..e539ceef6 --- /dev/null +++ b/java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java @@ -0,0 +1,132 @@ +/* + * 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.dialer.app.calllog; + +import android.app.Activity; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.os.Bundle; +import android.provider.CallLog; +import android.provider.VoicemailContract; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.app.list.ListsFragment; +import com.android.dialer.app.voicemail.VoicemailAudioManager; +import com.android.dialer.app.voicemail.VoicemailErrorManager; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; +import com.android.dialer.common.LogUtil; + +public class VisualVoicemailCallLogFragment extends CallLogFragment { + + private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver(); + private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; + + private VoicemailErrorManager mVoicemailAlertManager; + + @Override + public void onCreate(Bundle state) { + super.onCreate(state); + mCallTypeFilter = CallLog.Calls.VOICEMAIL_TYPE; + mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter.getInstance(getActivity(), state); + getActivity() + .getContentResolver() + .registerContentObserver( + VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver); + } + + @Override + protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() { + return mVoicemailPlaybackPresenter; + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + mVoicemailAlertManager = + new VoicemailErrorManager(getContext(), getAdapter().getAlertManager(), mModalAlertManager); + getActivity() + .getContentResolver() + .registerContentObserver( + VoicemailContract.Status.CONTENT_URI, + true, + mVoicemailAlertManager.getContentObserver()); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + View view = inflater.inflate(R.layout.call_log_fragment, container, false); + setupView(view); + return view; + } + + @Override + public void onResume() { + super.onResume(); + mVoicemailPlaybackPresenter.onResume(); + mVoicemailAlertManager.onResume(); + } + + @Override + public void onPause() { + mVoicemailPlaybackPresenter.onPause(); + mVoicemailAlertManager.onPause(); + super.onPause(); + } + + @Override + public void onDestroy() { + getActivity() + .getContentResolver() + .unregisterContentObserver(mVoicemailAlertManager.getContentObserver()); + mVoicemailPlaybackPresenter.onDestroy(); + getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); + super.onDestroy(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + mVoicemailPlaybackPresenter.onSaveInstanceState(outState); + } + + @Override + public void fetchCalls() { + super.fetchCalls(); + ((ListsFragment) getParentFragment()).updateTabUnreadCounts(); + } + + @Override + public void onPageResume(@Nullable Activity activity) { + LogUtil.d("VisualVoicemailCallLogFragment.onPageResume", null); + super.onPageResume(activity); + if (activity != null) { + activity.setVolumeControlStream(VoicemailAudioManager.PLAYBACK_STREAM); + } + } + + @Override + public void onPagePause(@Nullable Activity activity) { + LogUtil.d("VisualVoicemailCallLogFragment.onPagePause", null); + super.onPagePause(activity); + if (activity != null) { + activity.setVolumeControlStream(AudioManager.USE_DEFAULT_STREAM_TYPE); + } + } +} diff --git a/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java new file mode 100644 index 000000000..d6d8354ec --- /dev/null +++ b/java/com/android/dialer/app/calllog/VoicemailQueryHandler.java @@ -0,0 +1,74 @@ +/* + * 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.dialer.app.calllog; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.provider.CallLog.Calls; +import android.util.Log; + +/** Handles asynchronous queries to the call log for voicemail. */ +public class VoicemailQueryHandler extends AsyncQueryHandler { + + private static final String TAG = "VoicemailQueryHandler"; + + /** The token for the query to mark all new voicemails as old. */ + private static final int UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN = 50; + + private Context mContext; + + public VoicemailQueryHandler(Context context, ContentResolver contentResolver) { + super(contentResolver); + mContext = context; + } + + /** Updates all new voicemails to mark them as old. */ + public void markNewVoicemailsAsOld() { + // Mark all "new" voicemails as not new anymore. + StringBuilder where = new StringBuilder(); + where.append(Calls.NEW); + where.append(" = 1 AND "); + where.append(Calls.TYPE); + where.append(" = ?"); + + ContentValues values = new ContentValues(1); + values.put(Calls.NEW, "0"); + + startUpdate( + UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN, + null, + Calls.CONTENT_URI_WITH_VOICEMAIL, + values, + where.toString(), + new String[] {Integer.toString(Calls.VOICEMAIL_TYPE)}); + } + + @Override + protected void onUpdateComplete(int token, Object cookie, int result) { + if (token == UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN) { + if (mContext != null) { + Intent serviceIntent = new Intent(mContext, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); + mContext.startService(serviceIntent); + } else { + Log.w(TAG, "Unknown update completed: ignoring: " + token); + } + } + } +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java new file mode 100644 index 000000000..7645a333e --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java @@ -0,0 +1,105 @@ +/* + * 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.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import com.android.dialer.app.calllog.CallLogAdapter; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.CallUtil; + +/** + * This is the base class for the CallLogCaches. + * + *

Keeps a cache of recently made queries to the Telecom/Telephony processes. The aim of this + * cache is to reduce the number of cross-process requests to TelecomManager, which can negatively + * affect performance. + * + *

This is designed with the specific use case of the {@link CallLogAdapter} in mind. + */ +public abstract class CallLogCache { + // TODO: Dialer should be fixed so as not to check isVoicemail() so often but at the time of + // this writing, that was a much larger undertaking than creating this cache. + + protected final Context mContext; + + private boolean mHasCheckedForVideoAvailability; + private int mVideoAvailability; + + public CallLogCache(Context context) { + mContext = context; + } + + /** Return the most compatible version of the TelecomCallLogCache. */ + public static CallLogCache getCallLogCache(Context context) { + if (CompatUtils.isClassAvailable("android.telecom.PhoneAccountHandle")) { + return new CallLogCacheLollipopMr1(context); + } + return new CallLogCacheLollipop(context); + } + + public void reset() { + mHasCheckedForVideoAvailability = false; + mVideoAvailability = 0; + } + + /** + * Returns true if the given number is the number of the configured voicemail. To be able to + * mock-out this, it is not a static method. + */ + public abstract boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number); + + /** + * Returns {@code true} when the current sim supports video calls, regardless of the value in a + * contact's {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} + * column. + */ + public boolean isVideoEnabled() { + if (!mHasCheckedForVideoAvailability) { + mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); + mHasCheckedForVideoAvailability = true; + } + return (mVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) != 0; + } + + /** + * Returns {@code true} when the current sim supports checking video calling capabilities via the + * {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} column. + */ + public boolean canRelyOnVideoPresence() { + if (!mHasCheckedForVideoAvailability) { + mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); + mHasCheckedForVideoAvailability = true; + } + return (mVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) != 0; + } + + /** Extract account label from PhoneAccount object. */ + public abstract String getAccountLabel(PhoneAccountHandle accountHandle); + + /** Extract account color from PhoneAccount object. */ + public abstract int getAccountColor(PhoneAccountHandle accountHandle); + + /** + * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note) + * for outgoing calls. + * + * @param accountHandle The PhoneAccount handle. + * @return {@code true} if calling with a note is supported, {@code false} otherwise. + */ + public abstract boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle); +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java new file mode 100644 index 000000000..78aaa4193 --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java @@ -0,0 +1,74 @@ +/* + * 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.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; + +/** + * This is a compatibility class for the CallLogCache for versions of dialer before Lollipop Mr1 + * (the introduction of phone accounts). + * + *

This class should not be initialized directly and instead be acquired from {@link + * CallLogCache#getCallLogCache}. + */ +class CallLogCacheLollipop extends CallLogCache { + + private String mVoicemailNumber; + + /* package */ CallLogCacheLollipop(Context context) { + super(context); + } + + @Override + public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + String numberString = number.toString(); + + if (!TextUtils.isEmpty(mVoicemailNumber)) { + return PhoneNumberUtils.compare(numberString, mVoicemailNumber); + } + + if (PhoneNumberUtils.isVoiceMailNumber(numberString)) { + mVoicemailNumber = numberString; + return true; + } + + return false; + } + + @Override + public String getAccountLabel(PhoneAccountHandle accountHandle) { + return null; + } + + @Override + public int getAccountColor(PhoneAccountHandle accountHandle) { + return PhoneAccount.NO_HIGHLIGHT_COLOR; + } + + @Override + public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + return false; + } +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java new file mode 100644 index 000000000..c342b7e3b --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java @@ -0,0 +1,116 @@ +/* + * 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.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.Pair; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This is the CallLogCache for versions of dialer Lollipop Mr1 and above with support for multi-SIM + * devices. + * + *

This class should not be initialized directly and instead be acquired from {@link + * CallLogCache#getCallLogCache}. + */ +class CallLogCacheLollipopMr1 extends CallLogCache { + + /* + * Maps from a phone-account/number pair to a boolean because multiple numbers could return true + * for the voicemail number if those numbers are not pre-normalized. Access must be synchronzied + * as it's used in the background thread in CallLogAdapter. {@see CallLogAdapter#loadData} + */ + @VisibleForTesting + final Map, Boolean> mVoicemailQueryCache = + new ConcurrentHashMap<>(); + + private final Map mPhoneAccountLabelCache = new HashMap<>(); + private final Map mPhoneAccountColorCache = new HashMap<>(); + private final Map mPhoneAccountCallWithNoteCache = new HashMap<>(); + + /* package */ CallLogCacheLollipopMr1(Context context) { + super(context); + } + + @Override + public void reset() { + mVoicemailQueryCache.clear(); + mPhoneAccountLabelCache.clear(); + mPhoneAccountColorCache.clear(); + mPhoneAccountCallWithNoteCache.clear(); + + super.reset(); + } + + @Override + public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + Pair key = new Pair<>(accountHandle, number); + Boolean value = mVoicemailQueryCache.get(key); + if (value != null) { + return value; + } + boolean isVoicemail = + PhoneNumberHelper.isVoicemailNumber(mContext, accountHandle, number.toString()); + mVoicemailQueryCache.put(key, isVoicemail); + return isVoicemail; + } + + @Override + public String getAccountLabel(PhoneAccountHandle accountHandle) { + if (mPhoneAccountLabelCache.containsKey(accountHandle)) { + return mPhoneAccountLabelCache.get(accountHandle); + } else { + String label = PhoneAccountUtils.getAccountLabel(mContext, accountHandle); + mPhoneAccountLabelCache.put(accountHandle, label); + return label; + } + } + + @Override + public int getAccountColor(PhoneAccountHandle accountHandle) { + if (mPhoneAccountColorCache.containsKey(accountHandle)) { + return mPhoneAccountColorCache.get(accountHandle); + } else { + Integer color = PhoneAccountUtils.getAccountColor(mContext, accountHandle); + mPhoneAccountColorCache.put(accountHandle, color); + return color; + } + } + + @Override + public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) { + return mPhoneAccountCallWithNoteCache.get(accountHandle); + } else { + Boolean supportsCallWithNote = + PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle); + mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote); + return supportsCallWithNote; + } + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java new file mode 100644 index 000000000..4135cb7b8 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java @@ -0,0 +1,357 @@ +/* + * 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.dialer.app.contactinfo; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.util.ExpirableCache; +import java.lang.ref.WeakReference; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; + +/** + * This is a cache of contact details for the phone numbers in the c all log. The key is the phone + * number with the country in which teh call was placed or received. The content of the cache is + * expired (but not purged) whenever the application comes to the foreground. + * + *

This cache queues request for information and queries for information on a background thread, + * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction + * as needed. + * + *

TODO: Explore whether there is a pattern to remove external dependencies for starting and + * stopping the query thread. + */ +public class ContactInfoCache { + + private static final int REDRAW = 1; + private static final int START_THREAD = 2; + private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000; + + private final ExpirableCache mCache; + private final ContactInfoHelper mContactInfoHelper; + private final OnContactInfoChangedListener mOnContactInfoChangedListener; + private final BlockingQueue mUpdateRequests; + private final Handler mHandler; + private QueryThread mContactInfoQueryThread; + private volatile boolean mRequestProcessingDisabled = false; + + private static class InnerHandler extends Handler { + + private final WeakReference contactInfoCacheWeakReference; + + public InnerHandler(WeakReference contactInfoCacheWeakReference) { + this.contactInfoCacheWeakReference = contactInfoCacheWeakReference; + } + + @Override + public void handleMessage(Message msg) { + ContactInfoCache reference = contactInfoCacheWeakReference.get(); + if (reference == null) { + return; + } + switch (msg.what) { + case REDRAW: + reference.mOnContactInfoChangedListener.onContactInfoChanged(); + break; + case START_THREAD: + reference.startRequestProcessing(); + } + } + } + + public ContactInfoCache( + @NonNull ExpirableCache internalCache, + @NonNull ContactInfoHelper contactInfoHelper, + @NonNull OnContactInfoChangedListener listener) { + mCache = internalCache; + mContactInfoHelper = contactInfoHelper; + mOnContactInfoChangedListener = listener; + mUpdateRequests = new PriorityBlockingQueue<>(); + mHandler = new InnerHandler(new WeakReference<>(this)); + } + + public ContactInfo getValue( + String number, + String countryIso, + ContactInfo callLogContactInfo, + boolean remoteLookupIfNotFoundLocally) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ExpirableCache.CachedValue cachedInfo = mCache.getCachedValue(numberCountryIso); + ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue(); + if (cachedInfo == null) { + mCache.put(numberCountryIso, ContactInfo.EMPTY); + // Use the cached contact info from the call log. + info = callLogContactInfo; + // The db request should happen on a non-UI thread. + // Request the contact details immediately since they are currently missing. + int requestType = + remoteLookupIfNotFoundLocally + ? ContactInfoRequest.TYPE_LOCAL_AND_REMOTE + : ContactInfoRequest.TYPE_LOCAL; + enqueueRequest(number, countryIso, callLogContactInfo, /* immediate */ true, requestType); + // We will format the phone number when we make the background request. + } else { + if (cachedInfo.isExpired()) { + // The contact info is no longer up to date, we should request it. However, we + // do not need to request them immediately. + enqueueRequest( + number, + countryIso, + callLogContactInfo, /* immediate */ + false, + ContactInfoRequest.TYPE_LOCAL); + } else if (!callLogInfoMatches(callLogContactInfo, info)) { + // The call log information does not match the one we have, look it up again. + // We could simply update the call log directly, but that needs to be done in a + // background thread, so it is easier to simply request a new lookup, which will, as + // a side-effect, update the call log. + enqueueRequest( + number, + countryIso, + callLogContactInfo, /* immediate */ + false, + ContactInfoRequest.TYPE_LOCAL); + } + + if (info == ContactInfo.EMPTY) { + // Use the cached contact info from the call log. + info = callLogContactInfo; + } + } + return info; + } + + /** + * Queries the appropriate content provider for the contact associated with the number. + * + *

Upon completion it also updates the cache in the call log, if it is different from {@code + * callLogInfo}. + * + *

The number might be either a SIP address or a phone number. + * + *

It returns true if it updated the content of the cache and we should therefore tell the view + * to update its content. + */ + private boolean queryContactInfo(ContactInfoRequest request) { + ContactInfo info; + if (request.isLocalRequest()) { + info = mContactInfoHelper.lookupNumber(request.number, request.countryIso); + if (request.type == ContactInfoRequest.TYPE_LOCAL_AND_REMOTE) { + if (!mContactInfoHelper.hasName(info)) { + enqueueRequest( + request.number, + request.countryIso, + request.callLogInfo, + true, + ContactInfoRequest.TYPE_REMOTE); + return false; + } + } + } else { + info = mContactInfoHelper.lookupNumberInRemoteDirectory(request.number, request.countryIso); + } + + if (info == null) { + // The lookup failed, just return without requesting to update the view. + return false; + } + + // Check the existing entry in the cache: only if it has changed we should update the + // view. + NumberWithCountryIso numberCountryIso = + new NumberWithCountryIso(request.number, request.countryIso); + ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso); + + final boolean isRemoteSource = info.sourceType != 0; + + // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY} + // to avoid updating the data set for every new row that is scrolled into view. + + // Exception: Photo uris for contacts from remote sources are not cached in the call log + // cache, so we have to force a redraw for these contacts regardless. + boolean updated = + (existingInfo != ContactInfo.EMPTY || isRemoteSource) && !info.equals(existingInfo); + + // Store the data in the cache so that the UI thread can use to display it. Store it + // even if it has not changed so that it is marked as not expired. + mCache.put(numberCountryIso, info); + + // Update the call log even if the cache it is up-to-date: it is possible that the cache + // contains the value from a different call log entry. + mContactInfoHelper.updateCallLogContactInfo( + request.number, request.countryIso, info, request.callLogInfo); + if (!request.isLocalRequest()) { + mContactInfoHelper.updateCachedNumberLookupService(info); + } + return updated; + } + + /** + * After a delay, start the thread to begin processing requests. We perform lookups on a + * background thread, but this must be called to indicate the thread should be running. + */ + public void start() { + // Schedule a thread-creation message if the thread hasn't been created yet, as an + // optimization to queue fewer messages. + if (mContactInfoQueryThread == null) { + // TODO: Check whether this delay before starting to process is necessary. + mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS); + } + } + + /** + * Stops the thread and clears the queue of messages to process. This cleans up the thread for + * lookups so that it is not perpetually running. + */ + public void stop() { + stopRequestProcessing(); + } + + /** + * Starts a background thread to process contact-lookup requests, unless one has already been + * started. + */ + private synchronized void startRequestProcessing() { + // For unit-testing. + if (mRequestProcessingDisabled) { + return; + } + + // If a thread is already started, don't start another. + if (mContactInfoQueryThread != null) { + return; + } + + mContactInfoQueryThread = new QueryThread(); + mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY); + mContactInfoQueryThread.start(); + } + + public void invalidate() { + mCache.expireAll(); + stopRequestProcessing(); + } + + /** + * Stops the background thread that processes updates and cancels any pending requests to start + * it. + */ + private synchronized void stopRequestProcessing() { + // Remove any pending requests to start the processing thread. + mHandler.removeMessages(START_THREAD); + if (mContactInfoQueryThread != null) { + // Stop the thread; we are finished with it. + mContactInfoQueryThread.stopProcessing(); + mContactInfoQueryThread.interrupt(); + mContactInfoQueryThread = null; + } + } + + /** + * Enqueues a request to look up the contact details for the given phone number. + * + *

It also provides the current contact info stored in the call log for this number. + * + *

If the {@code immediate} parameter is true, it will start immediately the thread that looks + * up the contact information (if it has not been already started). Otherwise, it will be started + * with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MS}. + */ + private void enqueueRequest( + String number, + String countryIso, + ContactInfo callLogInfo, + boolean immediate, + @ContactInfoRequest.TYPE int type) { + ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo, type); + if (!mUpdateRequests.contains(request)) { + mUpdateRequests.offer(request); + } + + if (immediate) { + startRequestProcessing(); + } + } + + /** Checks whether the contact info from the call log matches the one from the contacts db. */ + private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) { + // The call log only contains a subset of the fields in the contacts db. Only check those. + return TextUtils.equals(callLogInfo.name, info.name) + && callLogInfo.type == info.type + && TextUtils.equals(callLogInfo.label, info.label); + } + + /** Sets whether processing of requests for contact details should be enabled. */ + public void disableRequestProcessing() { + mRequestProcessingDisabled = true; + } + + @VisibleForTesting + public void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + mCache.put(numberCountryIso, contactInfo); + } + + public interface OnContactInfoChangedListener { + + void onContactInfoChanged(); + } + + /* + * Handles requests for contact name and number type. + */ + private class QueryThread extends Thread { + + private volatile boolean mDone = false; + + public QueryThread() { + super("ContactInfoCache.QueryThread"); + } + + public void stopProcessing() { + mDone = true; + } + + @Override + public void run() { + boolean shouldRedraw = false; + while (true) { + // Check if thread is finished, and if so return immediately. + if (mDone) { + return; + } + + try { + ContactInfoRequest request = mUpdateRequests.take(); + shouldRedraw |= queryContactInfo(request); + if (shouldRedraw + && (mUpdateRequests.isEmpty() + || request.isLocalRequest() && !mUpdateRequests.peek().isLocalRequest())) { + shouldRedraw = false; + mHandler.sendEmptyMessage(REDRAW); + } + } catch (InterruptedException e) { + // Ignore and attempt to continue processing requests + } + } + } + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java new file mode 100644 index 000000000..5c2eb1dbb --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoRequest.java @@ -0,0 +1,122 @@ +/* + * 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.dialer.app.contactinfo; + +import android.support.annotation.IntDef; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; + +/** A request for contact details for the given number, used by the ContactInfoCache. */ +public final class ContactInfoRequest implements Comparable { + + private static final AtomicLong NEXT_SEQUENCE_NUMBER = new AtomicLong(0); + + private final long sequenceNumber; + + /** The number to look-up. */ + public final String number; + /** The country in which a call to or from this number was placed or received. */ + public final String countryIso; + /** The cached contact information stored in the call log. */ + public final ContactInfo callLogInfo; + + /** Is the request a remote lookup. Remote requests are treated as lower priority. */ + @TYPE public final int type; + + /** Specifies the type of the request is. */ + @IntDef( + value = { + TYPE_LOCAL, + TYPE_LOCAL_AND_REMOTE, + TYPE_REMOTE, + } + ) + @Retention(RetentionPolicy.SOURCE) + public @interface TYPE {} + + public static final int TYPE_LOCAL = 0; + /** If cannot find the contact locally, do remote lookup later. */ + public static final int TYPE_LOCAL_AND_REMOTE = 1; + + public static final int TYPE_REMOTE = 2; + + public ContactInfoRequest( + String number, String countryIso, ContactInfo callLogInfo, @TYPE int type) { + this.sequenceNumber = NEXT_SEQUENCE_NUMBER.getAndIncrement(); + this.number = number; + this.countryIso = countryIso; + this.callLogInfo = callLogInfo; + this.type = type; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof ContactInfoRequest)) { + return false; + } + + ContactInfoRequest other = (ContactInfoRequest) obj; + + if (!TextUtils.equals(number, other.number)) { + return false; + } + if (!TextUtils.equals(countryIso, other.countryIso)) { + return false; + } + if (!Objects.equals(callLogInfo, other.callLogInfo)) { + return false; + } + + if (type != other.type) { + return false; + } + + return true; + } + + public boolean isLocalRequest() { + return type == TYPE_LOCAL || type == TYPE_LOCAL_AND_REMOTE; + } + + @Override + public int hashCode() { + return Objects.hash(sequenceNumber, number, countryIso, callLogInfo, type); + } + + @Override + public int compareTo(ContactInfoRequest other) { + // Local query always comes first. + if (isLocalRequest() && !other.isLocalRequest()) { + return -1; + } + if (!isLocalRequest() && other.isLocalRequest()) { + return 1; + } + // First come first served. + return sequenceNumber < other.sequenceNumber ? -1 : 1; + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java new file mode 100644 index 000000000..a8c718502 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java @@ -0,0 +1,129 @@ +/* + * 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.dialer.app.contactinfo; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.v4.graphics.drawable.RoundedBitmapDrawable; +import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.app.R; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + +/** + * Class to create the appropriate contact icon from a ContactInfo. This class is for synchronous, + * blocking calls to generate bitmaps, while ContactCommons.ContactPhotoManager is to cache, manage + * and update a ImageView asynchronously. + */ +public class ContactPhotoLoader { + + private final Context mContext; + private final ContactInfo mContactInfo; + + public ContactPhotoLoader(Context context, ContactInfo contactInfo) { + mContext = Objects.requireNonNull(context); + mContactInfo = Objects.requireNonNull(contactInfo); + } + + private static Bitmap drawableToBitmap(Drawable drawable, int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } + + /** Create a contact photo icon bitmap appropriate for the ContactInfo. */ + public Bitmap loadPhotoIcon() { + Assert.isWorkerThread(); + int photoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); + return drawableToBitmap(getIcon(), photoSize, photoSize); + } + + @VisibleForTesting + Drawable getIcon() { + Drawable drawable = createPhotoIconDrawable(); + if (drawable == null) { + drawable = createLetterTileDrawable(); + } + return drawable; + } + + /** + * @return a {@link Drawable} of circular photo icon if the photo can be loaded, {@code null} + * otherwise. + */ + @Nullable + private Drawable createPhotoIconDrawable() { + if (mContactInfo.photoUri == null) { + return null; + } + try { + InputStream input = mContext.getContentResolver().openInputStream(mContactInfo.photoUri); + if (input == null) { + LogUtil.w( + "ContactPhotoLoader.createPhotoIconDrawable", + "createPhotoIconDrawable: InputStream is null"); + return null; + } + Bitmap bitmap = BitmapFactory.decodeStream(input); + input.close(); + + if (bitmap == null) { + LogUtil.w( + "ContactPhotoLoader.createPhotoIconDrawable", + "createPhotoIconDrawable: Bitmap is null"); + return null; + } + final RoundedBitmapDrawable drawable = + RoundedBitmapDrawableFactory.create(mContext.getResources(), bitmap); + drawable.setAntiAlias(true); + drawable.setCornerRadius(bitmap.getHeight() / 2); + return drawable; + } catch (IOException e) { + LogUtil.e("ContactPhotoLoader.createPhotoIconDrawable", e.toString()); + return null; + } + } + + /** @return a {@link LetterTileDrawable} based on the ContactInfo. */ + private Drawable createLetterTileDrawable() { + ContactInfoHelper helper = + new ContactInfoHelper(mContext, GeoUtil.getCurrentCountryIso(mContext)); + LetterTileDrawable drawable = new LetterTileDrawable(mContext.getResources()); + drawable.setCanonicalDialerLetterTileDetails( + mContactInfo.name, + mContactInfo.lookupKey, + LetterTileDrawable.SHAPE_CIRCLE, + helper.isBusiness(mContactInfo.sourceType) + ? LetterTileDrawable.TYPE_BUSINESS + : LetterTileDrawable.TYPE_DEFAULT); + return drawable; + } +} diff --git a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java new file mode 100644 index 000000000..aed51b507 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java @@ -0,0 +1,67 @@ +/* + * 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.dialer.app.contactinfo; + +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v7.app.AppCompatActivity; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.util.ExpirableCache; + +/** + * Fragment without any UI whose purpose is to retain an instance of {@link ExpirableCache} across + * configuration change through the use of {@link #setRetainInstance(boolean)}. This is done as + * opposed to implementing {@link android.os.Parcelable} as it is a less widespread change. + */ +public class ExpirableCacheHeadlessFragment extends Fragment { + + private static final String FRAGMENT_TAG = "ExpirableCacheHeadlessFragment"; + private static final int CONTACT_INFO_CACHE_SIZE = 100; + + private ExpirableCache retainedCache; + + @NonNull + public static ExpirableCacheHeadlessFragment attach(@NonNull AppCompatActivity parentActivity) { + return attach(parentActivity.getSupportFragmentManager()); + } + + @NonNull + private static ExpirableCacheHeadlessFragment attach(FragmentManager fragmentManager) { + ExpirableCacheHeadlessFragment fragment = + (ExpirableCacheHeadlessFragment) fragmentManager.findFragmentByTag(FRAGMENT_TAG); + if (fragment == null) { + fragment = new ExpirableCacheHeadlessFragment(); + // Allowing state loss since in rare cases this is called after activity's state is saved and + // it's fine if the cache is lost. + fragmentManager.beginTransaction().add(fragment, FRAGMENT_TAG).commitNowAllowingStateLoss(); + } + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + retainedCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); + setRetainInstance(true); + } + + public ExpirableCache getRetainedCache() { + return retainedCache; + } +} diff --git a/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java new file mode 100644 index 000000000..a005c447d --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/NumberWithCountryIso.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.dialer.app.contactinfo; + +import android.text.TextUtils; + +/** + * Stores a phone number of a call with the country code where it originally occurred. This object + * is used as a key in the {@code ContactInfoCache}. + * + *

The country does not necessarily specify the country of the phone number itself, but rather it + * is the country in which the user was in when the call was placed or received. + */ +public final class NumberWithCountryIso { + + public final String number; + public final String countryIso; + + public NumberWithCountryIso(String number, String countryIso) { + this.number = number; + this.countryIso = countryIso; + } + + @Override + public boolean equals(Object o) { + if (o == null) { + return false; + } + if (!(o instanceof NumberWithCountryIso)) { + return false; + } + NumberWithCountryIso other = (NumberWithCountryIso) o; + return TextUtils.equals(number, other.number) && TextUtils.equals(countryIso, other.countryIso); + } + + @Override + public int hashCode() { + int numberHashCode = number == null ? 0 : number.hashCode(); + int countryHashCode = countryIso == null ? 0 : countryIso.hashCode(); + + return numberHashCode ^ countryHashCode; + } +} diff --git a/java/com/android/dialer/app/dialpad/DialpadFragment.java b/java/com/android/dialer/app/dialpad/DialpadFragment.java new file mode 100644 index 000000000..18bb250ce --- /dev/null +++ b/java/com/android/dialer/app/dialpad/DialpadFragment.java @@ -0,0 +1,1689 @@ +/* + * 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.dialer.app.dialpad; + +import android.Manifest.permission; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.app.Fragment; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.ToneGenerator; +import android.net.Uri; +import android.os.Bundle; +import android.os.Trace; +import android.provider.Contacts.People; +import android.provider.Contacts.Phones; +import android.provider.Contacts.PhonesColumns; +import android.provider.Settings; +import android.support.annotation.VisibleForTesting; +import android.support.v4.content.ContextCompat; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberFormattingTextWatcher; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.RelativeLayout; +import android.widget.TextView; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.dialog.CallSubjectDialog; +import com.android.contacts.common.util.StopWatch; +import com.android.contacts.common.widget.FloatingActionButtonController; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.app.DialtactsActivity; +import com.android.dialer.app.R; +import com.android.dialer.app.SpecialCharSequenceMgr; +import com.android.dialer.app.calllog.CallLogAsync; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.LogUtil; +import com.android.dialer.dialpadview.DialpadKeyButton; +import com.android.dialer.dialpadview.DialpadView; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.PermissionsUtil; +import java.util.HashSet; +import java.util.List; + +/** Fragment that displays a twelve-key phone dialpad. */ +public class DialpadFragment extends Fragment + implements View.OnClickListener, + View.OnLongClickListener, + View.OnKeyListener, + AdapterView.OnItemClickListener, + TextWatcher, + PopupMenu.OnMenuItemClickListener, + DialpadKeyButton.OnPressedListener { + + private static final String TAG = "DialpadFragment"; + private static final boolean DEBUG = DialtactsActivity.DEBUG; + private static final String EMPTY_NUMBER = ""; + private static final char PAUSE = ','; + private static final char WAIT = ';'; + /** The length of DTMF tones in milliseconds */ + private static final int TONE_LENGTH_MS = 150; + + private static final int TONE_LENGTH_INFINITE = -1; + /** The DTMF tone volume relative to other sounds in the stream */ + private static final int TONE_RELATIVE_VOLUME = 80; + /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */ + private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF; + /** Identifier for the "Add Call" intent extra. */ + private static final String ADD_CALL_MODE_KEY = "add_call_mode"; + /** + * Identifier for intent extra for sending an empty Flash message for CDMA networks. This message + * is used by the network to simulate a press/depress of the "hookswitch" of a landline phone. Aka + * "empty flash". + * + *

TODO: Using an intent extra to tell the phone to send this flash is a temporary measure. To + * be replaced with an Telephony/TelecomManager call in the future. TODO: Keep in sync with the + * string defined in OutgoingCallBroadcaster.java in Phone app until this is replaced with the + * Telephony/Telecom API. + */ + private static final String EXTRA_SEND_EMPTY_FLASH = "com.android.phone.extra.SEND_EMPTY_FLASH"; + + private static final String PREF_DIGITS_FILLED_BY_INTENT = "pref_digits_filled_by_intent"; + private final Object mToneGeneratorLock = new Object(); + /** Set of dialpad keys that are currently being pressed */ + private final HashSet mPressedDialpadKeys = new HashSet(12); + // Last number dialed, retrieved asynchronously from the call DB + // in onCreate. This number is displayed when the user hits the + // send key and cleared in onPause. + private final CallLogAsync mCallLog = new CallLogAsync(); + private OnDialpadQueryChangedListener mDialpadQueryListener; + private DialpadView mDialpadView; + private EditText mDigits; + private int mDialpadSlideInDuration; + /** Remembers if we need to clear digits field when the screen is completely gone. */ + private boolean mClearDigitsOnStop; + + private View mOverflowMenuButton; + private PopupMenu mOverflowPopupMenu; + private View mDelete; + private ToneGenerator mToneGenerator; + private View mSpacer; + private FloatingActionButtonController mFloatingActionButtonController; + private ListView mDialpadChooser; + private DialpadChooserAdapter mDialpadChooserAdapter; + /** Regular expression prohibiting manual phone call. Can be empty, which means "no rule". */ + private String mProhibitedPhoneNumberRegexp; + + private PseudoEmergencyAnimator mPseudoEmergencyAnimator; + private String mLastNumberDialed = EMPTY_NUMBER; + + // determines if we want to playback local DTMF tones. + private boolean mDTMFToneEnabled; + private String mCurrentCountryIso; + private CallStateReceiver mCallStateReceiver; + private boolean mWasEmptyBeforeTextChange; + /** + * This field is set to true while processing an incoming DIAL intent, in order to make sure that + * SpecialCharSequenceMgr actions can be triggered by user input but *not* by a tel: URI passed by + * some other app. It will be set to false when all digits are cleared. + */ + private boolean mDigitsFilledByIntent; + + private boolean mStartedFromNewIntent = false; + private boolean mFirstLaunch = false; + private boolean mAnimate = false; + + /** + * Determines whether an add call operation is requested. + * + * @param intent The intent. + * @return {@literal true} if add call operation was requested. {@literal false} otherwise. + */ + public static boolean isAddCallMode(Intent intent) { + if (intent == null) { + return false; + } + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { + // see if we are "adding a call" from the InCallScreen; false by default. + return intent.getBooleanExtra(ADD_CALL_MODE_KEY, false); + } else { + return false; + } + } + + /** + * Format the provided string of digits into one that represents a properly formatted phone + * number. + * + * @param dialString String of characters to format + * @param normalizedNumber the E164 format number whose country code is used if the given + * phoneNumber doesn't have the country code. + * @param countryIso The country code representing the format to use if the provided normalized + * number is null or invalid. + * @return the provided string of digits as a formatted phone number, retaining any post-dial + * portion of the string. + */ + @VisibleForTesting + static String getFormattedDigits(String dialString, String normalizedNumber, String countryIso) { + String number = PhoneNumberUtils.extractNetworkPortion(dialString); + // Also retrieve the post dial portion of the provided data, so that the entire dial + // string can be reconstituted later. + final String postDial = PhoneNumberUtils.extractPostDialPortion(dialString); + + if (TextUtils.isEmpty(number)) { + return postDial; + } + + number = PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso); + + if (TextUtils.isEmpty(postDial)) { + return number; + } + + return number.concat(postDial); + } + + /** + * Returns true of the newDigit parameter can be added at the current selection point, otherwise + * returns false. Only prevents input of WAIT and PAUSE digits at an unsupported position. Fails + * early if start == -1 or start is larger than end. + */ + @VisibleForTesting + /* package */ static boolean canAddDigit(CharSequence digits, int start, int end, char newDigit) { + if (newDigit != WAIT && newDigit != PAUSE) { + throw new IllegalArgumentException( + "Should not be called for anything other than PAUSE & WAIT"); + } + + // False if no selection, or selection is reversed (end < start) + if (start == -1 || end < start) { + return false; + } + + // unsupported selection-out-of-bounds state + if (start > digits.length() || end > digits.length()) { + return false; + } + + // Special digit cannot be the first digit + if (start == 0) { + return false; + } + + if (newDigit == WAIT) { + // preceding char is ';' (WAIT) + if (digits.charAt(start - 1) == WAIT) { + return false; + } + + // next char is ';' (WAIT) + if ((digits.length() > end) && (digits.charAt(end) == WAIT)) { + return false; + } + } + + return true; + } + + private TelephonyManager getTelephonyManager() { + return (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE); + } + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + mWasEmptyBeforeTextChange = TextUtils.isEmpty(s); + } + + @Override + public void onTextChanged(CharSequence input, int start, int before, int changeCount) { + if (mWasEmptyBeforeTextChange != TextUtils.isEmpty(input)) { + final Activity activity = getActivity(); + if (activity != null) { + activity.invalidateOptionsMenu(); + updateMenuOverflowButton(mWasEmptyBeforeTextChange); + } + } + + // DTMF Tones do not need to be played here any longer - + // the DTMF dialer handles that functionality now. + } + + @Override + public void afterTextChanged(Editable input) { + // When DTMF dialpad buttons are being pressed, we delay SpecialCharSequenceMgr sequence, + // since some of SpecialCharSequenceMgr's behavior is too abrupt for the "touch-down" + // behavior. + if (!mDigitsFilledByIntent + && SpecialCharSequenceMgr.handleChars(getActivity(), input.toString(), mDigits)) { + // A special sequence was entered, clear the digits + mDigits.getText().clear(); + } + + if (isDigitsEmpty()) { + mDigitsFilledByIntent = false; + mDigits.setCursorVisible(false); + } + + if (mDialpadQueryListener != null) { + mDialpadQueryListener.onDialpadQueryChanged(mDigits.getText().toString()); + } + + updateDeleteButtonEnabledState(); + } + + @Override + public void onCreate(Bundle state) { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(state); + + mFirstLaunch = state == null; + + mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); + + mProhibitedPhoneNumberRegexp = + getResources().getString(R.string.config_prohibited_phone_number_regexp); + + if (state != null) { + mDigitsFilledByIntent = state.getBoolean(PREF_DIGITS_FILLED_BY_INTENT); + } + + mDialpadSlideInDuration = getResources().getInteger(R.integer.dialpad_slide_in_duration); + + if (mCallStateReceiver == null) { + IntentFilter callStateIntentFilter = + new IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED); + mCallStateReceiver = new CallStateReceiver(); + getActivity().registerReceiver(mCallStateReceiver, callStateIntentFilter); + } + Trace.endSection(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { + Trace.beginSection(TAG + " onCreateView"); + Trace.beginSection(TAG + " inflate view"); + final View fragmentView = inflater.inflate(R.layout.dialpad_fragment, container, false); + Trace.endSection(); + Trace.beginSection(TAG + " buildLayer"); + fragmentView.buildLayer(); + Trace.endSection(); + + Trace.beginSection(TAG + " setup views"); + + mDialpadView = (DialpadView) fragmentView.findViewById(R.id.dialpad_view); + mDialpadView.setCanDigitsBeEdited(true); + mDigits = mDialpadView.getDigits(); + mDigits.setKeyListener(UnicodeDialerKeyListener.INSTANCE); + mDigits.setOnClickListener(this); + mDigits.setOnKeyListener(this); + mDigits.setOnLongClickListener(this); + mDigits.addTextChangedListener(this); + mDigits.setElegantTextHeight(false); + + PhoneNumberFormattingTextWatcher watcher = + new PhoneNumberFormattingTextWatcher(GeoUtil.getCurrentCountryIso(getActivity())); + mDigits.addTextChangedListener(watcher); + + // Check for the presence of the keypad + View oneButton = fragmentView.findViewById(R.id.one); + if (oneButton != null) { + configureKeypadListeners(fragmentView); + } + + mDelete = mDialpadView.getDeleteButton(); + + if (mDelete != null) { + mDelete.setOnClickListener(this); + mDelete.setOnLongClickListener(this); + } + + mSpacer = fragmentView.findViewById(R.id.spacer); + mSpacer.setOnTouchListener( + new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (isDigitsEmpty()) { + if (getActivity() != null) { + return ((HostInterface) getActivity()).onDialpadSpacerTouchWithEmptyQuery(); + } + return true; + } + return false; + } + }); + + mDigits.setCursorVisible(false); + + // Set up the "dialpad chooser" UI; see showDialpadChooser(). + mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser); + mDialpadChooser.setOnItemClickListener(this); + + final View floatingActionButtonContainer = + fragmentView.findViewById(R.id.dialpad_floating_action_button_container); + final ImageButton floatingActionButton = + (ImageButton) fragmentView.findViewById(R.id.dialpad_floating_action_button); + floatingActionButton.setOnClickListener(this); + mFloatingActionButtonController = + new FloatingActionButtonController( + getActivity(), floatingActionButtonContainer, floatingActionButton); + Trace.endSection(); + Trace.endSection(); + return fragmentView; + } + + private boolean isLayoutReady() { + return mDigits != null; + } + + @VisibleForTesting + public EditText getDigitsWidget() { + return mDigits; + } + + /** @return true when {@link #mDigits} is actually filled by the Intent. */ + private boolean fillDigitsIfNecessary(Intent intent) { + // Only fills digits from an intent if it is a new intent. + // Otherwise falls back to the previously used number. + if (!mFirstLaunch && !mStartedFromNewIntent) { + return false; + } + + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) { + Uri uri = intent.getData(); + if (uri != null) { + if (PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) { + // Put the requested number into the input area + String data = uri.getSchemeSpecificPart(); + // Remember it is filled via Intent. + mDigitsFilledByIntent = true; + final String converted = + PhoneNumberUtils.convertKeypadLettersToDigits( + PhoneNumberUtils.replaceUnicodeDigits(data)); + setFormattedDigits(converted, null); + return true; + } else { + if (!PermissionsUtil.hasContactsPermissions(getActivity())) { + return false; + } + String type = intent.getType(); + if (People.CONTENT_ITEM_TYPE.equals(type) || Phones.CONTENT_ITEM_TYPE.equals(type)) { + // Query the phone number + Cursor c = + getActivity() + .getContentResolver() + .query( + intent.getData(), + new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY}, + null, + null, + null); + if (c != null) { + try { + if (c.moveToFirst()) { + // Remember it is filled via Intent. + mDigitsFilledByIntent = true; + // Put the number into the input area + setFormattedDigits(c.getString(0), c.getString(1)); + return true; + } + } finally { + c.close(); + } + } + } + } + } + } + return false; + } + + /** + * Checks the given Intent and changes dialpad's UI state. For example, if the Intent requires the + * screen to enter "Add Call" mode, this method will show correct UI for the mode. + */ + private void configureScreenFromIntent(Activity parent) { + // If we were not invoked with a DIAL intent, + if (!(parent instanceof DialtactsActivity)) { + setStartedFromNewIntent(false); + return; + } + // See if we were invoked with a DIAL intent. If we were, fill in the appropriate + // digits in the dialer field. + Intent intent = parent.getIntent(); + + if (!isLayoutReady()) { + // This happens typically when parent's Activity#onNewIntent() is called while + // Fragment#onCreateView() isn't called yet, and thus we cannot configure Views at + // this point. onViewCreate() should call this method after preparing layouts, so + // just ignore this call now. + LogUtil.i( + "DialpadFragment.configureScreenFromIntent", + "Screen configuration is requested before onCreateView() is called. Ignored"); + return; + } + + boolean needToShowDialpadChooser = false; + + // Be sure *not* to show the dialpad chooser if this is an + // explicit "Add call" action, though. + final boolean isAddCallMode = isAddCallMode(intent); + if (!isAddCallMode) { + + // Don't show the chooser when called via onNewIntent() and phone number is present. + // i.e. User clicks a telephone link from gmail for example. + // In this case, we want to show the dialpad with the phone number. + final boolean digitsFilled = fillDigitsIfNecessary(intent); + if (!(mStartedFromNewIntent && digitsFilled)) { + + final String action = intent.getAction(); + if (Intent.ACTION_DIAL.equals(action) + || Intent.ACTION_VIEW.equals(action) + || Intent.ACTION_MAIN.equals(action)) { + // If there's already an active call, bring up an intermediate UI to + // make the user confirm what they really want to do. + if (isPhoneInUse()) { + needToShowDialpadChooser = true; + } + } + } + } + showDialpadChooser(needToShowDialpadChooser); + setStartedFromNewIntent(false); + } + + public void setStartedFromNewIntent(boolean value) { + mStartedFromNewIntent = value; + } + + public void clearCallRateInformation() { + setCallRateInformation(null, null); + } + + public void setCallRateInformation(String countryName, String displayRate) { + mDialpadView.setCallRateInformation(countryName, displayRate); + } + + /** Sets formatted digits to digits field. */ + private void setFormattedDigits(String data, String normalizedNumber) { + final String formatted = getFormattedDigits(data, normalizedNumber, mCurrentCountryIso); + if (!TextUtils.isEmpty(formatted)) { + Editable digits = mDigits.getText(); + digits.replace(0, digits.length(), formatted); + // for some reason this isn't getting called in the digits.replace call above.. + // but in any case, this will make sure the background drawable looks right + afterTextChanged(digits); + } + } + + private void configureKeypadListeners(View fragmentView) { + final int[] buttonIds = + new int[] { + R.id.one, + R.id.two, + R.id.three, + R.id.four, + R.id.five, + R.id.six, + R.id.seven, + R.id.eight, + R.id.nine, + R.id.star, + R.id.zero, + R.id.pound + }; + + DialpadKeyButton dialpadKey; + + for (int i = 0; i < buttonIds.length; i++) { + dialpadKey = (DialpadKeyButton) fragmentView.findViewById(buttonIds[i]); + dialpadKey.setOnPressedListener(this); + } + + // Long-pressing one button will initiate Voicemail. + final DialpadKeyButton one = (DialpadKeyButton) fragmentView.findViewById(R.id.one); + one.setOnLongClickListener(this); + + // Long-pressing zero button will enter '+' instead. + final DialpadKeyButton zero = (DialpadKeyButton) fragmentView.findViewById(R.id.zero); + zero.setOnLongClickListener(this); + } + + @Override + public void onStart() { + Trace.beginSection(TAG + " onStart"); + super.onStart(); + // if the mToneGenerator creation fails, just continue without it. It is + // a local audio signal, and is not as important as the dtmf tone itself. + final long start = System.currentTimeMillis(); + synchronized (mToneGeneratorLock) { + if (mToneGenerator == null) { + try { + mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME); + } catch (RuntimeException e) { + LogUtil.e( + "DialpadFragment.onStart", + "Exception caught while creating local tone generator: " + e); + mToneGenerator = null; + } + } + } + final long total = System.currentTimeMillis() - start; + if (total > 50) { + LogUtil.i("DialpadFragment.onStart", "Time for ToneGenerator creation: " + total); + } + Trace.endSection(); + } + + @Override + public void onResume() { + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + final DialtactsActivity activity = (DialtactsActivity) getActivity(); + mDialpadQueryListener = activity; + + final StopWatch stopWatch = StopWatch.start("Dialpad.onResume"); + + // Query the last dialed number. Do it first because hitting + // the DB is 'slow'. This call is asynchronous. + queryLastOutgoingCall(); + + stopWatch.lap("qloc"); + + final ContentResolver contentResolver = activity.getContentResolver(); + + // retrieve the DTMF tone play back setting. + mDTMFToneEnabled = + Settings.System.getInt(contentResolver, Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; + + stopWatch.lap("dtwd"); + + stopWatch.lap("hptc"); + + mPressedDialpadKeys.clear(); + + configureScreenFromIntent(getActivity()); + + stopWatch.lap("fdin"); + + if (!isPhoneInUse()) { + // A sanity-check: the "dialpad chooser" UI should not be visible if the phone is idle. + showDialpadChooser(false); + } + + stopWatch.lap("hnt"); + + updateDeleteButtonEnabledState(); + + stopWatch.lap("bes"); + + stopWatch.stopAndLog(TAG, 50); + + // Populate the overflow menu in onResume instead of onCreate, so that if the SMS activity + // is disabled while Dialer is paused, the "Send a text message" option can be correctly + // removed when resumed. + mOverflowMenuButton = mDialpadView.getOverflowMenuButton(); + mOverflowPopupMenu = buildOptionsMenu(mOverflowMenuButton); + mOverflowMenuButton.setOnTouchListener(mOverflowPopupMenu.getDragToOpenListener()); + mOverflowMenuButton.setOnClickListener(this); + mOverflowMenuButton.setVisibility(isDigitsEmpty() ? View.INVISIBLE : View.VISIBLE); + + if (mFirstLaunch) { + // The onHiddenChanged callback does not get called the first time the fragment is + // attached, so call it ourselves here. + onHiddenChanged(false); + } + + mFirstLaunch = false; + Trace.endSection(); + } + + @Override + public void onPause() { + super.onPause(); + + // Make sure we don't leave this activity with a tone still playing. + stopTone(); + mPressedDialpadKeys.clear(); + + // TODO: I wonder if we should not check if the AsyncTask that + // lookup the last dialed number has completed. + mLastNumberDialed = EMPTY_NUMBER; // Since we are going to query again, free stale number. + + SpecialCharSequenceMgr.cleanup(); + } + + @Override + public void onStop() { + super.onStop(); + + synchronized (mToneGeneratorLock) { + if (mToneGenerator != null) { + mToneGenerator.release(); + mToneGenerator = null; + } + } + + if (mClearDigitsOnStop) { + mClearDigitsOnStop = false; + clearDialpad(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(PREF_DIGITS_FILLED_BY_INTENT, mDigitsFilledByIntent); + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mPseudoEmergencyAnimator != null) { + mPseudoEmergencyAnimator.destroy(); + mPseudoEmergencyAnimator = null; + } + getActivity().unregisterReceiver(mCallStateReceiver); + } + + private void keyPressed(int keyCode) { + if (getView() == null || getView().getTranslationY() != 0) { + return; + } + switch (keyCode) { + case KeyEvent.KEYCODE_1: + playTone(ToneGenerator.TONE_DTMF_1, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_2: + playTone(ToneGenerator.TONE_DTMF_2, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_3: + playTone(ToneGenerator.TONE_DTMF_3, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_4: + playTone(ToneGenerator.TONE_DTMF_4, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_5: + playTone(ToneGenerator.TONE_DTMF_5, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_6: + playTone(ToneGenerator.TONE_DTMF_6, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_7: + playTone(ToneGenerator.TONE_DTMF_7, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_8: + playTone(ToneGenerator.TONE_DTMF_8, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_9: + playTone(ToneGenerator.TONE_DTMF_9, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_0: + playTone(ToneGenerator.TONE_DTMF_0, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_POUND: + playTone(ToneGenerator.TONE_DTMF_P, TONE_LENGTH_INFINITE); + break; + case KeyEvent.KEYCODE_STAR: + playTone(ToneGenerator.TONE_DTMF_S, TONE_LENGTH_INFINITE); + break; + default: + break; + } + + getView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode); + mDigits.onKeyDown(keyCode, event); + + // If the cursor is at the end of the text we hide it. + final int length = mDigits.length(); + if (length == mDigits.getSelectionStart() && length == mDigits.getSelectionEnd()) { + mDigits.setCursorVisible(false); + } + } + + @Override + public boolean onKey(View view, int keyCode, KeyEvent event) { + if (view.getId() == R.id.digits) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + handleDialButtonPressed(); + return true; + } + } + return false; + } + + /** + * When a key is pressed, we start playing DTMF tone, do vibration, and enter the digit + * immediately. When a key is released, we stop the tone. Note that the "key press" event will be + * delivered by the system with certain amount of delay, it won't be synced with user's actual + * "touch-down" behavior. + */ + @Override + public void onPressed(View view, boolean pressed) { + if (DEBUG) { + LogUtil.d("DialpadFragment.onPressed", "view: " + view + ", pressed: " + pressed); + } + if (pressed) { + int resId = view.getId(); + if (resId == R.id.one) { + keyPressed(KeyEvent.KEYCODE_1); + } else if (resId == R.id.two) { + keyPressed(KeyEvent.KEYCODE_2); + } else if (resId == R.id.three) { + keyPressed(KeyEvent.KEYCODE_3); + } else if (resId == R.id.four) { + keyPressed(KeyEvent.KEYCODE_4); + } else if (resId == R.id.five) { + keyPressed(KeyEvent.KEYCODE_5); + } else if (resId == R.id.six) { + keyPressed(KeyEvent.KEYCODE_6); + } else if (resId == R.id.seven) { + keyPressed(KeyEvent.KEYCODE_7); + } else if (resId == R.id.eight) { + keyPressed(KeyEvent.KEYCODE_8); + } else if (resId == R.id.nine) { + keyPressed(KeyEvent.KEYCODE_9); + } else if (resId == R.id.zero) { + keyPressed(KeyEvent.KEYCODE_0); + } else if (resId == R.id.pound) { + keyPressed(KeyEvent.KEYCODE_POUND); + } else if (resId == R.id.star) { + keyPressed(KeyEvent.KEYCODE_STAR); + } else { + LogUtil.e( + "DialpadFragment.onPressed", "Unexpected onTouch(ACTION_DOWN) event from: " + view); + } + mPressedDialpadKeys.add(view); + } else { + mPressedDialpadKeys.remove(view); + if (mPressedDialpadKeys.isEmpty()) { + stopTone(); + } + } + } + + /** + * Called by the containing Activity to tell this Fragment to build an overflow options menu for + * display by the container when appropriate. + * + * @param invoker the View that invoked the options menu, to act as an anchor location. + */ + private PopupMenu buildOptionsMenu(View invoker) { + final PopupMenu popupMenu = + new PopupMenu(getActivity(), invoker) { + @Override + public void show() { + final Menu menu = getMenu(); + + boolean enable = !isDigitsEmpty(); + for (int i = 0; i < menu.size(); i++) { + MenuItem item = menu.getItem(i); + item.setEnabled(enable); + if (item.getItemId() == R.id.menu_call_with_note) { + item.setVisible(CallUtil.isCallWithSubjectSupported(getContext())); + } + } + super.show(); + } + }; + popupMenu.inflate(R.menu.dialpad_options); + popupMenu.setOnMenuItemClickListener(this); + return popupMenu; + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.dialpad_floating_action_button) { + view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + handleDialButtonPressed(); + } else if (resId == R.id.deleteButton) { + keyPressed(KeyEvent.KEYCODE_DEL); + } else if (resId == R.id.digits) { + if (!isDigitsEmpty()) { + mDigits.setCursorVisible(true); + } + } else if (resId == R.id.dialpad_overflow) { + mOverflowPopupMenu.show(); + } else { + LogUtil.w("DialpadFragment.onClick", "Unexpected event from: " + view); + return; + } + } + + @Override + public boolean onLongClick(View view) { + final Editable digits = mDigits.getText(); + final int id = view.getId(); + if (id == R.id.deleteButton) { + digits.clear(); + return true; + } else if (id == R.id.one) { + if (isDigitsEmpty() || TextUtils.equals(mDigits.getText(), "1")) { + // We'll try to initiate voicemail and thus we want to remove irrelevant string. + removePreviousDigitIfPossible('1'); + + List subscriptionAccountHandles = + PhoneAccountUtils.getSubscriptionPhoneAccounts(getActivity()); + boolean hasUserSelectedDefault = + subscriptionAccountHandles.contains( + TelecomUtil.getDefaultOutgoingPhoneAccount( + getActivity(), PhoneAccount.SCHEME_VOICEMAIL)); + boolean needsAccountDisambiguation = + subscriptionAccountHandles.size() > 1 && !hasUserSelectedDefault; + + if (needsAccountDisambiguation || isVoicemailAvailable()) { + // On a multi-SIM phone, if the user has not selected a default + // subscription, initiate a call to voicemail so they can select an account + // from the "Call with" dialog. + callVoicemail(); + } else if (getActivity() != null) { + // Voicemail is unavailable maybe because Airplane mode is turned on. + // Check the current status and show the most appropriate error message. + final boolean isAirplaneModeOn = + Settings.System.getInt( + getActivity().getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0) + != 0; + if (isAirplaneModeOn) { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_voicemail_airplane_mode_message); + dialogFragment.show(getFragmentManager(), "voicemail_request_during_airplane_mode"); + } else { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_voicemail_not_ready_message); + dialogFragment.show(getFragmentManager(), "voicemail_not_ready"); + } + } + return true; + } + return false; + } else if (id == R.id.zero) { + if (mPressedDialpadKeys.contains(view)) { + // If the zero key is currently pressed, then the long press occurred by touch + // (and not via other means like certain accessibility input methods). + // Remove the '0' that was input when the key was first pressed. + removePreviousDigitIfPossible('0'); + } + keyPressed(KeyEvent.KEYCODE_PLUS); + stopTone(); + mPressedDialpadKeys.remove(view); + return true; + } else if (id == R.id.digits) { + mDigits.setCursorVisible(true); + return false; + } + return false; + } + + /** + * Remove the digit just before the current position of the cursor, iff the following conditions + * are true: 1) The cursor is not positioned at index 0. 2) The digit before the current cursor + * position matches the current digit. + * + * @param digit to remove from the digits view. + */ + private void removePreviousDigitIfPossible(char digit) { + final int currentPosition = mDigits.getSelectionStart(); + if (currentPosition > 0 && digit == mDigits.getText().charAt(currentPosition - 1)) { + mDigits.setSelection(currentPosition); + mDigits.getText().delete(currentPosition - 1, currentPosition); + } + } + + public void callVoicemail() { + DialerUtils.startActivityWithErrorToast( + getActivity(), + new CallIntentBuilder(CallUtil.getVoicemailUri(), CallInitiationType.Type.DIALPAD).build()); + hideAndClearDialpad(false); + } + + private void hideAndClearDialpad(boolean animate) { + ((DialtactsActivity) getActivity()).hideDialpadFragment(animate, true); + } + + /** + * In most cases, when the dial button is pressed, there is a number in digits area. Pack it in + * the intent, start the outgoing call broadcast as a separate task and finish this activity. + * + *

When there is no digit and the phone is CDMA and off hook, we're sending a blank flash for + * CDMA. CDMA networks use Flash messages when special processing needs to be done, mainly for + * 3-way or call waiting scenarios. Presumably, here we're in a special 3-way scenario where the + * network needs a blank flash before being able to add the new participant. (This is not the case + * with all 3-way calls, just certain CDMA infrastructures.) + * + *

Otherwise, there is no digit, display the last dialed number. Don't finish since the user + * may want to edit it. The user needs to press the dial button again, to dial it (general case + * described above). + */ + private void handleDialButtonPressed() { + if (isDigitsEmpty()) { // No number entered. + handleDialButtonClickWithEmptyDigits(); + } else { + final String number = mDigits.getText().toString(); + + // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated + // test equipment. + // TODO: clean it up. + if (number != null + && !TextUtils.isEmpty(mProhibitedPhoneNumberRegexp) + && number.matches(mProhibitedPhoneNumberRegexp)) { + LogUtil.i( + "DialpadFragment.handleDialButtonPressed", + "The phone number is prohibited explicitly by a rule."); + if (getActivity() != null) { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message); + dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog"); + } + + // Clear the digits just in case. + clearDialpad(); + } else { + final Intent intent = + new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD).build(); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + hideAndClearDialpad(false); + } + } + } + + public void clearDialpad() { + if (mDigits != null) { + mDigits.getText().clear(); + } + } + + private void handleDialButtonClickWithEmptyDigits() { + if (phoneIsCdma() && isPhoneInUse()) { + // TODO: Move this logic into services/Telephony + // + // This is really CDMA specific. On GSM is it possible + // to be off hook and wanted to add a 3rd party using + // the redial feature. + startActivity(newFlashIntent()); + } else { + if (!TextUtils.isEmpty(mLastNumberDialed)) { + // Recall the last number dialed. + mDigits.setText(mLastNumberDialed); + + // ...and move the cursor to the end of the digits string, + // so you'll be able to delete digits using the Delete + // button (just as if you had typed the number manually.) + // + // Note we use mDigits.getText().length() here, not + // mLastNumberDialed.length(), since the EditText widget now + // contains a *formatted* version of mLastNumberDialed (due to + // mTextWatcher) and its length may have changed. + mDigits.setSelection(mDigits.getText().length()); + } else { + // There's no "last number dialed" or the + // background query is still running. There's + // nothing useful for the Dial button to do in + // this case. Note: with a soft dial button, this + // can never happens since the dial button is + // disabled under these conditons. + playTone(ToneGenerator.TONE_PROP_NACK); + } + } + } + + /** Plays the specified tone for TONE_LENGTH_MS milliseconds. */ + private void playTone(int tone) { + playTone(tone, TONE_LENGTH_MS); + } + + /** + * Play the specified tone for the specified milliseconds + * + *

The tone is played locally, using the audio stream for phone calls. Tones are played only if + * the "Audible touch tones" user preference is checked, and are NOT played if the device is in + * silent mode. + * + *

The tone length can be -1, meaning "keep playing the tone." If the caller does so, it should + * call stopTone() afterward. + * + * @param tone a tone code from {@link ToneGenerator} + * @param durationMs tone length. + */ + private void playTone(int tone, int durationMs) { + // if local tone playback is disabled, just return. + if (!mDTMFToneEnabled) { + return; + } + + // Also do nothing if the phone is in silent mode. + // We need to re-check the ringer mode for *every* playTone() + // call, rather than keeping a local flag that's updated in + // onResume(), since it's possible to toggle silent mode without + // leaving the current activity (via the ENDCALL-longpress menu.) + AudioManager audioManager = + (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE); + int ringerMode = audioManager.getRingerMode(); + if ((ringerMode == AudioManager.RINGER_MODE_SILENT) + || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) { + return; + } + + synchronized (mToneGeneratorLock) { + if (mToneGenerator == null) { + LogUtil.w("DialpadFragment.playTone", "mToneGenerator == null, tone: " + tone); + return; + } + + // Start the new tone (will stop any playing tone) + mToneGenerator.startTone(tone, durationMs); + } + } + + /** Stop the tone if it is played. */ + private void stopTone() { + // if local tone playback is disabled, just return. + if (!mDTMFToneEnabled) { + return; + } + synchronized (mToneGeneratorLock) { + if (mToneGenerator == null) { + LogUtil.w("DialpadFragment.stopTone", "mToneGenerator == null"); + return; + } + mToneGenerator.stopTone(); + } + } + + /** + * Brings up the "dialpad chooser" UI in place of the usual Dialer elements (the textfield/button + * and the dialpad underneath). + * + *

We show this UI if the user brings up the Dialer while a call is already in progress, since + * there's a good chance we got here accidentally (and the user really wanted the in-call dialpad + * instead). So in this situation we display an intermediate UI that lets the user explicitly + * choose between the in-call dialpad ("Use touch tone keypad") and the regular Dialer ("Add + * call"). (Or, the option "Return to call in progress" just goes back to the in-call UI with no + * dialpad at all.) + * + * @param enabled If true, show the "dialpad chooser" instead of the regular Dialer UI + */ + private void showDialpadChooser(boolean enabled) { + if (getActivity() == null) { + return; + } + // Check if onCreateView() is already called by checking one of View objects. + if (!isLayoutReady()) { + return; + } + + if (enabled) { + LogUtil.i("DialpadFragment.showDialpadChooser", "Showing dialpad chooser!"); + if (mDialpadView != null) { + mDialpadView.setVisibility(View.GONE); + } + + mFloatingActionButtonController.setVisible(false); + mDialpadChooser.setVisibility(View.VISIBLE); + + // Instantiate the DialpadChooserAdapter and hook it up to the + // ListView. We do this only once. + if (mDialpadChooserAdapter == null) { + mDialpadChooserAdapter = new DialpadChooserAdapter(getActivity()); + } + mDialpadChooser.setAdapter(mDialpadChooserAdapter); + } else { + LogUtil.i("DialpadFragment.showDialpadChooser", "Displaying normal Dialer UI."); + if (mDialpadView != null) { + mDialpadView.setVisibility(View.VISIBLE); + } else { + mDigits.setVisibility(View.VISIBLE); + } + + // mFloatingActionButtonController must also be 'scaled in', in order to be visible after + // 'scaleOut()' hidden method. + if (!mFloatingActionButtonController.isVisible()) { + // Just call 'scaleIn()' method if the mFloatingActionButtonController was not already + // previously visible. + mFloatingActionButtonController.scaleIn(0); + mFloatingActionButtonController.setVisible(true); + } + mDialpadChooser.setVisibility(View.GONE); + } + } + + /** @return true if we're currently showing the "dialpad chooser" UI. */ + private boolean isDialpadChooserVisible() { + return mDialpadChooser.getVisibility() == View.VISIBLE; + } + + /** Handle clicks from the dialpad chooser. */ + @Override + public void onItemClick(AdapterView parent, View v, int position, long id) { + DialpadChooserAdapter.ChoiceItem item = + (DialpadChooserAdapter.ChoiceItem) parent.getItemAtPosition(position); + int itemId = item.id; + if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_USE_DTMF_DIALPAD) { + // Fire off an intent to go back to the in-call UI + // with the dialpad visible. + returnToInCallScreen(true); + } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_RETURN_TO_CALL) { + // Fire off an intent to go back to the in-call UI + // (with the dialpad hidden). + returnToInCallScreen(false); + } else if (itemId == DialpadChooserAdapter.DIALPAD_CHOICE_ADD_NEW_CALL) { + // Ok, guess the user really did want to be here (in the + // regular Dialer) after all. Bring back the normal Dialer UI. + showDialpadChooser(false); + } else { + LogUtil.w("DialpadFragment.onItemClick", "Unexpected itemId: " + itemId); + } + } + + /** + * Returns to the in-call UI (where there's presumably a call in progress) in response to the user + * selecting "use touch tone keypad" or "return to call" from the dialpad chooser. + */ + private void returnToInCallScreen(boolean showDialpad) { + TelecomUtil.showInCallScreen(getActivity(), showDialpad); + + // Finally, finish() ourselves so that we don't stay on the + // activity stack. + // Note that we do this whether or not the showCallScreenWithDialpad() + // call above had any effect or not! (That call is a no-op if the + // phone is idle, which can happen if the current call ends while + // the dialpad chooser is up. In this case we can't show the + // InCallScreen, and there's no point staying here in the Dialer, + // so we just take the user back where he came from...) + getActivity().finish(); + } + + /** + * @return true if the phone is "in use", meaning that at least one line is active (ie. off hook + * or ringing or dialing, or on hold). + */ + private boolean isPhoneInUse() { + final Context context = getActivity(); + if (context != null) { + return TelecomUtil.isInCall(context); + } + return false; + } + + /** @return true if the phone is a CDMA phone type */ + private boolean phoneIsCdma() { + return getTelephonyManager().getPhoneType() == TelephonyManager.PHONE_TYPE_CDMA; + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + int resId = item.getItemId(); + if (resId == R.id.menu_2s_pause) { + updateDialString(PAUSE); + return true; + } else if (resId == R.id.menu_add_wait) { + updateDialString(WAIT); + return true; + } else if (resId == R.id.menu_call_with_note) { + CallSubjectDialog.start(getActivity(), mDigits.getText().toString()); + hideAndClearDialpad(false); + return true; + } else { + return false; + } + } + + /** + * Updates the dial string (mDigits) after inserting a Pause character (,) or Wait character (;). + */ + private void updateDialString(char newDigit) { + if (newDigit != WAIT && newDigit != PAUSE) { + throw new IllegalArgumentException("Not expected for anything other than PAUSE & WAIT"); + } + + int selectionStart; + int selectionEnd; + + // SpannableStringBuilder editable_text = new SpannableStringBuilder(mDigits.getText()); + int anchor = mDigits.getSelectionStart(); + int point = mDigits.getSelectionEnd(); + + selectionStart = Math.min(anchor, point); + selectionEnd = Math.max(anchor, point); + + if (selectionStart == -1) { + selectionStart = selectionEnd = mDigits.length(); + } + + Editable digits = mDigits.getText(); + + if (canAddDigit(digits, selectionStart, selectionEnd, newDigit)) { + digits.replace(selectionStart, selectionEnd, Character.toString(newDigit)); + + if (selectionStart != selectionEnd) { + // Unselect: back to a regular cursor, just pass the character inserted. + mDigits.setSelection(selectionStart + 1); + } + } + } + + /** Update the enabledness of the "Dial" and "Backspace" buttons if applicable. */ + private void updateDeleteButtonEnabledState() { + if (getActivity() == null) { + return; + } + final boolean digitsNotEmpty = !isDigitsEmpty(); + mDelete.setEnabled(digitsNotEmpty); + } + + /** + * Handle transitions for the menu button depending on the state of the digits edit text. + * Transition out when going from digits to no digits and transition in when the first digit is + * pressed. + * + * @param transitionIn True if transitioning in, False if transitioning out + */ + private void updateMenuOverflowButton(boolean transitionIn) { + mOverflowMenuButton = mDialpadView.getOverflowMenuButton(); + if (transitionIn) { + AnimUtils.fadeIn(mOverflowMenuButton, AnimUtils.DEFAULT_DURATION); + } else { + AnimUtils.fadeOut(mOverflowMenuButton, AnimUtils.DEFAULT_DURATION); + } + } + + /** + * Check if voicemail is enabled/accessible. + * + * @return true if voicemail is enabled and accessible. Note that this can be false "temporarily" + * after the app boot. + */ + private boolean isVoicemailAvailable() { + try { + PhoneAccountHandle defaultUserSelectedAccount = + TelecomUtil.getDefaultOutgoingPhoneAccount(getActivity(), PhoneAccount.SCHEME_VOICEMAIL); + if (defaultUserSelectedAccount == null) { + // In a single-SIM phone, there is no default outgoing phone account selected by + // the user, so just call TelephonyManager#getVoicemailNumber directly. + return !TextUtils.isEmpty(getTelephonyManager().getVoiceMailNumber()); + } else { + return !TextUtils.isEmpty( + TelecomUtil.getVoicemailNumber(getActivity(), defaultUserSelectedAccount)); + } + } catch (SecurityException se) { + // Possibly no READ_PHONE_STATE privilege. + LogUtil.w( + "DialpadFragment.isVoicemailAvailable", + "SecurityException is thrown. Maybe privilege isn't sufficient."); + } + return false; + } + + /** @return true if the widget with the phone number digits is empty. */ + private boolean isDigitsEmpty() { + return mDigits.length() == 0; + } + + /** + * Starts the asyn query to get the last dialed/outgoing number. When the background query + * finishes, mLastNumberDialed is set to the last dialed number or an empty string if none exists + * yet. + */ + private void queryLastOutgoingCall() { + mLastNumberDialed = EMPTY_NUMBER; + if (ContextCompat.checkSelfPermission(getActivity(), permission.READ_CALL_LOG) + != PackageManager.PERMISSION_GRANTED) { + return; + } + CallLogAsync.GetLastOutgoingCallArgs lastCallArgs = + new CallLogAsync.GetLastOutgoingCallArgs( + getActivity(), + new CallLogAsync.OnLastOutgoingCallComplete() { + @Override + public void lastOutgoingCall(String number) { + // TODO: Filter out emergency numbers if + // the carrier does not want redial for + // these. + // If the fragment has already been detached since the last time + // we called queryLastOutgoingCall in onResume there is no point + // doing anything here. + if (getActivity() == null) { + return; + } + mLastNumberDialed = number; + updateDeleteButtonEnabledState(); + } + }); + mCallLog.getLastOutgoingCall(lastCallArgs); + } + + private Intent newFlashIntent() { + Intent intent = new CallIntentBuilder(EMPTY_NUMBER, CallInitiationType.Type.DIALPAD).build(); + intent.putExtra(EXTRA_SEND_EMPTY_FLASH, true); + return intent; + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + final DialtactsActivity activity = (DialtactsActivity) getActivity(); + final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); + if (activity == null) { + return; + } + if (!hidden && !isDialpadChooserVisible()) { + if (mAnimate) { + dialpadView.animateShow(); + } + mFloatingActionButtonController.setVisible(false); + mFloatingActionButtonController.scaleIn(mAnimate ? mDialpadSlideInDuration : 0); + activity.onDialpadShown(); + mDigits.requestFocus(); + } + if (hidden) { + if (mAnimate) { + mFloatingActionButtonController.scaleOut(); + } else { + mFloatingActionButtonController.setVisible(false); + } + } + } + + public boolean getAnimate() { + return mAnimate; + } + + public void setAnimate(boolean value) { + mAnimate = value; + } + + public void setYFraction(float yFraction) { + ((DialpadSlidingRelativeLayout) getView()).setYFraction(yFraction); + } + + public int getDialpadHeight() { + if (mDialpadView == null) { + return 0; + } + return mDialpadView.getHeight(); + } + + public void process_quote_emergency_unquote(String query) { + if (PseudoEmergencyAnimator.PSEUDO_EMERGENCY_NUMBER.equals(query)) { + if (mPseudoEmergencyAnimator == null) { + mPseudoEmergencyAnimator = + new PseudoEmergencyAnimator( + new PseudoEmergencyAnimator.ViewProvider() { + @Override + public View getView() { + return DialpadFragment.this.getView(); + } + }); + } + mPseudoEmergencyAnimator.start(); + } else { + if (mPseudoEmergencyAnimator != null) { + mPseudoEmergencyAnimator.end(); + } + } + } + + public interface OnDialpadQueryChangedListener { + + void onDialpadQueryChanged(String query); + } + + public interface HostInterface { + + /** + * Notifies the parent activity that the space above the dialpad has been tapped with no query + * in the dialpad present. In most situations this will cause the dialpad to be dismissed, + * unless there happens to be content showing. + */ + boolean onDialpadSpacerTouchWithEmptyQuery(); + } + + /** + * LinearLayout with getter and setter methods for the translationY property using floats, for + * animation purposes. + */ + public static class DialpadSlidingRelativeLayout extends RelativeLayout { + + public DialpadSlidingRelativeLayout(Context context) { + super(context); + } + + public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DialpadSlidingRelativeLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @UsedByReflection(value = "dialpad_fragment.xml") + public float getYFraction() { + final int height = getHeight(); + if (height == 0) { + return 0; + } + return getTranslationY() / height; + } + + @UsedByReflection(value = "dialpad_fragment.xml") + public void setYFraction(float yFraction) { + setTranslationY(yFraction * getHeight()); + } + } + + public static class ErrorDialogFragment extends DialogFragment { + + private static final String ARG_TITLE_RES_ID = "argTitleResId"; + private static final String ARG_MESSAGE_RES_ID = "argMessageResId"; + private int mTitleResId; + private int mMessageResId; + + public static ErrorDialogFragment newInstance(int messageResId) { + return newInstance(0, messageResId); + } + + public static ErrorDialogFragment newInstance(int titleResId, int messageResId) { + final ErrorDialogFragment fragment = new ErrorDialogFragment(); + final Bundle args = new Bundle(); + args.putInt(ARG_TITLE_RES_ID, titleResId); + args.putInt(ARG_MESSAGE_RES_ID, messageResId); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID); + mMessageResId = getArguments().getInt(ARG_MESSAGE_RES_ID); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + if (mTitleResId != 0) { + builder.setTitle(mTitleResId); + } + if (mMessageResId != 0) { + builder.setMessage(mMessageResId); + } + builder.setPositiveButton( + android.R.string.ok, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dismiss(); + } + }); + return builder.create(); + } + } + + /** + * Simple list adapter, binding to an icon + text label for each item in the "dialpad chooser" + * list. + */ + private static class DialpadChooserAdapter extends BaseAdapter { + + // IDs for the possible "choices": + static final int DIALPAD_CHOICE_USE_DTMF_DIALPAD = 101; + static final int DIALPAD_CHOICE_RETURN_TO_CALL = 102; + static final int DIALPAD_CHOICE_ADD_NEW_CALL = 103; + private static final int NUM_ITEMS = 3; + private LayoutInflater mInflater; + private ChoiceItem[] mChoiceItems = new ChoiceItem[NUM_ITEMS]; + + public DialpadChooserAdapter(Context context) { + // Cache the LayoutInflate to avoid asking for a new one each time. + mInflater = LayoutInflater.from(context); + + // Initialize the possible choices. + // TODO: could this be specified entirely in XML? + + // - "Use touch tone keypad" + mChoiceItems[0] = + new ChoiceItem( + context.getString(R.string.dialer_useDtmfDialpad), + BitmapFactory.decodeResource( + context.getResources(), R.drawable.ic_dialer_fork_tt_keypad), + DIALPAD_CHOICE_USE_DTMF_DIALPAD); + + // - "Return to call in progress" + mChoiceItems[1] = + new ChoiceItem( + context.getString(R.string.dialer_returnToInCallScreen), + BitmapFactory.decodeResource( + context.getResources(), R.drawable.ic_dialer_fork_current_call), + DIALPAD_CHOICE_RETURN_TO_CALL); + + // - "Add call" + mChoiceItems[2] = + new ChoiceItem( + context.getString(R.string.dialer_addAnotherCall), + BitmapFactory.decodeResource( + context.getResources(), R.drawable.ic_dialer_fork_add_call), + DIALPAD_CHOICE_ADD_NEW_CALL); + } + + @Override + public int getCount() { + return NUM_ITEMS; + } + + /** Return the ChoiceItem for a given position. */ + @Override + public Object getItem(int position) { + return mChoiceItems[position]; + } + + /** Return a unique ID for each possible choice. */ + @Override + public long getItemId(int position) { + return position; + } + + /** Make a view for each row. */ + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // When convertView is non-null, we can reuse it (there's no need + // to reinflate it.) + if (convertView == null) { + convertView = mInflater.inflate(R.layout.dialpad_chooser_list_item, null); + } + + TextView text = (TextView) convertView.findViewById(R.id.text); + text.setText(mChoiceItems[position].text); + + ImageView icon = (ImageView) convertView.findViewById(R.id.icon); + icon.setImageBitmap(mChoiceItems[position].icon); + + return convertView; + } + + // Simple struct for a single "choice" item. + static class ChoiceItem { + + String text; + Bitmap icon; + int id; + + public ChoiceItem(String s, Bitmap b, int i) { + text = s; + icon = b; + id = i; + } + } + } + + private class CallStateReceiver extends BroadcastReceiver { + + /** + * Receive call state changes so that we can take down the "dialpad chooser" if the phone + * becomes idle while the chooser UI is visible. + */ + @Override + public void onReceive(Context context, Intent intent) { + String state = intent.getStringExtra(TelephonyManager.EXTRA_STATE); + if ((TextUtils.equals(state, TelephonyManager.EXTRA_STATE_IDLE) + || TextUtils.equals(state, TelephonyManager.EXTRA_STATE_OFFHOOK)) + && isDialpadChooserVisible()) { + // Note there's a race condition in the UI here: the + // dialpad chooser could conceivably disappear (on its + // own) at the exact moment the user was trying to select + // one of the choices, which would be confusing. (But at + // least that's better than leaving the dialpad chooser + // onscreen, but useless...) + showDialpadChooser(false); + } + } + } +} diff --git a/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java b/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java new file mode 100644 index 000000000..2ffacb6d8 --- /dev/null +++ b/java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java @@ -0,0 +1,161 @@ +/* + * 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.dialer.app.dialpad; + +import android.animation.Animator; +import android.animation.Animator.AnimatorListener; +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.animation.ValueAnimator.AnimatorUpdateListener; +import android.content.Context; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LightingColorFilter; +import android.os.Handler; +import android.os.Vibrator; +import android.view.View; +import com.android.dialer.app.R; + +/** Animates the dial button on "emergency" phone numbers. */ +public class PseudoEmergencyAnimator { + + public static final String PSEUDO_EMERGENCY_NUMBER = "01189998819991197253"; + private static final int VIBRATE_LENGTH_MILLIS = 200; + private static final int ITERATION_LENGTH_MILLIS = 1000; + private static final int ANIMATION_ITERATION_COUNT = 6; + private ViewProvider mViewProvider; + private ValueAnimator mPseudoEmergencyColorAnimator; + + PseudoEmergencyAnimator(ViewProvider viewProvider) { + mViewProvider = viewProvider; + } + + public void destroy() { + end(); + mViewProvider = null; + } + + public void start() { + if (mPseudoEmergencyColorAnimator == null) { + Integer colorFrom = Color.BLUE; + Integer colorTo = Color.RED; + mPseudoEmergencyColorAnimator = + ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo); + + mPseudoEmergencyColorAnimator.addUpdateListener( + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + try { + int color = (int) animator.getAnimatedValue(); + ColorFilter colorFilter = new LightingColorFilter(Color.BLACK, color); + + View floatingActionButtonContainer = + getView().findViewById(R.id.dialpad_floating_action_button_container); + if (floatingActionButtonContainer != null) { + floatingActionButtonContainer.getBackground().setColorFilter(colorFilter); + } + } catch (Exception e) { + animator.cancel(); + } + } + }); + + mPseudoEmergencyColorAnimator.addListener( + new AnimatorListener() { + @Override + public void onAnimationCancel(Animator animation) {} + + @Override + public void onAnimationRepeat(Animator animation) { + try { + vibrate(VIBRATE_LENGTH_MILLIS); + } catch (Exception e) { + animation.cancel(); + } + } + + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationEnd(Animator animation) { + try { + View floatingActionButtonContainer = + getView().findViewById(R.id.dialpad_floating_action_button_container); + if (floatingActionButtonContainer != null) { + floatingActionButtonContainer.getBackground().clearColorFilter(); + } + + new Handler() + .postDelayed( + new Runnable() { + @Override + public void run() { + try { + vibrate(VIBRATE_LENGTH_MILLIS); + } catch (Exception e) { + // ignored + } + } + }, + ITERATION_LENGTH_MILLIS); + } catch (Exception e) { + animation.cancel(); + } + } + }); + + mPseudoEmergencyColorAnimator.setDuration(VIBRATE_LENGTH_MILLIS); + mPseudoEmergencyColorAnimator.setRepeatMode(ValueAnimator.REVERSE); + mPseudoEmergencyColorAnimator.setRepeatCount(ANIMATION_ITERATION_COUNT); + } + if (!mPseudoEmergencyColorAnimator.isStarted()) { + mPseudoEmergencyColorAnimator.start(); + } + } + + public void end() { + if (mPseudoEmergencyColorAnimator != null && mPseudoEmergencyColorAnimator.isStarted()) { + mPseudoEmergencyColorAnimator.end(); + } + } + + private View getView() { + return mViewProvider == null ? null : mViewProvider.getView(); + } + + private Context getContext() { + View view = getView(); + return view != null ? view.getContext() : null; + } + + private void vibrate(long milliseconds) { + Context context = getContext(); + if (context != null) { + Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + if (vibrator != null) { + vibrator.vibrate(milliseconds); + } + } + } + + public interface ViewProvider { + + View getView(); + } +} diff --git a/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java b/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java new file mode 100644 index 000000000..f3a93f916 --- /dev/null +++ b/java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java @@ -0,0 +1,202 @@ +/* + * 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.dialer.app.dialpad; + +import android.content.AsyncTaskLoader; +import android.content.Context; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.util.Log; +import com.android.contacts.common.list.PhoneNumberListAdapter.PhoneQuery; +import com.android.dialer.database.Database; +import com.android.dialer.database.DialerDatabaseHelper; +import com.android.dialer.database.DialerDatabaseHelper.ContactNumber; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.util.PermissionsUtil; +import java.util.ArrayList; + +/** Implements a Loader class to asynchronously load SmartDial search results. */ +public class SmartDialCursorLoader extends AsyncTaskLoader { + + private static final String TAG = "SmartDialCursorLoader"; + private static final boolean DEBUG = false; + + private final Context mContext; + + private Cursor mCursor; + + private String mQuery; + private SmartDialNameMatcher mNameMatcher; + + private ForceLoadContentObserver mObserver; + + private boolean mShowEmptyListForNullQuery = true; + + public SmartDialCursorLoader(Context context) { + super(context); + mContext = context; + } + + /** + * Configures the query string to be used to find SmartDial matches. + * + * @param query The query string user typed. + */ + public void configureQuery(String query) { + if (DEBUG) { + Log.v(TAG, "Configure new query to be " + query); + } + mQuery = SmartDialNameMatcher.normalizeNumber(query, SmartDialPrefix.getMap()); + + /** Constructs a name matcher object for matching names. */ + mNameMatcher = new SmartDialNameMatcher(mQuery, SmartDialPrefix.getMap()); + mNameMatcher.setShouldMatchEmptyQuery(!mShowEmptyListForNullQuery); + } + + /** + * Queries the SmartDial database and loads results in background. + * + * @return Cursor of contacts that matches the SmartDial query. + */ + @Override + public Cursor loadInBackground() { + if (DEBUG) { + Log.v(TAG, "Load in background " + mQuery); + } + + if (!PermissionsUtil.hasContactsPermissions(mContext)) { + return new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + } + + /** Loads results from the database helper. */ + final DialerDatabaseHelper dialerDatabaseHelper = + Database.get(mContext).getDatabaseHelper(mContext); + final ArrayList allMatches = + dialerDatabaseHelper.getLooseMatches(mQuery, mNameMatcher); + + if (DEBUG) { + Log.v(TAG, "Loaded matches " + String.valueOf(allMatches.size())); + } + + /** Constructs a cursor for the returned array of results. */ + final MatrixCursor cursor = new MatrixCursor(PhoneQuery.PROJECTION_PRIMARY); + Object[] row = new Object[PhoneQuery.PROJECTION_PRIMARY.length]; + for (ContactNumber contact : allMatches) { + row[PhoneQuery.PHONE_ID] = contact.dataId; + row[PhoneQuery.PHONE_NUMBER] = contact.phoneNumber; + row[PhoneQuery.CONTACT_ID] = contact.id; + row[PhoneQuery.LOOKUP_KEY] = contact.lookupKey; + row[PhoneQuery.PHOTO_ID] = contact.photoId; + row[PhoneQuery.DISPLAY_NAME] = contact.displayName; + row[PhoneQuery.CARRIER_PRESENCE] = contact.carrierPresence; + cursor.addRow(row); + } + return cursor; + } + + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + /** The Loader has been reset; ignore the result and invalidate the data. */ + releaseResources(cursor); + return; + } + + /** Hold a reference to the old data so it doesn't get garbage collected. */ + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (mObserver == null) { + mObserver = new ForceLoadContentObserver(); + mContext + .getContentResolver() + .registerContentObserver(DialerDatabaseHelper.SMART_DIAL_UPDATED_URI, true, mObserver); + } + + if (isStarted()) { + /** If the Loader is in a started state, deliver the results to the client. */ + super.deliverResult(cursor); + } + + /** Invalidate the old data as we don't need it any more. */ + if (oldCursor != null && oldCursor != cursor) { + releaseResources(oldCursor); + } + } + + @Override + protected void onStartLoading() { + if (mCursor != null) { + /** Deliver any previously loaded data immediately. */ + deliverResult(mCursor); + } + if (mCursor == null) { + /** Force loads every time as our results change with queries. */ + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + /** The Loader is in a stopped state, so we should attempt to cancel the current load. */ + cancelLoad(); + } + + @Override + protected void onReset() { + /** Ensure the loader has been stopped. */ + onStopLoading(); + + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + + /** Release all previously saved query results. */ + if (mCursor != null) { + releaseResources(mCursor); + mCursor = null; + } + } + + @Override + public void onCanceled(Cursor cursor) { + super.onCanceled(cursor); + + if (mObserver != null) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + mObserver = null; + } + + /** The load has been canceled, so we should release the resources associated with 'data'. */ + releaseResources(cursor); + } + + private void releaseResources(Cursor cursor) { + if (cursor != null) { + cursor.close(); + } + } + + public void setShowEmptyListForNullQuery(boolean show) { + mShowEmptyListForNullQuery = show; + if (mNameMatcher != null) { + mNameMatcher.setShouldMatchEmptyQuery(!show); + } + } +} diff --git a/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java b/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java new file mode 100644 index 000000000..051daf46e --- /dev/null +++ b/java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java @@ -0,0 +1,56 @@ +/* + * 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.dialer.app.dialpad; + +import android.telephony.PhoneNumberUtils; +import android.text.Spanned; +import android.text.method.DialerKeyListener; + +/** + * {@link DialerKeyListener} with Unicode support. Converts any Unicode(e.g. Arabic) characters that + * represent digits into digits before filtering the results so that we can support pasted digits + * from Unicode languages. + */ +public class UnicodeDialerKeyListener extends DialerKeyListener { + + public static final UnicodeDialerKeyListener INSTANCE = new UnicodeDialerKeyListener(); + + @Override + public CharSequence filter( + CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + final String converted = + PhoneNumberUtils.convertKeypadLettersToDigits( + PhoneNumberUtils.replaceUnicodeDigits(source.toString())); + // PhoneNumberUtils.replaceUnicodeDigits performs a character for character replacement, + // so we can assume that start and end positions should remain unchanged. + CharSequence result = super.filter(converted, start, end, dest, dstart, dend); + if (result == null) { + if (source.equals(converted)) { + // There was no conversion or filtering performed. Just return null according to + // the behavior of DialerKeyListener. + return null; + } else { + // filter returns null if the charsequence is to be returned unchanged/unfiltered. + // But in this case we do want to return a modified character string (even if + // none of the characters in the modified string are filtered). So if + // result == null we return the unfiltered but converted numeric string instead. + return converted.subSequence(start, end); + } + } + return result; + } +} diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java new file mode 100644 index 000000000..b9381331c --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java @@ -0,0 +1,97 @@ +/* + * 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.dialer.app.filterednumber; + +import android.app.FragmentManager; +import android.content.Context; +import android.database.Cursor; +import android.telephony.PhoneNumberUtils; +import android.view.View; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.R; +import com.android.dialer.blocking.BlockNumberDialogFragment; +import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.InteractionEvent; +import com.android.dialer.phonenumbercache.ContactInfoHelper; + +public class BlockedNumbersAdapter extends NumbersAdapter { + + private BlockedNumbersAdapter( + Context context, + FragmentManager fragmentManager, + ContactInfoHelper contactInfoHelper, + ContactPhotoManager contactPhotoManager) { + super(context, fragmentManager, contactInfoHelper, contactPhotoManager); + } + + public static BlockedNumbersAdapter newBlockedNumbersAdapter( + Context context, FragmentManager fragmentManager) { + return new BlockedNumbersAdapter( + context, + fragmentManager, + new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)), + ContactPhotoManager.getInstance(context)); + } + + @Override + public void bindView(View view, final Context context, Cursor cursor) { + super.bindView(view, context, cursor); + final Integer id = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID)); + final String countryIso = + cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.COUNTRY_ISO)); + final String number = cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER)); + final String normalizedNumber = + cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NORMALIZED_NUMBER)); + + final View deleteButton = view.findViewById(R.id.delete_button); + deleteButton.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View view) { + BlockNumberDialogFragment.show( + id, + number, + countryIso, + PhoneNumberUtils.formatNumber(number, countryIso), + R.id.blocked_numbers_activity_container, + getFragmentManager(), + new BlockNumberDialogFragment.Callback() { + @Override + public void onFilterNumberSuccess() {} + + @Override + public void onUnfilterNumberSuccess() { + Logger.get(context) + .logInteraction(InteractionEvent.Type.UNBLOCK_NUMBER_MANAGEMENT_SCREEN); + } + + @Override + public void onChangeFilteredNumberUndo() {} + }); + } + }); + + updateView(view, number, countryIso); + } + + @Override + public boolean isEmpty() { + // Always return false, so that the header with blocking-related options always shows. + return false; + } +} diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java new file mode 100644 index 000000000..f53a45840 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java @@ -0,0 +1,271 @@ +/* + * 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.dialer.app.filterednumber; + +import android.app.ListFragment; +import android.app.LoaderManager; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.v4.app.ActivityCompat; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.contacts.common.lettertiles.LetterTileDrawable; +import com.android.dialer.app.R; +import com.android.dialer.blocking.BlockedNumbersMigrator; +import com.android.dialer.blocking.BlockedNumbersMigrator.Listener; +import com.android.dialer.blocking.FilteredNumberCompat; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.blocking.FilteredNumbersUtil.CheckForSendToVoicemailContactListener; +import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener; +import com.android.dialer.database.FilteredNumberContract; +import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker; + +public class BlockedNumbersFragment extends ListFragment + implements LoaderManager.LoaderCallbacks, + View.OnClickListener, + VisualVoicemailEnabledChecker.Callback { + + private static final char ADD_BLOCKED_NUMBER_ICON_LETTER = '+'; + protected View migratePromoView; + private BlockedNumbersMigrator blockedNumbersMigratorForTest; + private TextView blockedNumbersText; + private TextView footerText; + private BlockedNumbersAdapter mAdapter; + private VisualVoicemailEnabledChecker mVoicemailEnabledChecker; + private View mImportSettings; + private View mBlockedNumbersDisabledForEmergency; + private View mBlockedNumberListDivider; + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + LayoutInflater inflater = + (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); + getListView().addHeaderView(inflater.inflate(R.layout.blocked_number_header, null)); + getListView().addFooterView(inflater.inflate(R.layout.blocked_number_footer, null)); + //replace the icon for add number with LetterTileDrawable(), so it will have identical style + ImageView addNumberIcon = (ImageView) getActivity().findViewById(R.id.add_number_icon); + LetterTileDrawable drawable = new LetterTileDrawable(getResources()); + drawable.setLetter(ADD_BLOCKED_NUMBER_ICON_LETTER); + drawable.setColor( + ActivityCompat.getColor(getActivity(), R.color.add_blocked_number_icon_color)); + drawable.setIsCircular(true); + addNumberIcon.setImageDrawable(drawable); + + if (mAdapter == null) { + mAdapter = + BlockedNumbersAdapter.newBlockedNumbersAdapter( + getContext(), getActivity().getFragmentManager()); + } + setListAdapter(mAdapter); + + blockedNumbersText = (TextView) getListView().findViewById(R.id.blocked_number_text_view); + migratePromoView = getListView().findViewById(R.id.migrate_promo); + getListView().findViewById(R.id.migrate_promo_allow_button).setOnClickListener(this); + mImportSettings = getListView().findViewById(R.id.import_settings); + mBlockedNumbersDisabledForEmergency = + getListView().findViewById(R.id.blocked_numbers_disabled_for_emergency); + mBlockedNumberListDivider = getActivity().findViewById(R.id.blocked_number_list_divider); + getListView().findViewById(R.id.import_button).setOnClickListener(this); + getListView().findViewById(R.id.view_numbers_button).setOnClickListener(this); + getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(this); + + footerText = (TextView) getActivity().findViewById(R.id.blocked_number_footer_textview); + mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getContext(), this); + mVoicemailEnabledChecker.asyncUpdate(); + updateActiveVoicemailProvider(); + } + + @Override + public void onDestroy() { + setListAdapter(null); + super.onDestroy(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getLoaderManager().initLoader(0, null, this); + } + + @Override + public void onResume() { + super.onResume(); + + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + ColorDrawable backgroundDrawable = + new ColorDrawable(ActivityCompat.getColor(getActivity(), R.color.dialer_theme_color)); + actionBar.setBackgroundDrawable(backgroundDrawable); + actionBar.setDisplayShowCustomEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + actionBar.setTitle(R.string.manage_blocked_numbers_label); + + // If the device can use the framework blocking solution, users should not be able to add + // new blocked numbers from the Blocked Management UI. They will be shown a promo card + // asking them to migrate to new blocking instead. + if (FilteredNumberCompat.canUseNewFiltering()) { + migratePromoView.setVisibility(View.VISIBLE); + blockedNumbersText.setVisibility(View.GONE); + getListView().findViewById(R.id.add_number_linear_layout).setVisibility(View.GONE); + getListView().findViewById(R.id.add_number_linear_layout).setOnClickListener(null); + mBlockedNumberListDivider.setVisibility(View.GONE); + mImportSettings.setVisibility(View.GONE); + getListView().findViewById(R.id.import_button).setOnClickListener(null); + getListView().findViewById(R.id.view_numbers_button).setOnClickListener(null); + mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE); + footerText.setVisibility(View.GONE); + } else { + FilteredNumbersUtil.checkForSendToVoicemailContact( + getActivity(), + new CheckForSendToVoicemailContactListener() { + @Override + public void onComplete(boolean hasSendToVoicemailContact) { + final int visibility = hasSendToVoicemailContact ? View.VISIBLE : View.GONE; + mImportSettings.setVisibility(visibility); + } + }); + } + + // All views except migrate and the block list are hidden when new filtering is available + if (!FilteredNumberCompat.canUseNewFiltering() + && FilteredNumbersUtil.hasRecentEmergencyCall(getContext())) { + mBlockedNumbersDisabledForEmergency.setVisibility(View.VISIBLE); + } else { + mBlockedNumbersDisabledForEmergency.setVisibility(View.GONE); + } + + mVoicemailEnabledChecker.asyncUpdate(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.blocked_number_fragment, container, false); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + final String[] projection = { + FilteredNumberContract.FilteredNumberColumns._ID, + FilteredNumberContract.FilteredNumberColumns.COUNTRY_ISO, + FilteredNumberContract.FilteredNumberColumns.NUMBER, + FilteredNumberContract.FilteredNumberColumns.NORMALIZED_NUMBER + }; + final String selection = + FilteredNumberContract.FilteredNumberColumns.TYPE + + "=" + + FilteredNumberContract.FilteredNumberTypes.BLOCKED_NUMBER; + return new CursorLoader( + getContext(), + FilteredNumberContract.FilteredNumber.CONTENT_URI, + projection, + selection, + null, + null); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + if (FilteredNumberCompat.canUseNewFiltering() || data.getCount() == 0) { + mBlockedNumberListDivider.setVisibility(View.INVISIBLE); + } else { + mBlockedNumberListDivider.setVisibility(View.VISIBLE); + } + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + + @Override + public void onClick(final View view) { + final BlockedNumbersSettingsActivity activity = (BlockedNumbersSettingsActivity) getActivity(); + if (activity == null) { + return; + } + + int resId = view.getId(); + if (resId == R.id.add_number_linear_layout) { + activity.showSearchUi(); + } else if (resId == R.id.view_numbers_button) { + activity.showNumbersToImportPreviewUi(); + } else if (resId == R.id.import_button) { + FilteredNumbersUtil.importSendToVoicemailContacts( + activity, + new ImportSendToVoicemailContactsListener() { + @Override + public void onImportComplete() { + mImportSettings.setVisibility(View.GONE); + } + }); + } else if (resId == R.id.migrate_promo_allow_button) { + view.setEnabled(false); + (blockedNumbersMigratorForTest != null + ? blockedNumbersMigratorForTest + : new BlockedNumbersMigrator(getContext())) + .migrate( + new Listener() { + @Override + public void onComplete() { + getContext() + .startActivity( + FilteredNumberCompat.createManageBlockedNumbersIntent(getContext())); + // Remove this activity from the backstack + activity.finish(); + } + }); + } + } + + @Override + public void onVisualVoicemailEnabledStatusChanged(boolean newStatus) { + updateActiveVoicemailProvider(); + } + + private void updateActiveVoicemailProvider() { + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) { + footerText.setText(R.string.block_number_footer_message_vvm); + } else { + footerText.setText(R.string.block_number_footer_message_no_vvm); + } + } + + void setBlockedNumbersMigratorForTest(BlockedNumbersMigrator blockedNumbersMigrator) { + blockedNumbersMigratorForTest = blockedNumbersMigrator; + } +} diff --git a/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java new file mode 100644 index 000000000..eef920710 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java @@ -0,0 +1,146 @@ +/* + * 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.dialer.app.filterednumber; + +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.view.MenuItem; +import com.android.dialer.app.R; +import com.android.dialer.app.list.BlockedListSearchFragment; +import com.android.dialer.app.list.SearchFragment; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.ScreenEvent; + +public class BlockedNumbersSettingsActivity extends AppCompatActivity + implements SearchFragment.HostInterface { + + private static final String TAG_BLOCKED_MANAGEMENT_FRAGMENT = "blocked_management"; + private static final String TAG_BLOCKED_SEARCH_FRAGMENT = "blocked_search"; + private static final String TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT = "view_numbers_to_import"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.blocked_numbers_activity); + + // If savedInstanceState != null, the Activity will automatically restore the last fragment. + if (savedInstanceState == null) { + showManagementUi(); + } + } + + /** Shows fragment with the list of currently blocked numbers and settings related to blocking. */ + public void showManagementUi() { + BlockedNumbersFragment fragment = + (BlockedNumbersFragment) + getFragmentManager().findFragmentByTag(TAG_BLOCKED_MANAGEMENT_FRAGMENT); + if (fragment == null) { + fragment = new BlockedNumbersFragment(); + } + + getFragmentManager() + .beginTransaction() + .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_MANAGEMENT_FRAGMENT) + .commit(); + + Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_MANAGEMENT, this); + } + + /** Shows fragment with search UI for browsing/finding numbers to block. */ + public void showSearchUi() { + BlockedListSearchFragment fragment = + (BlockedListSearchFragment) + getFragmentManager().findFragmentByTag(TAG_BLOCKED_SEARCH_FRAGMENT); + if (fragment == null) { + fragment = new BlockedListSearchFragment(); + fragment.setHasOptionsMenu(false); + fragment.setShowEmptyListForNullQuery(true); + fragment.setDirectorySearchEnabled(false); + } + + getFragmentManager() + .beginTransaction() + .replace(R.id.blocked_numbers_activity_container, fragment, TAG_BLOCKED_SEARCH_FRAGMENT) + .addToBackStack(null) + .commit(); + + Logger.get(this).logScreenView(ScreenEvent.Type.BLOCKED_NUMBER_ADD_NUMBER, this); + } + + /** + * Shows fragment with UI to preview the numbers of contacts currently marked as send-to-voicemail + * in Contacts. These numbers can be imported into Dialer's blocked number list. + */ + public void showNumbersToImportPreviewUi() { + ViewNumbersToImportFragment fragment = + (ViewNumbersToImportFragment) + getFragmentManager().findFragmentByTag(TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT); + if (fragment == null) { + fragment = new ViewNumbersToImportFragment(); + } + + getFragmentManager() + .beginTransaction() + .replace( + R.id.blocked_numbers_activity_container, fragment, TAG_VIEW_NUMBERS_TO_IMPORT_FRAGMENT) + .addToBackStack(null) + .commit(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return false; + } + + @Override + public void onBackPressed() { + // TODO: Achieve back navigation without overriding onBackPressed. + if (getFragmentManager().getBackStackEntryCount() > 0) { + getFragmentManager().popBackStack(); + } else { + super.onBackPressed(); + } + } + + @Override + public boolean isActionBarShowing() { + return false; + } + + @Override + public boolean isDialpadShown() { + return false; + } + + @Override + public int getDialpadHeight() { + return 0; + } + + @Override + public int getActionBarHideOffset() { + return 0; + } + + @Override + public int getActionBarHeight() { + return 0; + } +} diff --git a/java/com/android/dialer/app/filterednumber/NumbersAdapter.java b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java new file mode 100644 index 000000000..f71517a44 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/NumbersAdapter.java @@ -0,0 +1,138 @@ +/* + * 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.dialer.app.filterednumber; + +import android.app.FragmentManager; +import android.content.Context; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.View; +import android.widget.QuickContactBadge; +import android.widget.SimpleCursorAdapter; +import android.widget.TextView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.R; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; + +public class NumbersAdapter extends SimpleCursorAdapter { + + private Context mContext; + private FragmentManager mFragmentManager; + private ContactInfoHelper mContactInfoHelper; + private BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private ContactPhotoManager mContactPhotoManager; + + public NumbersAdapter( + Context context, + FragmentManager fragmentManager, + ContactInfoHelper contactInfoHelper, + ContactPhotoManager contactPhotoManager) { + super(context, R.layout.blocked_number_item, null, new String[] {}, new int[] {}, 0); + mContext = context; + mFragmentManager = fragmentManager; + mContactInfoHelper = contactInfoHelper; + mContactPhotoManager = contactPhotoManager; + } + + public void updateView(View view, String number, String countryIso) { + final TextView callerName = (TextView) view.findViewById(R.id.caller_name); + final TextView callerNumber = (TextView) view.findViewById(R.id.caller_number); + final QuickContactBadge quickContactBadge = + (QuickContactBadge) view.findViewById(R.id.quick_contact_photo); + quickContactBadge.setOverlay(null); + if (CompatUtils.hasPrioritizedMimeType()) { + quickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); + } + + ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso); + if (info == null) { + info = new ContactInfo(); + info.number = number; + } + final CharSequence locationOrType = getNumberTypeOrLocation(info); + final String displayNumber = getDisplayNumber(info); + final String displayNumberStr = + mBidiFormatter.unicodeWrap(displayNumber, TextDirectionHeuristics.LTR); + + String nameForDefaultImage; + if (!TextUtils.isEmpty(info.name)) { + nameForDefaultImage = info.name; + callerName.setText(info.name); + callerNumber.setText(locationOrType + " " + displayNumberStr); + } else { + nameForDefaultImage = displayNumber; + callerName.setText(displayNumberStr); + if (!TextUtils.isEmpty(locationOrType)) { + callerNumber.setText(locationOrType); + callerNumber.setVisibility(View.VISIBLE); + } else { + callerNumber.setVisibility(View.GONE); + } + } + loadContactPhoto(info, nameForDefaultImage, quickContactBadge); + } + + private void loadContactPhoto(ContactInfo info, String displayName, QuickContactBadge badge) { + final String lookupKey = + info.lookupUri == null ? null : UriUtils.getLookupKeyFromUri(info.lookupUri); + final int contactType = + mContactInfoHelper.isBusiness(info.sourceType) + ? ContactPhotoManager.TYPE_BUSINESS + : ContactPhotoManager.TYPE_DEFAULT; + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + badge.assignContactUri(info.lookupUri); + badge.setContentDescription( + mContext.getResources().getString(R.string.description_contact_details, displayName)); + mContactPhotoManager.loadDirectoryPhoto( + badge, info.photoUri, false /* darkTheme */, true /* isCircular */, request); + } + + private String getDisplayNumber(ContactInfo info) { + if (!TextUtils.isEmpty(info.formattedNumber)) { + return info.formattedNumber; + } else if (!TextUtils.isEmpty(info.number)) { + return info.number; + } else { + return ""; + } + } + + private CharSequence getNumberTypeOrLocation(ContactInfo info) { + if (!TextUtils.isEmpty(info.name)) { + return ContactsContract.CommonDataKinds.Phone.getTypeLabel( + mContext.getResources(), info.type, info.label); + } else { + return PhoneNumberHelper.getGeoDescription(mContext, info.number); + } + } + + protected Context getContext() { + return mContext; + } + + protected FragmentManager getFragmentManager() { + return mFragmentManager; + } +} diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java new file mode 100644 index 000000000..5228a1d79 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java @@ -0,0 +1,56 @@ +/* + * 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.dialer.app.filterednumber; + +import android.app.FragmentManager; +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.GeoUtil; +import com.android.dialer.app.R; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.phonenumbercache.ContactInfoHelper; + +public class ViewNumbersToImportAdapter extends NumbersAdapter { + + private ViewNumbersToImportAdapter( + Context context, + FragmentManager fragmentManager, + ContactInfoHelper contactInfoHelper, + ContactPhotoManager contactPhotoManager) { + super(context, fragmentManager, contactInfoHelper, contactPhotoManager); + } + + public static ViewNumbersToImportAdapter newViewNumbersToImportAdapter( + Context context, FragmentManager fragmentManager) { + return new ViewNumbersToImportAdapter( + context, + fragmentManager, + new ContactInfoHelper(context, GeoUtil.getCurrentCountryIso(context)), + ContactPhotoManager.getInstance(context)); + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + super.bindView(view, context, cursor); + + final String number = cursor.getString(FilteredNumbersUtil.PhoneQuery.NUMBER_COLUMN_INDEX); + + view.findViewById(R.id.delete_button).setVisibility(View.GONE); + updateView(view, number, null /* countryIso */); + } +} diff --git a/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java new file mode 100644 index 000000000..d45f61ed7 --- /dev/null +++ b/java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java @@ -0,0 +1,130 @@ +/* + * 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.dialer.app.filterednumber; + +import android.app.ListFragment; +import android.app.LoaderManager; +import android.content.Context; +import android.content.CursorLoader; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.dialer.app.R; +import com.android.dialer.blocking.FilteredNumbersUtil; +import com.android.dialer.blocking.FilteredNumbersUtil.ImportSendToVoicemailContactsListener; + +public class ViewNumbersToImportFragment extends ListFragment + implements LoaderManager.LoaderCallbacks, View.OnClickListener { + + private ViewNumbersToImportAdapter mAdapter; + + @Override + public Context getContext() { + return getActivity(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (mAdapter == null) { + mAdapter = + ViewNumbersToImportAdapter.newViewNumbersToImportAdapter( + getContext(), getActivity().getFragmentManager()); + } + setListAdapter(mAdapter); + } + + @Override + public void onDestroy() { + setListAdapter(null); + super.onDestroy(); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getLoaderManager().initLoader(0, null, this); + } + + @Override + public void onResume() { + super.onResume(); + + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + actionBar.setTitle(R.string.import_send_to_voicemail_numbers_label); + actionBar.setDisplayShowCustomEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); + actionBar.setDisplayShowHomeEnabled(true); + actionBar.setDisplayShowTitleEnabled(true); + + getActivity().findViewById(R.id.cancel_button).setOnClickListener(this); + getActivity().findViewById(R.id.import_button).setOnClickListener(this); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.view_numbers_to_import_fragment, container, false); + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + final CursorLoader cursorLoader = + new CursorLoader( + getContext(), + Phone.CONTENT_URI, + FilteredNumbersUtil.PhoneQuery.PROJECTION, + FilteredNumbersUtil.PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE, + null, + null); + return cursorLoader; + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mAdapter.swapCursor(null); + } + + @Override + public void onClick(final View view) { + if (view.getId() == R.id.import_button) { + FilteredNumbersUtil.importSendToVoicemailContacts( + getContext(), + new ImportSendToVoicemailContactsListener() { + @Override + public void onImportComplete() { + if (getActivity() != null) { + getActivity().onBackPressed(); + } + } + }); + } else if (view.getId() == R.id.cancel_button) { + getActivity().onBackPressed(); + } + } +} diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java new file mode 100644 index 000000000..2125a1524 --- /dev/null +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java @@ -0,0 +1,47 @@ +/* + * 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.dialer.app.legacybindings; + +import android.app.Activity; +import android.view.ViewGroup; +import com.android.dialer.app.calllog.CallLogAdapter; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; + +/** + * These are old bindings between Dialer and the container application. All new bindings should be + * added to the bindings module and not here. + */ +public interface DialerLegacyBindings { + + /** + * activityType must be one of following constants: CallLogAdapter.ACTIVITY_TYPE_CALL_LOG, or + * CallLogAdapter.ACTIVITY_TYPE_DIALTACTS. + */ + CallLogAdapter newCallLogAdapter( + Activity activity, + ViewGroup alertContainer, + CallLogAdapter.CallFetcher callFetcher, + CallLogCache callLogCache, + ContactInfoCache contactInfoCache, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + int activityType); + + RegularSearchFragment newRegularSearchFragment(); +} diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java new file mode 100644 index 000000000..70d379c9f --- /dev/null +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java @@ -0,0 +1,26 @@ +/* + * 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.dialer.app.legacybindings; + +/** + * This interface should be implementated by the Application subclass. It allows the dialer module + * to get references to the DialerLegacyBindings. + */ +public interface DialerLegacyBindingsFactory { + + DialerLegacyBindings newDialerLegacyBindings(); +} diff --git a/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java new file mode 100644 index 000000000..f01df78f8 --- /dev/null +++ b/java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java @@ -0,0 +1,53 @@ +/* + * 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.dialer.app.legacybindings; + +import android.app.Activity; +import android.view.ViewGroup; +import com.android.dialer.app.calllog.CallLogAdapter; +import com.android.dialer.app.calllog.calllogcache.CallLogCache; +import com.android.dialer.app.contactinfo.ContactInfoCache; +import com.android.dialer.app.list.RegularSearchFragment; +import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; + +/** Default implementation for dialer legacy bindings. */ +public class DialerLegacyBindingsStub implements DialerLegacyBindings { + + @Override + public CallLogAdapter newCallLogAdapter( + Activity activity, + ViewGroup alertContainer, + CallLogAdapter.CallFetcher callFetcher, + CallLogCache callLogCache, + ContactInfoCache contactInfoCache, + VoicemailPlaybackPresenter voicemailPlaybackPresenter, + int activityType) { + return new CallLogAdapter( + activity, + alertContainer, + callFetcher, + callLogCache, + contactInfoCache, + voicemailPlaybackPresenter, + activityType); + } + + @Override + public RegularSearchFragment newRegularSearchFragment() { + return new RegularSearchFragment(); + } +} diff --git a/java/com/android/dialer/app/list/AllContactsFragment.java b/java/com/android/dialer/app/list/AllContactsFragment.java new file mode 100644 index 000000000..093e8f384 --- /dev/null +++ b/java/com/android/dialer/app/list/AllContactsFragment.java @@ -0,0 +1,209 @@ +/* + * 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.dialer.app.list; + +import static android.Manifest.permission.READ_CONTACTS; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.Loader; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.QuickContact; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.ContactEntryListFragment; +import com.android.contacts.common.list.ContactListFilter; +import com.android.contacts.common.list.DefaultContactListAdapter; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.app.R; +import com.android.dialer.app.list.ListsFragment.ListsPage; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.common.LogUtil; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; + +/** Fragments to show all contacts with phone numbers. */ +public class AllContactsFragment extends ContactEntryListFragment + implements ListsPage, + OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; + + private EmptyContentView mEmptyListView; + + /** + * Listen to broadcast events about permissions in order to be notified if the READ_CONTACTS + * permission is granted via the UI in another fragment. + */ + private BroadcastReceiver mReadContactsPermissionGrantedReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + reloadData(); + } + }; + + public AllContactsFragment() { + setQuickContactEnabled(false); + setAdjustSelectionBoundsEnabled(true); + setPhotoLoaderEnabled(true); + setSectionHeaderDisplayEnabled(true); + setDarkTheme(false); + setVisibleScrollbarEnabled(true); + } + + @Override + public void onViewCreated(View view, android.os.Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); + mEmptyListView.setImage(R.drawable.empty_contacts); + mEmptyListView.setDescription(R.string.all_contacts_empty); + mEmptyListView.setActionClickedListener(this); + getListView().setEmptyView(mEmptyListView); + mEmptyListView.setVisibility(View.GONE); + + FabUtil.addBottomPaddingToListViewForFab(getListView(), getResources()); + } + + @Override + public void onStart() { + super.onStart(); + PermissionsUtil.registerPermissionReceiver( + getActivity(), mReadContactsPermissionGrantedReceiver, READ_CONTACTS); + } + + @Override + public void onStop() { + PermissionsUtil.unregisterPermissionReceiver( + getActivity(), mReadContactsPermissionGrantedReceiver); + super.onStop(); + } + + @Override + protected void startLoading() { + if (PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) { + super.startLoading(); + mEmptyListView.setDescription(R.string.all_contacts_empty); + mEmptyListView.setActionLabel(R.string.all_contacts_empty_add_contact_action); + } else { + mEmptyListView.setDescription(R.string.permission_no_contacts); + mEmptyListView.setActionLabel(R.string.permission_single_turn_on); + mEmptyListView.setVisibility(View.VISIBLE); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + super.onLoadFinished(loader, data); + + if (data == null || data.getCount() == 0) { + mEmptyListView.setVisibility(View.VISIBLE); + } + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + final DefaultContactListAdapter adapter = + new DefaultContactListAdapter(getActivity()) { + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + itemView.setTag(this.getContactUri(partition, cursor)); + } + }; + adapter.setDisplayPhotos(true); + adapter.setFilter( + ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_DEFAULT)); + adapter.setSectionHeaderDisplayEnabled(isSectionHeaderDisplayEnabled()); + return adapter; + } + + @Override + protected View inflateView(LayoutInflater inflater, ViewGroup container) { + return inflater.inflate(R.layout.all_contacts_fragment, null); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final Uri uri = (Uri) view.getTag(); + if (uri != null) { + if (CompatUtils.hasPrioritizedMimeType()) { + QuickContact.showQuickContact(getContext(), view, uri, null, Phone.CONTENT_ITEM_TYPE); + } else { + QuickContact.showQuickContact(getActivity(), view, uri, QuickContact.MODE_LARGE, null); + } + } + } + + @Override + protected void onItemClick(int position, long id) { + // Do nothing. Implemented to satisfy ContactEntryListFragment. + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) { + FragmentCompat.requestPermissions( + this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE); + } else { + // Add new contact + DialerUtils.startActivityWithErrorToast( + activity, IntentUtil.getNewContactIntent(), R.string.add_contact_not_available); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) { + if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + // Force a refresh of the data since we were missing the permission before this. + reloadData(); + } + } + } + + @Override + public void onPageResume(@Nullable Activity activity) { + LogUtil.i("AllContactsFragment.onPageResume", null); + } + + @Override + public void onPagePause(@Nullable Activity activity) { + LogUtil.i("AllContactsFragment.onPagePause", null); + } +} diff --git a/java/com/android/dialer/app/list/BlockedListSearchAdapter.java b/java/com/android/dialer/app/list/BlockedListSearchAdapter.java new file mode 100644 index 000000000..a90ce7a0d --- /dev/null +++ b/java/com/android/dialer/app/list/BlockedListSearchAdapter.java @@ -0,0 +1,84 @@ +/* + * 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.dialer.app.list; + +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.view.View; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactListItemView; +import com.android.dialer.app.R; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; + +/** List adapter to display search results for adding a blocked number. */ +public class BlockedListSearchAdapter extends RegularSearchListAdapter { + + private Resources mResources; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + + public BlockedListSearchAdapter(Context context) { + super(context); + mResources = context.getResources(); + disableAllShortcuts(); + setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, true); + + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(context); + } + + @Override + protected boolean isChanged(boolean showNumberShortcuts) { + return setShortcutEnabled(SHORTCUT_BLOCK_NUMBER, showNumberShortcuts || mIsQuerySipAddress); + } + + public void setViewBlocked(ContactListItemView view, Integer id) { + view.setTag(R.id.block_id, id); + final int textColor = mResources.getColor(R.color.blocked_number_block_color); + view.getDataView().setTextColor(textColor); + view.getLabelView().setTextColor(textColor); + //TODO: Add icon + } + + public void setViewUnblocked(ContactListItemView view) { + view.setTag(R.id.block_id, null); + final int textColor = mResources.getColor(R.color.dialer_secondary_text_color); + view.getDataView().setTextColor(textColor); + view.getLabelView().setTextColor(textColor); + //TODO: Remove icon + } + + @Override + protected void bindView(View itemView, int partition, Cursor cursor, int position) { + super.bindView(itemView, partition, cursor, position); + + final ContactListItemView view = (ContactListItemView) itemView; + // Reset view state to unblocked. + setViewUnblocked(view); + + final String number = getPhoneNumber(position); + final String countryIso = GeoUtil.getCurrentCountryIso(mContext); + final FilteredNumberAsyncQueryHandler.OnCheckBlockedListener onCheckListener = + new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() { + @Override + public void onCheckComplete(Integer id) { + if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) { + setViewBlocked(view, id); + } + } + }; + mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso); + } +} diff --git a/java/com/android/dialer/app/list/BlockedListSearchFragment.java b/java/com/android/dialer/app/list/BlockedListSearchFragment.java new file mode 100644 index 000000000..2129981c0 --- /dev/null +++ b/java/com/android/dialer/app/list/BlockedListSearchFragment.java @@ -0,0 +1,245 @@ +/* + * 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.dialer.app.list; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.telephony.PhoneNumberUtils; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.View; +import android.widget.AdapterView; +import android.widget.EditText; +import android.widget.Toast; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.R; +import com.android.dialer.app.widget.SearchEditTextLayout; +import com.android.dialer.blocking.BlockNumberDialogFragment; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.InteractionEvent; + +public class BlockedListSearchFragment extends RegularSearchFragment + implements BlockNumberDialogFragment.Callback { + + private static final String TAG = BlockedListSearchFragment.class.getSimpleName(); + + private final TextWatcher mPhoneSearchQueryTextListener = + new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + setQueryString(s.toString()); + } + + @Override + public void afterTextChanged(Editable s) {} + }; + private final SearchEditTextLayout.Callback mSearchLayoutCallback = + new SearchEditTextLayout.Callback() { + @Override + public void onBackButtonClicked() { + getActivity().onBackPressed(); + } + + @Override + public void onSearchViewClicked() {} + }; + private FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; + private EditText mSearchView; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setShowEmptyListForNullQuery(true); + /* + * Pass in the empty string here so ContactEntryListFragment#setQueryString interprets it as + * an empty search query, rather than as an uninitalized value. In the latter case, the + * adapter returned by #createListAdapter is used, which populates the view with contacts. + * Passing in the empty string forces ContactEntryListFragment to interpret it as an empty + * query, which results in showing an empty view + */ + setQueryString(getQueryString() == null ? "" : getQueryString()); + mFilteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(getContext()); + } + + @Override + public void onResume() { + super.onResume(); + + ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + actionBar.setCustomView(R.layout.search_edittext); + actionBar.setDisplayShowCustomEnabled(true); + actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setDisplayShowHomeEnabled(false); + + final SearchEditTextLayout searchEditTextLayout = + (SearchEditTextLayout) actionBar.getCustomView().findViewById(R.id.search_view_container); + searchEditTextLayout.expand(false, true); + searchEditTextLayout.setCallback(mSearchLayoutCallback); + searchEditTextLayout.setBackgroundDrawable(null); + + mSearchView = (EditText) searchEditTextLayout.findViewById(R.id.search_view); + mSearchView.addTextChangedListener(mPhoneSearchQueryTextListener); + mSearchView.setHint(R.string.block_number_search_hint); + + searchEditTextLayout + .findViewById(R.id.search_box_expanded) + .setBackgroundColor(getContext().getResources().getColor(android.R.color.white)); + + if (!TextUtils.isEmpty(getQueryString())) { + mSearchView.setText(getQueryString()); + } + + // TODO: Don't set custom text size; use default search text size. + mSearchView.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + getResources().getDimension(R.dimen.blocked_number_search_text_size)); + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + BlockedListSearchAdapter adapter = new BlockedListSearchAdapter(getActivity()); + adapter.setDisplayPhotos(true); + // Don't show SIP addresses. + adapter.setUseCallableUri(false); + // Keep in sync with the queryString set in #onCreate + adapter.setQueryString(getQueryString() == null ? "" : getQueryString()); + return adapter; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + super.onItemClick(parent, view, position, id); + final int adapterPosition = position - getListView().getHeaderViewsCount(); + final BlockedListSearchAdapter adapter = (BlockedListSearchAdapter) getAdapter(); + final int shortcutType = adapter.getShortcutTypeFromPosition(adapterPosition); + final Integer blockId = (Integer) view.getTag(R.id.block_id); + final String number; + switch (shortcutType) { + case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: + // Handles click on a search result, either contact or nearby places result. + number = adapter.getPhoneNumber(adapterPosition); + blockContactNumber(number, blockId); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_BLOCK_NUMBER: + // Handles click on 'Block number' shortcut to add the user query as a number. + number = adapter.getQueryString(); + blockNumber(number); + break; + default: + Log.w(TAG, "Ignoring unsupported shortcut type: " + shortcutType); + break; + } + } + + @Override + protected void onItemClick(int position, long id) { + // Prevent SearchFragment.onItemClicked from being called. + } + + private void blockNumber(final String number) { + final String countryIso = GeoUtil.getCurrentCountryIso(getContext()); + final OnCheckBlockedListener onCheckListener = + new OnCheckBlockedListener() { + @Override + public void onCheckComplete(Integer id) { + if (id == null) { + BlockNumberDialogFragment.show( + id, + number, + countryIso, + PhoneNumberUtils.formatNumber(number, countryIso), + R.id.blocked_numbers_activity_container, + getFragmentManager(), + BlockedListSearchFragment.this); + } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) { + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.invalidNumber, number), + Toast.LENGTH_SHORT) + .show(); + } else { + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.alreadyBlocked, number), + Toast.LENGTH_SHORT) + .show(); + } + } + }; + mFilteredNumberAsyncQueryHandler.isBlockedNumber(onCheckListener, number, countryIso); + } + + @Override + public void onFilterNumberSuccess() { + Logger.get(getContext()).logInteraction(InteractionEvent.Type.BLOCK_NUMBER_MANAGEMENT_SCREEN); + goBack(); + } + + @Override + public void onUnfilterNumberSuccess() { + Log.wtf(TAG, "Unblocked a number from the BlockedListSearchFragment"); + goBack(); + } + + private void goBack() { + Activity activity = getActivity(); + if (activity == null) { + return; + } + activity.onBackPressed(); + } + + @Override + public void onChangeFilteredNumberUndo() { + getAdapter().notifyDataSetChanged(); + } + + private void blockContactNumber(final String number, final Integer blockId) { + if (blockId != null) { + Toast.makeText( + getContext(), + ContactDisplayUtils.getTtsSpannedPhoneNumber( + getResources(), R.string.alreadyBlocked, number), + Toast.LENGTH_SHORT) + .show(); + return; + } + + BlockNumberDialogFragment.show( + blockId, + number, + GeoUtil.getCurrentCountryIso(getContext()), + number, + R.id.blocked_numbers_activity_container, + getFragmentManager(), + this); + } +} diff --git a/java/com/android/dialer/app/list/ContentChangedFilter.java b/java/com/android/dialer/app/list/ContentChangedFilter.java new file mode 100644 index 000000000..663846da5 --- /dev/null +++ b/java/com/android/dialer/app/list/ContentChangedFilter.java @@ -0,0 +1,56 @@ +/* + * 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.dialer.app.list; + +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * AccessibilityDelegate that will filter out TYPE_WINDOW_CONTENT_CHANGED Used to suppress "Showing + * items x of y" from firing of ListView whenever it's content changes. AccessibilityEvent can only + * be rejected at a view's parent once it is generated, use addToParent() to add this delegate to + * the parent. + */ +public class ContentChangedFilter extends AccessibilityDelegate { + + //the view we don't want TYPE_WINDOW_CONTENT_CHANGED to fire. + private View mView; + + private ContentChangedFilter(View view) { + super(); + mView = view; + } + + /** Add this delegate to the parent of @param view to filter out TYPE_WINDOW_CONTENT_CHANGED */ + public static void addToParent(View view) { + View parent = (View) view.getParent(); + parent.setAccessibilityDelegate(new ContentChangedFilter(view)); + } + + @Override + public boolean onRequestSendAccessibilityEvent( + ViewGroup host, View child, AccessibilityEvent event) { + if (child == mView) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) { + return false; + } + } + return super.onRequestSendAccessibilityEvent(host, child, event); + } +} diff --git a/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java b/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java new file mode 100644 index 000000000..7e2525f24 --- /dev/null +++ b/java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java @@ -0,0 +1,228 @@ +/* + * 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.dialer.app.list; + +import android.content.Context; +import android.content.res.Resources; +import android.database.Cursor; +import android.telephony.PhoneNumberUtils; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.PhoneNumberListAdapter; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.app.R; +import com.android.dialer.util.CallUtil; + +/** + * {@link PhoneNumberListAdapter} with the following added shortcuts, that are displayed as list + * items: 1) Directly calling the phone number query 2) Adding the phone number query to a contact + * + *

These shortcuts can be enabled or disabled to toggle whether or not they show up in the list. + */ +public class DialerPhoneNumberListAdapter extends PhoneNumberListAdapter { + + public static final int SHORTCUT_INVALID = -1; + public static final int SHORTCUT_DIRECT_CALL = 0; + public static final int SHORTCUT_CREATE_NEW_CONTACT = 1; + public static final int SHORTCUT_ADD_TO_EXISTING_CONTACT = 2; + public static final int SHORTCUT_SEND_SMS_MESSAGE = 3; + public static final int SHORTCUT_MAKE_VIDEO_CALL = 4; + public static final int SHORTCUT_BLOCK_NUMBER = 5; + public static final int SHORTCUT_COUNT = 6; + private final boolean[] mShortcutEnabled = new boolean[SHORTCUT_COUNT]; + private final BidiFormatter mBidiFormatter = BidiFormatter.getInstance(); + private String mFormattedQueryString; + private String mCountryIso; + private boolean mVideoCallingEnabled = false; + + public DialerPhoneNumberListAdapter(Context context) { + super(context); + + mCountryIso = GeoUtil.getCurrentCountryIso(context); + mVideoCallingEnabled = CallUtil.isVideoEnabled(context); + } + + @Override + public int getCount() { + return super.getCount() + getShortcutCount(); + } + + /** @return The number of enabled shortcuts. Ranges from 0 to a maximum of SHORTCUT_COUNT */ + public int getShortcutCount() { + int count = 0; + for (int i = 0; i < mShortcutEnabled.length; i++) { + if (mShortcutEnabled[i]) { + count++; + } + } + return count; + } + + public void disableAllShortcuts() { + for (int i = 0; i < mShortcutEnabled.length; i++) { + mShortcutEnabled[i] = false; + } + } + + @Override + public int getItemViewType(int position) { + final int shortcut = getShortcutTypeFromPosition(position); + if (shortcut >= 0) { + // shortcutPos should always range from 1 to SHORTCUT_COUNT + return super.getViewTypeCount() + shortcut; + } else { + return super.getItemViewType(position); + } + } + + @Override + public int getViewTypeCount() { + // Number of item view types in the super implementation + 2 for the 2 new shortcuts + return super.getViewTypeCount() + SHORTCUT_COUNT; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final int shortcutType = getShortcutTypeFromPosition(position); + if (shortcutType >= 0) { + if (convertView != null) { + assignShortcutToView((ContactListItemView) convertView, shortcutType); + return convertView; + } else { + final ContactListItemView v = + new ContactListItemView(getContext(), null, mVideoCallingEnabled); + assignShortcutToView(v, shortcutType); + return v; + } + } else { + return super.getView(position, convertView, parent); + } + } + + @Override + protected ContactListItemView newView( + Context context, int partition, Cursor cursor, int position, ViewGroup parent) { + final ContactListItemView view = super.newView(context, partition, cursor, position, parent); + + view.setSupportVideoCallIcon(mVideoCallingEnabled); + return view; + } + + /** + * @param position The position of the item + * @return The enabled shortcut type matching the given position if the item is a shortcut, -1 + * otherwise + */ + public int getShortcutTypeFromPosition(int position) { + int shortcutCount = position - super.getCount(); + if (shortcutCount >= 0) { + // Iterate through the array of shortcuts, looking only for shortcuts where + // mShortcutEnabled[i] is true + for (int i = 0; shortcutCount >= 0 && i < mShortcutEnabled.length; i++) { + if (mShortcutEnabled[i]) { + shortcutCount--; + if (shortcutCount < 0) { + return i; + } + } + } + throw new IllegalArgumentException( + "Invalid position - greater than cursor count " + " but not a shortcut."); + } + return SHORTCUT_INVALID; + } + + @Override + public boolean isEmpty() { + return getShortcutCount() == 0 && super.isEmpty(); + } + + @Override + public boolean isEnabled(int position) { + final int shortcutType = getShortcutTypeFromPosition(position); + if (shortcutType >= 0) { + return true; + } else { + return super.isEnabled(position); + } + } + + private void assignShortcutToView(ContactListItemView v, int shortcutType) { + final CharSequence text; + final int drawableId; + final Resources resources = getContext().getResources(); + final String number = getFormattedQueryString(); + switch (shortcutType) { + case SHORTCUT_DIRECT_CALL: + text = + ContactDisplayUtils.getTtsSpannedPhoneNumber( + resources, + R.string.search_shortcut_call_number, + mBidiFormatter.unicodeWrap(number, TextDirectionHeuristics.LTR)); + drawableId = R.drawable.ic_search_phone; + break; + case SHORTCUT_CREATE_NEW_CONTACT: + text = resources.getString(R.string.search_shortcut_create_new_contact); + drawableId = R.drawable.ic_search_add_contact; + break; + case SHORTCUT_ADD_TO_EXISTING_CONTACT: + text = resources.getString(R.string.search_shortcut_add_to_contact); + drawableId = R.drawable.ic_person_24dp; + break; + case SHORTCUT_SEND_SMS_MESSAGE: + text = resources.getString(R.string.search_shortcut_send_sms_message); + drawableId = R.drawable.ic_message_24dp; + break; + case SHORTCUT_MAKE_VIDEO_CALL: + text = resources.getString(R.string.search_shortcut_make_video_call); + drawableId = R.drawable.ic_videocam; + break; + case SHORTCUT_BLOCK_NUMBER: + text = resources.getString(R.string.search_shortcut_block_number); + drawableId = R.drawable.ic_not_interested_googblue_24dp; + break; + default: + throw new IllegalArgumentException("Invalid shortcut type"); + } + v.setDrawableResource(drawableId); + v.setDisplayName(text); + v.setPhotoPosition(super.getPhotoPosition()); + v.setAdjustSelectionBoundsEnabled(false); + } + + /** @return True if the shortcut state (disabled vs enabled) was changed by this operation */ + public boolean setShortcutEnabled(int shortcutType, boolean visible) { + final boolean changed = mShortcutEnabled[shortcutType] != visible; + mShortcutEnabled[shortcutType] = visible; + return changed; + } + + public String getFormattedQueryString() { + return mFormattedQueryString; + } + + @Override + public void setQueryString(String queryString) { + mFormattedQueryString = + PhoneNumberUtils.formatNumber(PhoneNumberUtils.normalizeNumber(queryString), mCountryIso); + super.setQueryString(queryString); + } +} diff --git a/java/com/android/dialer/app/list/DragDropController.java b/java/com/android/dialer/app/list/DragDropController.java new file mode 100644 index 000000000..c22dd1318 --- /dev/null +++ b/java/com/android/dialer/app/list/DragDropController.java @@ -0,0 +1,106 @@ +/* + * 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.dialer.app.list; + +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.View; +import java.util.ArrayList; +import java.util.List; + +/** + * Class that handles and combines drag events generated from multiple views, and then fires off + * events to any OnDragDropListeners that have registered for callbacks. + */ +public class DragDropController { + + private final List mOnDragDropListeners = new ArrayList(); + private final DragItemContainer mDragItemContainer; + private final int[] mLocationOnScreen = new int[2]; + + public DragDropController(DragItemContainer dragItemContainer) { + mDragItemContainer = dragItemContainer; + } + + /** @return True if the drag is started, false if the drag is cancelled for some reason. */ + boolean handleDragStarted(View v, int x, int y) { + int screenX = x; + int screenY = y; + // The coordinates in dragEvent of DragEvent.ACTION_DRAG_STARTED before NYC is window-related. + // This is fixed in NYC. + if (VERSION.SDK_INT >= VERSION_CODES.N) { + v.getLocationOnScreen(mLocationOnScreen); + screenX = x + mLocationOnScreen[0]; + screenY = y + mLocationOnScreen[1]; + } + final PhoneFavoriteSquareTileView tileView = + mDragItemContainer.getViewForLocation(screenX, screenY); + if (tileView == null) { + return false; + } + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDragStarted(screenX, screenY, tileView); + } + + return true; + } + + public void handleDragHovered(View v, int x, int y) { + v.getLocationOnScreen(mLocationOnScreen); + final int screenX = x + mLocationOnScreen[0]; + final int screenY = y + mLocationOnScreen[1]; + final PhoneFavoriteSquareTileView view = + mDragItemContainer.getViewForLocation(screenX, screenY); + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDragHovered(screenX, screenY, view); + } + } + + public void handleDragFinished(int x, int y, boolean isRemoveView) { + if (isRemoveView) { + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDroppedOnRemove(); + } + } + + for (int i = 0; i < mOnDragDropListeners.size(); i++) { + mOnDragDropListeners.get(i).onDragFinished(x, y); + } + } + + public void addOnDragDropListener(OnDragDropListener listener) { + if (!mOnDragDropListeners.contains(listener)) { + mOnDragDropListeners.add(listener); + } + } + + public void removeOnDragDropListener(OnDragDropListener listener) { + if (mOnDragDropListeners.contains(listener)) { + mOnDragDropListeners.remove(listener); + } + } + + /** + * Callback interface used to retrieve views based on the current touch coordinates of the drag + * event. The {@link DragItemContainer} houses the draggable views that this {@link + * DragDropController} controls. + */ + public interface DragItemContainer { + + PhoneFavoriteSquareTileView getViewForLocation(int x, int y); + } +} diff --git a/java/com/android/dialer/app/list/ListsFragment.java b/java/com/android/dialer/app/list/ListsFragment.java new file mode 100644 index 000000000..725ad3001 --- /dev/null +++ b/java/com/android/dialer/app/list/ListsFragment.java @@ -0,0 +1,587 @@ +/* + * 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.dialer.app.list; + +import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentManager; +import android.content.SharedPreferences; +import android.database.ContentObserver; +import android.database.Cursor; +import android.os.Bundle; +import android.os.Handler; +import android.os.Trace; +import android.preference.PreferenceManager; +import android.provider.VoicemailContract; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentPagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v4.view.ViewPager.OnPageChangeListener; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import com.android.contacts.common.list.ViewPagerTabs; +import com.android.dialer.app.R; +import com.android.dialer.app.calllog.CallLogFragment; +import com.android.dialer.app.calllog.CallLogNotificationsHelper; +import com.android.dialer.app.calllog.VisualVoicemailCallLogFragment; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source; +import com.android.dialer.app.widget.ActionBarController; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.util.ViewUtil; +import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker; +import com.android.dialer.voicemailstatus.VoicemailStatusHelper; +import com.android.dialer.voicemailstatus.VoicemailStatusHelperImpl; +import java.util.ArrayList; +import java.util.List; + +/** + * Fragment that is used as the main screen of the Dialer. + * + *

Contains a ViewPager that contains various contact lists like the Speed Dial list and the All + * Contacts list. This will also eventually contain the logic that allows sliding the ViewPager + * containing the lists up above the search bar and pin it against the top of the screen. + */ +public class ListsFragment extends Fragment + implements ViewPager.OnPageChangeListener, CallLogQueryHandler.Listener { + + /** Every fragment in the list show implement this interface. */ + public interface ListsPage { + + /** + * Called when the page is resumed, including selecting the page or activity resume. Note: This + * is called before the page fragment is attached to a activity. + * + * @param activity the activity hosting the ListFragment + */ + void onPageResume(@Nullable Activity activity); + + /** + * Called when the page is paused, including selecting another page or activity pause. Note: + * This is called after the page fragment is detached from a activity. + * + * @param activity the activity hosting the ListFragment + */ + void onPagePause(@Nullable Activity activity); + } + + public static final int TAB_INDEX_SPEED_DIAL = 0; + public static final int TAB_INDEX_HISTORY = 1; + public static final int TAB_INDEX_ALL_CONTACTS = 2; + public static final int TAB_INDEX_VOICEMAIL = 3; + public static final int TAB_COUNT_DEFAULT = 3; + public static final int TAB_COUNT_WITH_VOICEMAIL = 4; + private static final String TAG = "ListsFragment"; + private ActionBar mActionBar; + private ViewPager mViewPager; + private ViewPagerTabs mViewPagerTabs; + private ViewPagerAdapter mViewPagerAdapter; + private RemoveView mRemoveView; + private View mRemoveViewContent; + private SpeedDialFragment mSpeedDialFragment; + private CallLogFragment mHistoryFragment; + private AllContactsFragment mAllContactsFragment; + private CallLogFragment mVoicemailFragment; + private ListsPage mCurrentPage; + private SharedPreferences mPrefs; + private boolean mHasActiveVoicemailProvider; + private boolean mHasFetchedVoicemailStatus; + private boolean mShowVoicemailTabAfterVoicemailStatusIsFetched; + private VoicemailStatusHelper mVoicemailStatusHelper; + private ArrayList mOnPageChangeListeners = + new ArrayList(); + private String[] mTabTitles; + private int[] mTabIcons; + /** The position of the currently selected tab. */ + private int mTabIndex = TAB_INDEX_SPEED_DIAL; + + private CallLogQueryHandler mCallLogQueryHandler; + + private final ContentObserver mVoicemailStatusObserver = + new ContentObserver(new Handler()) { + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + mCallLogQueryHandler.fetchVoicemailStatus(); + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + LogUtil.d("ListsFragment.onCreate", null); + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedInstanceState); + + mVoicemailStatusHelper = new VoicemailStatusHelperImpl(); + mHasFetchedVoicemailStatus = false; + + mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + mHasActiveVoicemailProvider = + mPrefs.getBoolean( + VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false); + + Trace.endSection(); + } + + @Override + public void onResume() { + LogUtil.d("ListsFragment.onResume", null); + Trace.beginSection(TAG + " onResume"); + super.onResume(); + + mActionBar = ((AppCompatActivity) getActivity()).getSupportActionBar(); + if (getUserVisibleHint()) { + sendScreenViewForCurrentPosition(); + } + + // Fetch voicemail status to determine if we should show the voicemail tab. + mCallLogQueryHandler = + new CallLogQueryHandler(getActivity(), getActivity().getContentResolver(), this); + mCallLogQueryHandler.fetchVoicemailStatus(); + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + Trace.endSection(); + mCurrentPage = getListsPage(mViewPager.getCurrentItem()); + if (mCurrentPage != null) { + mCurrentPage.onPageResume(getActivity()); + } + } + + @Override + public void onPause() { + LogUtil.d("ListsFragment.onPause", null); + if (mCurrentPage != null) { + mCurrentPage.onPagePause(getActivity()); + } + super.onPause(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + mViewPager.removeOnPageChangeListener(this); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + LogUtil.d("ListsFragment.onCreateView", null); + Trace.beginSection(TAG + " onCreateView"); + Trace.beginSection(TAG + " inflate view"); + final View parentView = inflater.inflate(R.layout.lists_fragment, container, false); + Trace.endSection(); + Trace.beginSection(TAG + " setup views"); + mViewPager = (ViewPager) parentView.findViewById(R.id.lists_pager); + mViewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); + mViewPager.setAdapter(mViewPagerAdapter); + mViewPager.setOffscreenPageLimit(TAB_COUNT_WITH_VOICEMAIL - 1); + mViewPager.addOnPageChangeListener(this); + showTab(TAB_INDEX_SPEED_DIAL); + + mTabTitles = new String[TAB_COUNT_WITH_VOICEMAIL]; + mTabTitles[TAB_INDEX_SPEED_DIAL] = getResources().getString(R.string.tab_speed_dial); + mTabTitles[TAB_INDEX_HISTORY] = getResources().getString(R.string.tab_history); + mTabTitles[TAB_INDEX_ALL_CONTACTS] = getResources().getString(R.string.tab_all_contacts); + mTabTitles[TAB_INDEX_VOICEMAIL] = getResources().getString(R.string.tab_voicemail); + + mTabIcons = new int[TAB_COUNT_WITH_VOICEMAIL]; + mTabIcons[TAB_INDEX_SPEED_DIAL] = R.drawable.ic_grade_24dp; + mTabIcons[TAB_INDEX_HISTORY] = R.drawable.ic_schedule_24dp; + mTabIcons[TAB_INDEX_ALL_CONTACTS] = R.drawable.ic_people_24dp; + mTabIcons[TAB_INDEX_VOICEMAIL] = R.drawable.ic_voicemail_24dp; + + mViewPagerTabs = (ViewPagerTabs) parentView.findViewById(R.id.lists_pager_header); + mViewPagerTabs.configureTabIcons(mTabIcons); + mViewPagerTabs.setViewPager(mViewPager); + addOnPageChangeListener(mViewPagerTabs); + + mRemoveView = (RemoveView) parentView.findViewById(R.id.remove_view); + mRemoveViewContent = parentView.findViewById(R.id.remove_view_content); + + getActivity() + .getContentResolver() + .registerContentObserver( + VoicemailContract.Status.CONTENT_URI, true, mVoicemailStatusObserver); + + Trace.endSection(); + Trace.endSection(); + return parentView; + } + + @Override + public void onDestroy() { + getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver); + super.onDestroy(); + } + + public void addOnPageChangeListener(OnPageChangeListener onPageChangeListener) { + if (!mOnPageChangeListeners.contains(onPageChangeListener)) { + mOnPageChangeListeners.add(onPageChangeListener); + } + } + + /** + * Shows the tab with the specified index. If the voicemail tab index is specified, but the + * voicemail status hasn't been fetched, it will try to show the tab after the voicemail status + * has been fetched. + */ + public void showTab(int index) { + if (index == TAB_INDEX_VOICEMAIL) { + if (mHasActiveVoicemailProvider) { + Logger.get(getContext()).logImpression(DialerImpression.Type.VVM_TAB_VISIBLE); + mViewPager.setCurrentItem(getRtlPosition(TAB_INDEX_VOICEMAIL)); + } else if (!mHasFetchedVoicemailStatus) { + // Try to show the voicemail tab after the voicemail status returns. + mShowVoicemailTabAfterVoicemailStatusIsFetched = true; + } + } else if (index < getTabCount()) { + mViewPager.setCurrentItem(getRtlPosition(index)); + } + } + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + mTabIndex = getRtlPosition(position); + + final int count = mOnPageChangeListeners.size(); + for (int i = 0; i < count; i++) { + mOnPageChangeListeners.get(i).onPageScrolled(position, positionOffset, positionOffsetPixels); + } + } + + @Override + public void onPageSelected(int position) { + LogUtil.i("ListsFragment.onPageSelected", "position: %d", position); + mTabIndex = getRtlPosition(position); + + // Show the tab which has been selected instead. + mShowVoicemailTabAfterVoicemailStatusIsFetched = false; + + final int count = mOnPageChangeListeners.size(); + for (int i = 0; i < count; i++) { + mOnPageChangeListeners.get(i).onPageSelected(position); + } + sendScreenViewForCurrentPosition(); + + if (mCurrentPage != null) { + mCurrentPage.onPagePause(getActivity()); + } + mCurrentPage = getListsPage(position); + if (mCurrentPage != null) { + mCurrentPage.onPageResume(getActivity()); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + final int count = mOnPageChangeListeners.size(); + for (int i = 0; i < count; i++) { + mOnPageChangeListeners.get(i).onPageScrollStateChanged(state); + } + } + + @Override + public void onVoicemailStatusFetched(Cursor statusCursor) { + mHasFetchedVoicemailStatus = true; + + if (getActivity() == null || getActivity().isFinishing()) { + return; + } + + VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus( + getContext(), statusCursor, Source.Activity); + + // Update mHasActiveVoicemailProvider, which controls the number of tabs displayed. + boolean hasActiveVoicemailProvider = + mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0; + if (hasActiveVoicemailProvider != mHasActiveVoicemailProvider) { + mHasActiveVoicemailProvider = hasActiveVoicemailProvider; + mViewPagerAdapter.notifyDataSetChanged(); + + if (hasActiveVoicemailProvider) { + mViewPagerTabs.updateTab(TAB_INDEX_VOICEMAIL); + } else { + mViewPagerTabs.removeTab(TAB_INDEX_VOICEMAIL); + removeVoicemailFragment(); + } + + mPrefs + .edit() + .putBoolean( + VisualVoicemailEnabledChecker.PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, + hasActiveVoicemailProvider) + .commit(); + } + + if (hasActiveVoicemailProvider) { + mCallLogQueryHandler.fetchVoicemailUnreadCount(); + } + + if (mHasActiveVoicemailProvider && mShowVoicemailTabAfterVoicemailStatusIsFetched) { + mShowVoicemailTabAfterVoicemailStatusIsFetched = false; + showTab(TAB_INDEX_VOICEMAIL); + } + } + + @Override + public void onVoicemailUnreadCountFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing() || cursor == null) { + return; + } + + int count = 0; + try { + count = cursor.getCount(); + } finally { + cursor.close(); + } + + mViewPagerTabs.setUnreadCount(count, TAB_INDEX_VOICEMAIL); + mViewPagerTabs.updateTab(TAB_INDEX_VOICEMAIL); + } + + @Override + public void onMissedCallsUnreadCountFetched(Cursor cursor) { + if (getActivity() == null || getActivity().isFinishing() || cursor == null) { + return; + } + + int count = 0; + try { + count = cursor.getCount(); + } finally { + cursor.close(); + } + + mViewPagerTabs.setUnreadCount(count, TAB_INDEX_HISTORY); + mViewPagerTabs.updateTab(TAB_INDEX_HISTORY); + } + + @Override + public boolean onCallsFetched(Cursor statusCursor) { + // Return false; did not take ownership of cursor + return false; + } + + public int getCurrentTabIndex() { + return mTabIndex; + } + + /** + * External method to update unread count because the unread count changes when the user expands a + * voicemail in the call log or when the user expands an unread call in the call history tab. + */ + public void updateTabUnreadCounts() { + if (mCallLogQueryHandler != null) { + mCallLogQueryHandler.fetchMissedCallsUnreadCount(); + if (mHasActiveVoicemailProvider) { + mCallLogQueryHandler.fetchVoicemailUnreadCount(); + } + } + } + + /** External method to mark all missed calls as read. */ + public void markMissedCallsAsReadAndRemoveNotifications() { + if (mCallLogQueryHandler != null) { + mCallLogQueryHandler.markMissedCallsAsRead(); + CallLogNotificationsHelper.removeMissedCallNotifications(getActivity()); + } + } + + public void showRemoveView(boolean show) { + mRemoveViewContent.setVisibility(show ? View.VISIBLE : View.GONE); + mRemoveView.setAlpha(show ? 0 : 1); + mRemoveView.animate().alpha(show ? 1 : 0).start(); + } + + public boolean shouldShowActionBar() { + // TODO: Update this based on scroll state. + return mActionBar != null; + } + + public SpeedDialFragment getSpeedDialFragment() { + return mSpeedDialFragment; + } + + public RemoveView getRemoveView() { + return mRemoveView; + } + + public int getTabCount() { + return mViewPagerAdapter.getCount(); + } + + private int getRtlPosition(int position) { + if (ViewUtil.isRtl()) { + return mViewPagerAdapter.getCount() - 1 - position; + } + return position; + } + + public void sendScreenViewForCurrentPosition() { + if (!isResumed()) { + return; + } + + int screenType; + switch (getCurrentTabIndex()) { + case TAB_INDEX_SPEED_DIAL: + screenType = ScreenEvent.Type.SPEED_DIAL; + break; + case TAB_INDEX_HISTORY: + screenType = ScreenEvent.Type.CALL_LOG; + break; + case TAB_INDEX_ALL_CONTACTS: + screenType = ScreenEvent.Type.ALL_CONTACTS; + break; + case TAB_INDEX_VOICEMAIL: + screenType = ScreenEvent.Type.VOICEMAIL_LOG; + break; + default: + return; + } + Logger.get(getActivity()).logScreenView(screenType, getActivity()); + } + + private void removeVoicemailFragment() { + if (mVoicemailFragment != null) { + getChildFragmentManager() + .beginTransaction() + .remove(mVoicemailFragment) + .commitAllowingStateLoss(); + mVoicemailFragment = null; + } + } + + private ListsPage getListsPage(int position) { + switch (getRtlPosition(position)) { + case TAB_INDEX_SPEED_DIAL: + return mSpeedDialFragment; + case TAB_INDEX_HISTORY: + return mHistoryFragment; + case TAB_INDEX_ALL_CONTACTS: + return mAllContactsFragment; + case TAB_INDEX_VOICEMAIL: + return mVoicemailFragment; + } + throw new IllegalStateException("No fragment at position " + position); + } + + public interface HostInterface { + + ActionBarController getActionBarController(); + } + + public class ViewPagerAdapter extends FragmentPagerAdapter { + + private final List mFragments = new ArrayList<>(); + + public ViewPagerAdapter(FragmentManager fm) { + super(fm); + for (int i = 0; i < TAB_COUNT_WITH_VOICEMAIL; i++) { + mFragments.add(null); + } + } + + @Override + public long getItemId(int position) { + return getRtlPosition(position); + } + + @Override + public Fragment getItem(int position) { + LogUtil.d("ViewPagerAdapter.getItem", "position: %d", position); + switch (getRtlPosition(position)) { + case TAB_INDEX_SPEED_DIAL: + if (mSpeedDialFragment == null) { + mSpeedDialFragment = new SpeedDialFragment(); + } + return mSpeedDialFragment; + case TAB_INDEX_HISTORY: + if (mHistoryFragment == null) { + mHistoryFragment = new CallLogFragment(); + } + return mHistoryFragment; + case TAB_INDEX_ALL_CONTACTS: + if (mAllContactsFragment == null) { + mAllContactsFragment = new AllContactsFragment(); + } + return mAllContactsFragment; + case TAB_INDEX_VOICEMAIL: + if (mVoicemailFragment == null) { + mVoicemailFragment = new VisualVoicemailCallLogFragment(); + LogUtil.v( + "ViewPagerAdapter.getItem", + "new VisualVoicemailCallLogFragment: %s", + mVoicemailFragment); + } + return mVoicemailFragment; + } + throw new IllegalStateException("No fragment at position " + position); + } + + @Override + public Fragment instantiateItem(ViewGroup container, int position) { + LogUtil.d("ViewPagerAdapter.instantiateItem", "position: %d", position); + // On rotation the FragmentManager handles rotation. Therefore getItem() isn't called. + // Copy the fragments that the FragmentManager finds so that we can store them in + // instance variables for later. + final Fragment fragment = (Fragment) super.instantiateItem(container, position); + if (fragment instanceof SpeedDialFragment) { + mSpeedDialFragment = (SpeedDialFragment) fragment; + } else if (fragment instanceof CallLogFragment && position == TAB_INDEX_HISTORY) { + mHistoryFragment = (CallLogFragment) fragment; + } else if (fragment instanceof AllContactsFragment) { + mAllContactsFragment = (AllContactsFragment) fragment; + } else if (fragment instanceof CallLogFragment && position == TAB_INDEX_VOICEMAIL) { + mVoicemailFragment = (CallLogFragment) fragment; + LogUtil.v("ViewPagerAdapter.instantiateItem", mVoicemailFragment.toString()); + } + mFragments.set(position, fragment); + return fragment; + } + + /** + * When {@link android.support.v4.view.PagerAdapter#notifyDataSetChanged} is called, this method + * is called on all pages to determine whether they need to be recreated. When the voicemail tab + * is removed, the view needs to be recreated by returning POSITION_NONE. If + * notifyDataSetChanged is called for some other reason, the voicemail tab is recreated only if + * it is active. All other tabs do not need to be recreated and POSITION_UNCHANGED is returned. + */ + @Override + public int getItemPosition(Object object) { + return !mHasActiveVoicemailProvider && mFragments.indexOf(object) == TAB_INDEX_VOICEMAIL + ? POSITION_NONE + : POSITION_UNCHANGED; + } + + @Override + public int getCount() { + return mHasActiveVoicemailProvider ? TAB_COUNT_WITH_VOICEMAIL : TAB_COUNT_DEFAULT; + } + + @Override + public CharSequence getPageTitle(int position) { + return mTabTitles[position]; + } + } +} diff --git a/java/com/android/dialer/app/list/OnDragDropListener.java b/java/com/android/dialer/app/list/OnDragDropListener.java new file mode 100644 index 000000000..b71c7fef6 --- /dev/null +++ b/java/com/android/dialer/app/list/OnDragDropListener.java @@ -0,0 +1,58 @@ +/* + * 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.dialer.app.list; + +/** + * Classes that want to receive callbacks in response to drag events should implement this + * interface. + */ +public interface OnDragDropListener { + + /** + * Called when a drag is started. + * + * @param x X-coordinate of the drag event + * @param y Y-coordinate of the drag event + * @param view The contact tile which the drag was started on + */ + void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view); + + /** + * Called when a drag is in progress and the user moves the dragged contact to a location. + * + * @param x X-coordinate of the drag event + * @param y Y-coordinate of the drag event + * @param view Contact tile in the ListView which is currently being displaced by the dragged + * contact + */ + void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view); + + /** + * Called when a drag is completed (whether by dropping it somewhere or simply by dragging the + * contact off the screen) + * + * @param x X-coordinate of the drag event + * @param y Y-coordinate of the drag event + */ + void onDragFinished(int x, int y); + + /** + * Called when a contact has been dropped on the remove view, indicating that the user wants to + * remove this contact. + */ + void onDroppedOnRemove(); +} diff --git a/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java b/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java new file mode 100644 index 000000000..a76f3b527 --- /dev/null +++ b/java/com/android/dialer/app/list/OnListFragmentScrolledListener.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 Google Inc. + * Licensed to 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.dialer.app.list; + +/* + * Interface to provide callback to activity when a child fragment is scrolled + */ +public interface OnListFragmentScrolledListener { + + void onListFragmentScrollStateChange(int scrollState); + + void onListFragmentScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount); +} diff --git a/java/com/android/dialer/app/list/PhoneFavoriteListView.java b/java/com/android/dialer/app/list/PhoneFavoriteListView.java new file mode 100644 index 000000000..9516f0611 --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoriteListView.java @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2012 Google Inc. + * Licensed to 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.dialer.app.list; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.DragEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.GridView; +import android.widget.ImageView; +import com.android.dialer.app.R; +import com.android.dialer.app.list.DragDropController.DragItemContainer; + +/** Viewgroup that presents the user's speed dial contacts in a grid. */ +public class PhoneFavoriteListView extends GridView + implements OnDragDropListener, DragItemContainer { + + public static final String LOG_TAG = PhoneFavoriteListView.class.getSimpleName(); + final int[] mLocationOnScreen = new int[2]; + private final long SCROLL_HANDLER_DELAY_MILLIS = 5; + private final int DRAG_SCROLL_PX_UNIT = 25; + private final float DRAG_SHADOW_ALPHA = 0.7f; + /** + * {@link #mTopScrollBound} and {@link mBottomScrollBound} will be offseted to the top / bottom by + * {@link #getHeight} * {@link #BOUND_GAP_RATIO} pixels. + */ + private final float BOUND_GAP_RATIO = 0.2f; + + private float mTouchSlop; + private int mTopScrollBound; + private int mBottomScrollBound; + private int mLastDragY; + private Handler mScrollHandler; + private final Runnable mDragScroller = + new Runnable() { + @Override + public void run() { + if (mLastDragY <= mTopScrollBound) { + smoothScrollBy(-DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); + } else if (mLastDragY >= mBottomScrollBound) { + smoothScrollBy(DRAG_SCROLL_PX_UNIT, (int) SCROLL_HANDLER_DELAY_MILLIS); + } + mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY_MILLIS); + } + }; + private boolean mIsDragScrollerRunning = false; + private int mTouchDownForDragStartX; + private int mTouchDownForDragStartY; + private Bitmap mDragShadowBitmap; + private ImageView mDragShadowOverlay; + private final AnimatorListenerAdapter mDragShadowOverAnimatorListener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (mDragShadowBitmap != null) { + mDragShadowBitmap.recycle(); + mDragShadowBitmap = null; + } + mDragShadowOverlay.setVisibility(GONE); + mDragShadowOverlay.setImageBitmap(null); + } + }; + private View mDragShadowParent; + private int mAnimationDuration; + // X and Y offsets inside the item from where the user grabbed to the + // child's left coordinate. This is used to aid in the drawing of the drag shadow. + private int mTouchOffsetToChildLeft; + private int mTouchOffsetToChildTop; + private int mDragShadowLeft; + private int mDragShadowTop; + private DragDropController mDragDropController = new DragDropController(this); + + public PhoneFavoriteListView(Context context) { + this(context, null); + } + + public PhoneFavoriteListView(Context context, AttributeSet attrs) { + this(context, attrs, -1); + } + + public PhoneFavoriteListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + mAnimationDuration = context.getResources().getInteger(R.integer.fade_duration); + mTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop(); + mDragDropController.addOnDragDropListener(this); + } + + @Override + protected void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); + } + + /** + * TODO: This is all swipe to remove code (nothing to do with drag to remove). This should be + * cleaned up and removed once drag to remove becomes the only way to remove contacts. + */ + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mTouchDownForDragStartX = (int) ev.getX(); + mTouchDownForDragStartY = (int) ev.getY(); + } + + return super.onInterceptTouchEvent(ev); + } + + @Override + public boolean onDragEvent(DragEvent event) { + final int action = event.getAction(); + final int eX = (int) event.getX(); + final int eY = (int) event.getY(); + switch (action) { + case DragEvent.ACTION_DRAG_STARTED: + { + if (!PhoneFavoriteTileView.DRAG_PHONE_FAVORITE_TILE.equals(event.getLocalState())) { + // Ignore any drag events that were not propagated by long pressing + // on a {@link PhoneFavoriteTileView} + return false; + } + if (!mDragDropController.handleDragStarted(this, eX, eY)) { + return false; + } + break; + } + case DragEvent.ACTION_DRAG_LOCATION: + mLastDragY = eY; + mDragDropController.handleDragHovered(this, eX, eY); + // Kick off {@link #mScrollHandler} if it's not started yet. + if (!mIsDragScrollerRunning + && + // And if the distance traveled while dragging exceeds the touch slop + (Math.abs(mLastDragY - mTouchDownForDragStartY) >= 4 * mTouchSlop)) { + mIsDragScrollerRunning = true; + ensureScrollHandler(); + mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY_MILLIS); + } + break; + case DragEvent.ACTION_DRAG_ENTERED: + final int boundGap = (int) (getHeight() * BOUND_GAP_RATIO); + mTopScrollBound = (getTop() + boundGap); + mBottomScrollBound = (getBottom() - boundGap); + break; + case DragEvent.ACTION_DRAG_EXITED: + case DragEvent.ACTION_DRAG_ENDED: + case DragEvent.ACTION_DROP: + ensureScrollHandler(); + mScrollHandler.removeCallbacks(mDragScroller); + mIsDragScrollerRunning = false; + // Either a successful drop or it's ended with out drop. + if (action == DragEvent.ACTION_DROP || action == DragEvent.ACTION_DRAG_ENDED) { + mDragDropController.handleDragFinished(eX, eY, false); + } + break; + default: + break; + } + // This ListView will consume the drag events on behalf of its children. + return true; + } + + public void setDragShadowOverlay(ImageView overlay) { + mDragShadowOverlay = overlay; + mDragShadowParent = (View) mDragShadowOverlay.getParent(); + } + + /** Find the view under the pointer. */ + private View getViewAtPosition(int x, int y) { + final int count = getChildCount(); + View child; + for (int childIdx = 0; childIdx < count; childIdx++) { + child = getChildAt(childIdx); + if (y >= child.getTop() + && y <= child.getBottom() + && x >= child.getLeft() + && x <= child.getRight()) { + return child; + } + } + return null; + } + + private void ensureScrollHandler() { + if (mScrollHandler == null) { + mScrollHandler = getHandler(); + } + } + + public DragDropController getDragDropController() { + return mDragDropController; + } + + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView tileView) { + if (mDragShadowOverlay == null) { + return; + } + + mDragShadowOverlay.clearAnimation(); + mDragShadowBitmap = createDraggedChildBitmap(tileView); + if (mDragShadowBitmap == null) { + return; + } + + tileView.getLocationOnScreen(mLocationOnScreen); + mDragShadowLeft = mLocationOnScreen[0]; + mDragShadowTop = mLocationOnScreen[1]; + + // x and y are the coordinates of the on-screen touch event. Using these + // and the on-screen location of the tileView, calculate the difference between + // the position of the user's finger and the position of the tileView. These will + // be used to offset the location of the drag shadow so that it appears that the + // tileView is positioned directly under the user's finger. + mTouchOffsetToChildLeft = x - mDragShadowLeft; + mTouchOffsetToChildTop = y - mDragShadowTop; + + mDragShadowParent.getLocationOnScreen(mLocationOnScreen); + mDragShadowLeft -= mLocationOnScreen[0]; + mDragShadowTop -= mLocationOnScreen[1]; + + mDragShadowOverlay.setImageBitmap(mDragShadowBitmap); + mDragShadowOverlay.setVisibility(VISIBLE); + mDragShadowOverlay.setAlpha(DRAG_SHADOW_ALPHA); + + mDragShadowOverlay.setX(mDragShadowLeft); + mDragShadowOverlay.setY(mDragShadowTop); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView tileView) { + // Update the drag shadow location. + mDragShadowParent.getLocationOnScreen(mLocationOnScreen); + mDragShadowLeft = x - mTouchOffsetToChildLeft - mLocationOnScreen[0]; + mDragShadowTop = y - mTouchOffsetToChildTop - mLocationOnScreen[1]; + // Draw the drag shadow at its last known location if the drag shadow exists. + if (mDragShadowOverlay != null) { + mDragShadowOverlay.setX(mDragShadowLeft); + mDragShadowOverlay.setY(mDragShadowTop); + } + } + + @Override + public void onDragFinished(int x, int y) { + if (mDragShadowOverlay != null) { + mDragShadowOverlay.clearAnimation(); + mDragShadowOverlay + .animate() + .alpha(0.0f) + .setDuration(mAnimationDuration) + .setListener(mDragShadowOverAnimatorListener) + .start(); + } + } + + @Override + public void onDroppedOnRemove() {} + + private Bitmap createDraggedChildBitmap(View view) { + view.setDrawingCacheEnabled(true); + final Bitmap cache = view.getDrawingCache(); + + Bitmap bitmap = null; + if (cache != null) { + try { + bitmap = cache.copy(Bitmap.Config.ARGB_8888, false); + } catch (final OutOfMemoryError e) { + Log.w(LOG_TAG, "Failed to copy bitmap from Drawing cache", e); + bitmap = null; + } + } + + view.destroyDrawingCache(); + view.setDrawingCacheEnabled(false); + + return bitmap; + } + + @Override + public PhoneFavoriteSquareTileView getViewForLocation(int x, int y) { + getLocationOnScreen(mLocationOnScreen); + // Calculate the X and Y coordinates of the drag event relative to the view + final int viewX = x - mLocationOnScreen[0]; + final int viewY = y - mLocationOnScreen[1]; + final View child = getViewAtPosition(viewX, viewY); + + if (!(child instanceof PhoneFavoriteSquareTileView)) { + return null; + } + + return (PhoneFavoriteSquareTileView) child; + } +} diff --git a/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java new file mode 100644 index 000000000..5a18d039b --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java @@ -0,0 +1,119 @@ +/* + +* 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.dialer.app.list; + +import android.content.Context; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.QuickContact; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageButton; +import android.widget.TextView; +import com.android.contacts.common.list.ContactEntry; +import com.android.dialer.app.R; +import com.android.dialer.compat.CompatUtils; + +/** Displays the contact's picture overlaid with their name and number type in a tile. */ +public class PhoneFavoriteSquareTileView extends PhoneFavoriteTileView { + + private static final String TAG = PhoneFavoriteSquareTileView.class.getSimpleName(); + + private final float mHeightToWidthRatio; + + private ImageButton mSecondaryButton; + + private ContactEntry mContactEntry; + + public PhoneFavoriteSquareTileView(Context context, AttributeSet attrs) { + super(context, attrs); + + mHeightToWidthRatio = + getResources().getFraction(R.dimen.contact_tile_height_to_width_ratio, 1, 1); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + final TextView nameView = (TextView) findViewById(R.id.contact_tile_name); + nameView.setElegantTextHeight(false); + final TextView phoneTypeView = (TextView) findViewById(R.id.contact_tile_phone_type); + phoneTypeView.setElegantTextHeight(false); + mSecondaryButton = (ImageButton) findViewById(R.id.contact_tile_secondary_button); + } + + @Override + protected int getApproximateImageSize() { + // The picture is the full size of the tile (minus some padding, but we can be generous) + return getWidth(); + } + + private void launchQuickContact() { + if (CompatUtils.hasPrioritizedMimeType()) { + QuickContact.showQuickContact( + getContext(), + PhoneFavoriteSquareTileView.this, + getLookupUri(), + null, + Phone.CONTENT_ITEM_TYPE); + } else { + QuickContact.showQuickContact( + getContext(), + PhoneFavoriteSquareTileView.this, + getLookupUri(), + QuickContact.MODE_LARGE, + null); + } + } + + @Override + public void loadFromContact(ContactEntry entry) { + super.loadFromContact(entry); + if (entry != null) { + mSecondaryButton.setOnClickListener( + new OnClickListener() { + @Override + public void onClick(View v) { + launchQuickContact(); + } + }); + } + mContactEntry = entry; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int width = MeasureSpec.getSize(widthMeasureSpec); + final int height = (int) (mHeightToWidthRatio * width); + final int count = getChildCount(); + for (int i = 0; i < count; i++) { + getChildAt(i) + .measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); + } + setMeasuredDimension(width, height); + } + + @Override + protected String getNameForView(ContactEntry contactEntry) { + return contactEntry.getPreferredDisplayName(); + } + + public ContactEntry getContactEntry() { + return mContactEntry; + } +} diff --git a/java/com/android/dialer/app/list/PhoneFavoriteTileView.java b/java/com/android/dialer/app/list/PhoneFavoriteTileView.java new file mode 100644 index 000000000..db89cf3dc --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoriteTileView.java @@ -0,0 +1,155 @@ +/* + +* 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.dialer.app.list; + +import android.content.ClipData; +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.MoreContactUtils; +import com.android.contacts.common.list.ContactEntry; +import com.android.contacts.common.list.ContactTileView; +import com.android.dialer.app.R; + +/** + * A light version of the {@link com.android.contacts.common.list.ContactTileView} that is used in + * Dialtacts for frequently called contacts. Slightly different behavior from superclass when you + * tap it, you want to call the frequently-called number for the contact, even if that is not the + * default number for that contact. This abstract class is the super class to both the row and tile + * view. + */ +public abstract class PhoneFavoriteTileView extends ContactTileView { + + // Constant to pass to the drag event so that the drag action only happens when a phone favorite + // tile is long pressed. + static final String DRAG_PHONE_FAVORITE_TILE = "PHONE_FAVORITE_TILE"; + private static final String TAG = PhoneFavoriteTileView.class.getSimpleName(); + private static final boolean DEBUG = false; + // These parameters instruct the photo manager to display the default image/letter at 70% of + // its normal size, and vertically offset upwards 12% towards the top of the letter tile, to + // make room for the contact name and number label at the bottom of the image. + private static final float DEFAULT_IMAGE_LETTER_OFFSET = -0.12f; + private static final float DEFAULT_IMAGE_LETTER_SCALE = 0.70f; + // Dummy clip data object that is attached to drag shadows so that text views + // don't crash with an NPE if the drag shadow is released in their bounds + private static final ClipData EMPTY_CLIP_DATA = ClipData.newPlainText("", ""); + /** View that contains the transparent shadow that is overlaid on top of the contact image. */ + private View mShadowOverlay; + /** Users' most frequent phone number. */ + private String mPhoneNumberString; + + public PhoneFavoriteTileView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mShadowOverlay = findViewById(R.id.shadow_overlay); + + setOnLongClickListener( + new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + final PhoneFavoriteTileView view = (PhoneFavoriteTileView) v; + // NOTE The drag shadow is handled in the ListView. + view.startDrag( + EMPTY_CLIP_DATA, new View.DragShadowBuilder(), DRAG_PHONE_FAVORITE_TILE, 0); + return true; + } + }); + } + + @Override + public void loadFromContact(ContactEntry entry) { + super.loadFromContact(entry); + // Set phone number to null in case we're reusing the view. + mPhoneNumberString = null; + if (entry != null) { + // Grab the phone-number to call directly. See {@link onClick()}. + mPhoneNumberString = entry.phoneNumber; + + // If this is a blank entry, don't show anything. + // TODO krelease: Just hide the view for now. For this to truly look like an empty row + // the entire ContactTileRow needs to be hidden. + if (entry == ContactEntry.BLANK_ENTRY) { + setVisibility(View.INVISIBLE); + } else { + final ImageView starIcon = (ImageView) findViewById(R.id.contact_star_icon); + starIcon.setVisibility(entry.isFavorite ? View.VISIBLE : View.GONE); + setVisibility(View.VISIBLE); + } + } + } + + @Override + protected boolean isDarkTheme() { + return false; + } + + @Override + protected OnClickListener createClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (mListener == null) { + return; + } + if (TextUtils.isEmpty(mPhoneNumberString)) { + // Copy "superclass" implementation + mListener.onContactSelected( + getLookupUri(), MoreContactUtils.getTargetRectFromView(PhoneFavoriteTileView.this)); + } else { + // When you tap a frequently-called contact, you want to + // call them at the number that you usually talk to them + // at (i.e. the one displayed in the UI), regardless of + // whether that's their default number. + mListener.onCallNumberDirectly(mPhoneNumberString); + } + } + }; + } + + @Override + protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) { + return new DefaultImageRequest( + displayName, + lookupKey, + ContactPhotoManager.TYPE_DEFAULT, + DEFAULT_IMAGE_LETTER_SCALE, + DEFAULT_IMAGE_LETTER_OFFSET, + false); + } + + @Override + protected void configureViewForImage(boolean isDefaultImage) { + // Hide the shadow overlay if the image is a default image (i.e. colored letter tile) + if (mShadowOverlay != null) { + mShadowOverlay.setVisibility(isDefaultImage ? View.GONE : View.VISIBLE); + } + } + + @Override + protected boolean isContactPhotoCircular() { + // Unlike Contacts' tiles, the Dialer's favorites tiles are square. + return false; + } +} diff --git a/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java b/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java new file mode 100644 index 000000000..c692ecac7 --- /dev/null +++ b/java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java @@ -0,0 +1,627 @@ +/* + * 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.dialer.app.list; + +import android.content.ContentProviderOperation; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.OperationApplicationException; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.RemoteException; +import android.provider.ContactsContract; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.provider.ContactsContract.Contacts; +import android.provider.ContactsContract.PinnedPositions; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; +import android.util.LongSparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactTileLoaderFactory; +import com.android.contacts.common.list.ContactEntry; +import com.android.contacts.common.list.ContactTileView; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.dialer.app.R; +import com.android.dialer.shortcuts.ShortcutRefresher; +import com.google.common.collect.ComparisonChain; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; +import java.util.PriorityQueue; + +/** Also allows for a configurable number of columns as well as a maximum row of tiled contacts. */ +public class PhoneFavoritesTileAdapter extends BaseAdapter implements OnDragDropListener { + + // Pinned positions start from 1, so there are a total of 20 maximum pinned contacts + private static final int PIN_LIMIT = 21; + private static final String TAG = PhoneFavoritesTileAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + /** + * The soft limit on how many contact tiles to show. NOTE This soft limit would not restrict the + * number of starred contacts to show, rather 1. If the count of starred contacts is less than + * this limit, show 20 tiles total. 2. If the count of starred contacts is more than or equal to + * this limit, show all starred tiles and no frequents. + */ + private static final int TILES_SOFT_LIMIT = 20; + /** Contact data stored in cache. This is used to populate the associated view. */ + private ArrayList mContactEntries = null; + + private int mNumFrequents; + private int mNumStarred; + + private ContactTileView.Listener mListener; + private OnDataSetChangedForAnimationListener mDataSetChangedListener; + private Context mContext; + private Resources mResources; + private ContactsPreferences mContactsPreferences; + private final Comparator mContactEntryComparator = + new Comparator() { + @Override + public int compare(ContactEntry lhs, ContactEntry rhs) { + return ComparisonChain.start() + .compare(lhs.pinned, rhs.pinned) + .compare(getPreferredSortName(lhs), getPreferredSortName(rhs)) + .result(); + } + + private String getPreferredSortName(ContactEntry contactEntry) { + if (mContactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY + || TextUtils.isEmpty(contactEntry.nameAlternative)) { + return contactEntry.namePrimary; + } + return contactEntry.nameAlternative; + } + }; + /** Back up of the temporarily removed Contact during dragging. */ + private ContactEntry mDraggedEntry = null; + /** Position of the temporarily removed contact in the cache. */ + private int mDraggedEntryIndex = -1; + /** New position of the temporarily removed contact in the cache. */ + private int mDropEntryIndex = -1; + /** New position of the temporarily entered contact in the cache. */ + private int mDragEnteredEntryIndex = -1; + + private boolean mAwaitingRemove = false; + private boolean mDelayCursorUpdates = false; + private ContactPhotoManager mPhotoManager; + + /** Indicates whether a drag is in process. */ + private boolean mInDragging = false; + + public PhoneFavoritesTileAdapter( + Context context, + ContactTileView.Listener listener, + OnDataSetChangedForAnimationListener dataSetChangedListener) { + mDataSetChangedListener = dataSetChangedListener; + mListener = listener; + mContext = context; + mResources = context.getResources(); + mContactsPreferences = new ContactsPreferences(mContext); + mNumFrequents = 0; + mContactEntries = new ArrayList<>(); + } + + void setPhotoLoader(ContactPhotoManager photoLoader) { + mPhotoManager = photoLoader; + } + + /** + * Indicates whether a drag is in process. + * + * @param inDragging Boolean variable indicating whether there is a drag in process. + */ + private void setInDragging(boolean inDragging) { + mDelayCursorUpdates = inDragging; + mInDragging = inDragging; + } + + void refreshContactsPreferences() { + mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); + mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY); + } + + /** + * Gets the number of frequents from the passed in cursor. + * + *

This methods is needed so the GroupMemberTileAdapter can override this. + * + * @param cursor The cursor to get number of frequents from. + */ + private void saveNumFrequentsFromCursor(Cursor cursor) { + mNumFrequents = cursor.getCount() - mNumStarred; + } + + /** + * Creates {@link ContactTileView}s for each item in {@link Cursor}. + * + *

Else use {@link ContactTileLoaderFactory} + */ + void setContactCursor(Cursor cursor) { + if (!mDelayCursorUpdates && cursor != null && !cursor.isClosed()) { + mNumStarred = getNumStarredContacts(cursor); + if (mAwaitingRemove) { + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + } + + saveNumFrequentsFromCursor(cursor); + saveCursorToCache(cursor); + // cause a refresh of any views that rely on this data + notifyDataSetChanged(); + // about to start redraw + mDataSetChangedListener.onDataSetChangedForAnimation(); + } + } + + /** + * Saves the cursor data to the cache, to speed up UI changes. + * + * @param cursor Returned cursor from {@link ContactTileLoaderFactory} with data to populate the + * view. + */ + private void saveCursorToCache(Cursor cursor) { + mContactEntries.clear(); + + if (cursor == null) { + return; + } + + final LongSparseArray duplicates = new LongSparseArray<>(cursor.getCount()); + + // Track the length of {@link #mContactEntries} and compare to {@link #TILES_SOFT_LIMIT}. + int counter = 0; + + // The cursor should not be closed since this is invoked from a CursorLoader. + if (cursor.moveToFirst()) { + int starredColumn = cursor.getColumnIndexOrThrow(Contacts.STARRED); + int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID); + int photoUriColumn = cursor.getColumnIndexOrThrow(Contacts.PHOTO_URI); + int lookupKeyColumn = cursor.getColumnIndexOrThrow(Contacts.LOOKUP_KEY); + int pinnedColumn = cursor.getColumnIndexOrThrow(Contacts.PINNED); + int nameColumn = cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY); + int nameAlternativeColumn = cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_ALTERNATIVE); + int isDefaultNumberColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY); + int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE); + int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL); + int phoneNumberColumn = cursor.getColumnIndexOrThrow(Phone.NUMBER); + do { + final int starred = cursor.getInt(starredColumn); + final long id; + + // We display a maximum of TILES_SOFT_LIMIT contacts, or the total number of starred + // whichever is greater. + if (starred < 1 && counter >= TILES_SOFT_LIMIT) { + break; + } else { + id = cursor.getLong(contactIdColumn); + } + + final ContactEntry existing = (ContactEntry) duplicates.get(id); + if (existing != null) { + // Check if the existing number is a default number. If not, clear the phone number + // and label fields so that the disambiguation dialog will show up. + if (!existing.isDefaultNumber) { + existing.phoneLabel = null; + existing.phoneNumber = null; + } + continue; + } + + final String photoUri = cursor.getString(photoUriColumn); + final String lookupKey = cursor.getString(lookupKeyColumn); + final int pinned = cursor.getInt(pinnedColumn); + final String name = cursor.getString(nameColumn); + final String nameAlternative = cursor.getString(nameAlternativeColumn); + final boolean isStarred = cursor.getInt(starredColumn) > 0; + final boolean isDefaultNumber = cursor.getInt(isDefaultNumberColumn) > 0; + + final ContactEntry contact = new ContactEntry(); + + contact.id = id; + contact.namePrimary = + (!TextUtils.isEmpty(name)) ? name : mResources.getString(R.string.missing_name); + contact.nameAlternative = + (!TextUtils.isEmpty(nameAlternative)) + ? nameAlternative + : mResources.getString(R.string.missing_name); + contact.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); + contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null); + contact.lookupKey = lookupKey; + contact.lookupUri = + ContentUris.withAppendedId( + Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), id); + contact.isFavorite = isStarred; + contact.isDefaultNumber = isDefaultNumber; + + // Set phone number and label + final int phoneNumberType = cursor.getInt(phoneTypeColumn); + final String phoneNumberCustomLabel = cursor.getString(phoneLabelColumn); + contact.phoneLabel = + (String) Phone.getTypeLabel(mResources, phoneNumberType, phoneNumberCustomLabel); + contact.phoneNumber = cursor.getString(phoneNumberColumn); + + contact.pinned = pinned; + mContactEntries.add(contact); + + duplicates.put(id, contact); + + counter++; + } while (cursor.moveToNext()); + } + + mAwaitingRemove = false; + + arrangeContactsByPinnedPosition(mContactEntries); + + ShortcutRefresher.refresh(mContext, mContactEntries); + notifyDataSetChanged(); + } + + /** Iterates over the {@link Cursor} Returns position of the first NON Starred Contact */ + private int getNumStarredContacts(Cursor cursor) { + if (cursor == null) { + return 0; + } + + if (cursor.moveToFirst()) { + int starredColumn = cursor.getColumnIndex(Contacts.STARRED); + do { + if (cursor.getInt(starredColumn) == 0) { + return cursor.getPosition(); + } + } while (cursor.moveToNext()); + } + // There are not NON Starred contacts in cursor + // Set divider position to end + return cursor.getCount(); + } + + /** Returns the number of frequents that will be displayed in the list. */ + int getNumFrequents() { + return mNumFrequents; + } + + @Override + public int getCount() { + if (mContactEntries == null) { + return 0; + } + + return mContactEntries.size(); + } + + /** + * Returns an ArrayList of the {@link ContactEntry}s that are to appear on the row for the given + * position. + */ + @Override + public ContactEntry getItem(int position) { + return mContactEntries.get(position); + } + + /** + * For the top row of tiled contacts, the item id is the position of the row of contacts. For + * frequent contacts, the item id is the maximum number of rows of tiled contacts + the actual + * contact id. Since contact ids are always greater than 0, this guarantees that all items within + * this adapter will always have unique ids. + */ + @Override + public long getItemId(int position) { + return getItem(position).id; + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return getCount() > 0; + } + + @Override + public void notifyDataSetChanged() { + if (DEBUG) { + Log.v(TAG, "notifyDataSetChanged"); + } + super.notifyDataSetChanged(); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (DEBUG) { + Log.v(TAG, "get view for " + String.valueOf(position)); + } + + PhoneFavoriteTileView tileView = null; + + if (convertView instanceof PhoneFavoriteTileView) { + tileView = (PhoneFavoriteTileView) convertView; + } + + if (tileView == null) { + tileView = + (PhoneFavoriteTileView) View.inflate(mContext, R.layout.phone_favorite_tile_view, null); + } + tileView.setPhotoManager(mPhotoManager); + tileView.setListener(mListener); + tileView.loadFromContact(getItem(position)); + return tileView; + } + + @Override + public int getViewTypeCount() { + return ViewTypes.COUNT; + } + + @Override + public int getItemViewType(int position) { + return ViewTypes.TILE; + } + + /** + * Temporarily removes a contact from the list for UI refresh. Stores data for this contact in the + * back-up variable. + * + * @param index Position of the contact to be removed. + */ + private void popContactEntry(int index) { + if (isIndexInBound(index)) { + mDraggedEntry = mContactEntries.get(index); + mDraggedEntryIndex = index; + mDragEnteredEntryIndex = index; + markDropArea(mDragEnteredEntryIndex); + } + } + + /** + * @param itemIndex Position of the contact in {@link #mContactEntries}. + * @return True if the given index is valid for {@link #mContactEntries}. + */ + boolean isIndexInBound(int itemIndex) { + return itemIndex >= 0 && itemIndex < mContactEntries.size(); + } + + /** + * Mark the tile as drop area by given the item index in {@link #mContactEntries}. + * + * @param itemIndex Position of the contact in {@link #mContactEntries}. + */ + private void markDropArea(int itemIndex) { + if (mDraggedEntry != null + && isIndexInBound(mDragEnteredEntryIndex) + && isIndexInBound(itemIndex)) { + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + // Remove the old placeholder item and place the new placeholder item. + mContactEntries.remove(mDragEnteredEntryIndex); + mDragEnteredEntryIndex = itemIndex; + mContactEntries.add(mDragEnteredEntryIndex, ContactEntry.BLANK_ENTRY); + ContactEntry.BLANK_ENTRY.id = mDraggedEntry.id; + mDataSetChangedListener.onDataSetChangedForAnimation(); + notifyDataSetChanged(); + } + } + + /** Drops the temporarily removed contact to the desired location in the list. */ + private void handleDrop() { + boolean changed = false; + if (mDraggedEntry != null) { + if (isIndexInBound(mDragEnteredEntryIndex) && mDragEnteredEntryIndex != mDraggedEntryIndex) { + // Don't add the ContactEntry here (to prevent a double animation from occuring). + // When we receive a new cursor the list of contact entries will automatically be + // populated with the dragged ContactEntry at the correct spot. + mDropEntryIndex = mDragEnteredEntryIndex; + mContactEntries.set(mDropEntryIndex, mDraggedEntry); + mDataSetChangedListener.cacheOffsetsForDatasetChange(); + changed = true; + } else if (isIndexInBound(mDraggedEntryIndex)) { + // If {@link #mDragEnteredEntryIndex} is invalid, + // falls back to the original position of the contact. + mContactEntries.remove(mDragEnteredEntryIndex); + mContactEntries.add(mDraggedEntryIndex, mDraggedEntry); + mDropEntryIndex = mDraggedEntryIndex; + notifyDataSetChanged(); + } + + if (changed && mDropEntryIndex < PIN_LIMIT) { + final ArrayList operations = + getReflowedPinningOperations(mContactEntries, mDraggedEntryIndex, mDropEntryIndex); + if (!operations.isEmpty()) { + // update the database here with the new pinned positions + try { + mContext.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations); + } catch (RemoteException | OperationApplicationException e) { + Log.e(TAG, "Exception thrown when pinning contacts", e); + } + } + } + mDraggedEntry = null; + } + } + + /** + * Used when a contact is removed from speeddial. This will both unstar and set pinned position of + * the contact to PinnedPosition.DEMOTED so that it doesn't show up anymore in the favorites list. + */ + private void unstarAndUnpinContact(Uri contactUri) { + final ContentValues values = new ContentValues(2); + values.put(Contacts.STARRED, false); + values.put(Contacts.PINNED, PinnedPositions.DEMOTED); + mContext.getContentResolver().update(contactUri, values, null, null); + } + + /** + * Given a list of contacts that each have pinned positions, rearrange the list (destructive) such + * that all pinned contacts are in their defined pinned positions, and unpinned contacts take the + * spaces between those pinned contacts. Demoted contacts should not appear in the resulting list. + * + *

This method also updates the pinned positions of pinned contacts so that they are all unique + * positive integers within range from 0 to toArrange.size() - 1. This is because when the contact + * entries are read from the database, it is possible for them to have overlapping pin positions + * due to sync or modifications by third party apps. + */ + @VisibleForTesting + private void arrangeContactsByPinnedPosition(ArrayList toArrange) { + final PriorityQueue pinnedQueue = + new PriorityQueue<>(PIN_LIMIT, mContactEntryComparator); + + final List unpinnedContacts = new LinkedList<>(); + + final int length = toArrange.size(); + for (int i = 0; i < length; i++) { + final ContactEntry contact = toArrange.get(i); + // Decide whether the contact is hidden(demoted), pinned, or unpinned + if (contact.pinned > PIN_LIMIT || contact.pinned == PinnedPositions.UNPINNED) { + unpinnedContacts.add(contact); + } else if (contact.pinned > PinnedPositions.DEMOTED) { + // Demoted or contacts with negative pinned positions are ignored. + // Pinned contacts go into a priority queue where they are ranked by pinned + // position. This is required because the contacts provider does not return + // contacts ordered by pinned position. + pinnedQueue.add(contact); + } + } + + final int maxToPin = Math.min(PIN_LIMIT, pinnedQueue.size() + unpinnedContacts.size()); + + toArrange.clear(); + for (int i = 1; i < maxToPin + 1; i++) { + if (!pinnedQueue.isEmpty() && pinnedQueue.peek().pinned <= i) { + final ContactEntry toPin = pinnedQueue.poll(); + toPin.pinned = i; + toArrange.add(toPin); + } else if (!unpinnedContacts.isEmpty()) { + toArrange.add(unpinnedContacts.remove(0)); + } + } + + // If there are still contacts in pinnedContacts at this point, it means that the pinned + // positions of these pinned contacts exceed the actual number of contacts in the list. + // For example, the user had 10 frequents, starred and pinned one of them at the last spot, + // and then cleared frequents. Contacts in this situation should become unpinned. + while (!pinnedQueue.isEmpty()) { + final ContactEntry entry = pinnedQueue.poll(); + entry.pinned = PinnedPositions.UNPINNED; + toArrange.add(entry); + } + + // Any remaining unpinned contacts that weren't in the gaps between the pinned contacts + // now just get appended to the end of the list. + toArrange.addAll(unpinnedContacts); + } + + /** + * Given an existing list of contact entries and a single entry that is to be pinned at a + * particular position, return a list of {@link ContentProviderOperation}s that contains new + * pinned positions for all contacts that are forced to be pinned at new positions, trying as much + * as possible to keep pinned contacts at their original location. + * + *

At this point in time the pinned position of each contact in the list has already been + * updated by {@link #arrangeContactsByPinnedPosition}, so we can assume that all pinned + * positions(within {@link #PIN_LIMIT} are unique positive integers. + */ + @VisibleForTesting + private ArrayList getReflowedPinningOperations( + ArrayList list, int oldPos, int newPinPos) { + final ArrayList positions = new ArrayList<>(); + final int lowerBound = Math.min(oldPos, newPinPos); + final int upperBound = Math.max(oldPos, newPinPos); + for (int i = lowerBound; i <= upperBound; i++) { + final ContactEntry entry = list.get(i); + + // Pinned positions in the database start from 1 instead of being zero-indexed like + // arrays, so offset by 1. + final int databasePinnedPosition = i + 1; + if (entry.pinned == databasePinnedPosition) { + continue; + } + + final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_URI, String.valueOf(entry.id)); + final ContentValues values = new ContentValues(); + values.put(Contacts.PINNED, databasePinnedPosition); + positions.add(ContentProviderOperation.newUpdate(uri).withValues(values).build()); + } + return positions; + } + + @Override + public void onDragStarted(int x, int y, PhoneFavoriteSquareTileView view) { + setInDragging(true); + final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); + popContactEntry(itemIndex); + } + + @Override + public void onDragHovered(int x, int y, PhoneFavoriteSquareTileView view) { + if (view == null) { + // The user is hovering over a view that is not a contact tile, no need to do + // anything here. + return; + } + final int itemIndex = mContactEntries.indexOf(view.getContactEntry()); + if (mInDragging + && mDragEnteredEntryIndex != itemIndex + && isIndexInBound(itemIndex) + && itemIndex < PIN_LIMIT + && itemIndex >= 0) { + markDropArea(itemIndex); + } + } + + @Override + public void onDragFinished(int x, int y) { + setInDragging(false); + // A contact has been dragged to the RemoveView in order to be unstarred, so simply wait + // for the new contact cursor which will cause the UI to be refreshed without the unstarred + // contact. + if (!mAwaitingRemove) { + handleDrop(); + } + } + + @Override + public void onDroppedOnRemove() { + if (mDraggedEntry != null) { + unstarAndUnpinContact(mDraggedEntry.lookupUri); + mAwaitingRemove = true; + } + } + + interface OnDataSetChangedForAnimationListener { + + void onDataSetChangedForAnimation(long... idsInPlace); + + void cacheOffsetsForDatasetChange(); + } + + private static class ViewTypes { + + static final int TILE = 0; + static final int COUNT = 1; + } +} diff --git a/java/com/android/dialer/app/list/RegularSearchFragment.java b/java/com/android/dialer/app/list/RegularSearchFragment.java new file mode 100644 index 000000000..26959539b --- /dev/null +++ b/java/com/android/dialer/app/list/RegularSearchFragment.java @@ -0,0 +1,146 @@ +/* + * 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.dialer.app.list; + +import static android.Manifest.permission.READ_CONTACTS; + +import android.app.Activity; +import android.content.pm.PackageManager; +import android.support.v13.app.FragmentCompat; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.PinnedHeaderListView; +import com.android.dialer.app.R; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.app.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.PhoneNumberCache; +import com.android.dialer.util.PermissionsUtil; + +public class RegularSearchFragment extends SearchFragment + implements OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + public static final int PERMISSION_REQUEST_CODE = 1; + + private static final int SEARCH_DIRECTORY_RESULT_LIMIT = 5; + protected String mPermissionToRequest; + + public RegularSearchFragment() { + configureDirectorySearch(); + } + + public void configureDirectorySearch() { + setDirectorySearchEnabled(true); + setDirectoryResultLimit(SEARCH_DIRECTORY_RESULT_LIMIT); + } + + @Override + protected void onCreateView(LayoutInflater inflater, ViewGroup container) { + super.onCreateView(inflater, container); + ((PinnedHeaderListView) getListView()).setScrollToSectionOnHeaderTouch(true); + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + RegularSearchListAdapter adapter = new RegularSearchListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(usesCallableUri()); + adapter.setListener(this); + return adapter; + } + + @Override + protected void cacheContactInfo(int position) { + CachedNumberLookupService cachedNumberLookupService = + PhoneNumberCache.get(getContext()).getCachedNumberLookupService(); + if (cachedNumberLookupService != null) { + final RegularSearchListAdapter adapter = (RegularSearchListAdapter) getAdapter(); + cachedNumberLookupService.addContact( + getContext(), adapter.getContactInfo(cachedNumberLookupService, position)); + } + } + + @Override + protected void setupEmptyView() { + if (mEmptyView != null && getActivity() != null) { + final int imageResource; + final int actionLabelResource; + final int descriptionResource; + final OnEmptyViewActionButtonClickedListener listener; + if (!PermissionsUtil.hasPermission(getActivity(), READ_CONTACTS)) { + imageResource = R.drawable.empty_contacts; + actionLabelResource = R.string.permission_single_turn_on; + descriptionResource = R.string.permission_no_search; + listener = this; + mPermissionToRequest = READ_CONTACTS; + } else { + imageResource = EmptyContentView.NO_IMAGE; + actionLabelResource = EmptyContentView.NO_LABEL; + descriptionResource = EmptyContentView.NO_LABEL; + listener = null; + mPermissionToRequest = null; + } + + mEmptyView.setImage(imageResource); + mEmptyView.setActionLabel(actionLabelResource); + mEmptyView.setDescription(descriptionResource); + if (listener != null) { + mEmptyView.setActionClickedListener(listener); + } + } + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (READ_CONTACTS.equals(mPermissionToRequest)) { + FragmentCompat.requestPermissions( + this, new String[] {mPermissionToRequest}, PERMISSION_REQUEST_CODE); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == PERMISSION_REQUEST_CODE) { + setupEmptyView(); + if (grantResults != null + && grantResults.length == 1 + && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + PermissionsUtil.notifyPermissionGranted(getActivity(), permissions[0]); + } + } + } + + @Override + protected int getCallInitiationType(boolean isRemoteDirectory) { + return isRemoteDirectory + ? CallInitiationType.Type.REMOTE_DIRECTORY + : CallInitiationType.Type.REGULAR_SEARCH; + } + + public interface CapabilityChecker { + + boolean isNearbyPlacesSearchEnabled(); + } +} diff --git a/java/com/android/dialer/app/list/RegularSearchListAdapter.java b/java/com/android/dialer/app/list/RegularSearchListAdapter.java new file mode 100644 index 000000000..94544d2db --- /dev/null +++ b/java/com/android/dialer/app/list/RegularSearchListAdapter.java @@ -0,0 +1,126 @@ +/* + * 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.dialer.app.list; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.text.TextUtils; +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.DirectoryCompat; +import com.android.contacts.common.list.DirectoryPartition; +import com.android.dialer.phonenumbercache.CachedNumberLookupService; +import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.util.CallUtil; + +/** List adapter to display regular search results. */ +public class RegularSearchListAdapter extends DialerPhoneNumberListAdapter { + + protected boolean mIsQuerySipAddress; + + public RegularSearchListAdapter(Context context) { + super(context); + setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, false); + setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, false); + } + + public CachedContactInfo getContactInfo(CachedNumberLookupService lookupService, int position) { + ContactInfo info = new ContactInfo(); + CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info); + final Cursor item = (Cursor) getItem(position); + if (item != null) { + final DirectoryPartition partition = + (DirectoryPartition) getPartition(getPartitionForPosition(position)); + final long directoryId = partition.getDirectoryId(); + final boolean isExtendedDirectory = isExtendedDirectory(directoryId); + + info.name = item.getString(PhoneQuery.DISPLAY_NAME); + info.type = item.getInt(PhoneQuery.PHONE_TYPE); + info.label = item.getString(PhoneQuery.PHONE_LABEL); + info.number = item.getString(PhoneQuery.PHONE_NUMBER); + final String photoUriStr = item.getString(PhoneQuery.PHOTO_URI); + info.photoUri = photoUriStr == null ? null : Uri.parse(photoUriStr); + /* + * An extended directory is custom directory in the app, but not a directory provided by + * framework. So it can't be USER_TYPE_WORK. + * + * When a search result is selected, RegularSearchFragment calls getContactInfo and + * cache the resulting @{link ContactInfo} into local db. Set usertype to USER_TYPE_WORK + * only if it's NOT extended directory id and is enterprise directory. + */ + info.userType = + !isExtendedDirectory && DirectoryCompat.isEnterpriseDirectoryId(directoryId) + ? ContactsUtils.USER_TYPE_WORK + : ContactsUtils.USER_TYPE_CURRENT; + + cacheInfo.setLookupKey(item.getString(PhoneQuery.LOOKUP_KEY)); + + final String sourceName = partition.getLabel(); + if (isExtendedDirectory) { + cacheInfo.setExtendedSource(sourceName, directoryId); + } else { + cacheInfo.setDirectorySource(sourceName, directoryId); + } + } + return cacheInfo; + } + + @Override + public String getFormattedQueryString() { + if (mIsQuerySipAddress) { + // Return unnormalized SIP address + return getQueryString(); + } + return super.getFormattedQueryString(); + } + + @Override + public void setQueryString(String queryString) { + // Don't show actions if the query string contains a letter. + final boolean showNumberShortcuts = + !TextUtils.isEmpty(getFormattedQueryString()) && hasDigitsInQueryString(); + mIsQuerySipAddress = PhoneNumberHelper.isUriNumber(queryString); + + if (isChanged(showNumberShortcuts)) { + notifyDataSetChanged(); + } + super.setQueryString(queryString); + } + + protected boolean isChanged(boolean showNumberShortcuts) { + boolean changed = false; + changed |= setShortcutEnabled(SHORTCUT_DIRECT_CALL, showNumberShortcuts || mIsQuerySipAddress); + changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts); + changed |= + setShortcutEnabled( + SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext())); + return changed; + } + + /** Whether there is at least one digit in the query string. */ + private boolean hasDigitsInQueryString() { + String queryString = getQueryString(); + int length = queryString.length(); + for (int i = 0; i < length; i++) { + if (Character.isDigit(queryString.charAt(i))) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/list/RemoveView.java b/java/com/android/dialer/app/list/RemoveView.java new file mode 100644 index 000000000..3b917db43 --- /dev/null +++ b/java/com/android/dialer/app/list/RemoveView.java @@ -0,0 +1,105 @@ +/* + * 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.dialer.app.list; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.DragEvent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; +import com.android.dialer.app.R; + +public class RemoveView extends FrameLayout { + + DragDropController mDragDropController; + TextView mRemoveText; + ImageView mRemoveIcon; + int mUnhighlightedColor; + int mHighlightedColor; + Drawable mRemoveDrawable; + + public RemoveView(Context context) { + super(context); + } + + public RemoveView(Context context, AttributeSet attrs) { + this(context, attrs, -1); + } + + public RemoveView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onFinishInflate() { + mRemoveText = (TextView) findViewById(R.id.remove_view_text); + mRemoveIcon = (ImageView) findViewById(R.id.remove_view_icon); + final Resources r = getResources(); + mUnhighlightedColor = r.getColor(R.color.remove_text_color); + mHighlightedColor = r.getColor(R.color.remove_highlighted_text_color); + mRemoveDrawable = r.getDrawable(R.drawable.ic_remove); + } + + public void setDragDropController(DragDropController controller) { + mDragDropController = controller; + } + + @Override + public boolean onDragEvent(DragEvent event) { + final int action = event.getAction(); + switch (action) { + case DragEvent.ACTION_DRAG_ENTERED: + // TODO: This is temporary solution and should be removed once accessibility for + // drag and drop is supported by framework(b/26871588). + sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT); + setAppearanceHighlighted(); + break; + case DragEvent.ACTION_DRAG_EXITED: + setAppearanceNormal(); + break; + case DragEvent.ACTION_DRAG_LOCATION: + if (mDragDropController != null) { + mDragDropController.handleDragHovered(this, (int) event.getX(), (int) event.getY()); + } + break; + case DragEvent.ACTION_DROP: + sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT); + if (mDragDropController != null) { + mDragDropController.handleDragFinished((int) event.getX(), (int) event.getY(), true); + } + setAppearanceNormal(); + break; + } + return true; + } + + private void setAppearanceNormal() { + mRemoveText.setTextColor(mUnhighlightedColor); + mRemoveIcon.setColorFilter(mUnhighlightedColor); + invalidate(); + } + + private void setAppearanceHighlighted() { + mRemoveText.setTextColor(mHighlightedColor); + mRemoveIcon.setColorFilter(mHighlightedColor); + invalidate(); + } +} diff --git a/java/com/android/dialer/app/list/SearchFragment.java b/java/com/android/dialer/app/list/SearchFragment.java new file mode 100644 index 000000000..4a7d48ae4 --- /dev/null +++ b/java/com/android/dialer/app/list/SearchFragment.java @@ -0,0 +1,425 @@ +/* + * 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.dialer.app.list; + +import android.animation.Animator; +import android.animation.AnimatorInflater; +import android.animation.AnimatorListenerAdapter; +import android.app.Activity; +import android.app.DialogFragment; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.Space; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.contacts.common.list.ContactListItemView; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.contacts.common.list.PhoneNumberPickerFragment; +import com.android.contacts.common.util.FabUtil; +import com.android.dialer.animation.AnimUtils; +import com.android.dialer.app.R; +import com.android.dialer.app.dialpad.DialpadFragment.ErrorDialogFragment; +import com.android.dialer.app.widget.DialpadSearchEmptyContentView; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.IntentUtil; +import com.android.dialer.util.PermissionsUtil; + +public class SearchFragment extends PhoneNumberPickerFragment { + + protected EmptyContentView mEmptyView; + private OnListFragmentScrolledListener mActivityScrollListener; + private View.OnTouchListener mActivityOnTouchListener; + /* + * Stores the untouched user-entered string that is used to populate the add to contacts + * intent. + */ + private String mAddToContactNumber; + private int mActionBarHeight; + private int mShadowHeight; + private int mPaddingTop; + private int mShowDialpadDuration; + private int mHideDialpadDuration; + /** + * Used to resize the list view containing search results so that it fits the available space + * above the dialpad. Does not have a user-visible effect in regular touch usage (since the + * dialpad hides that portion of the ListView anyway), but improves usability in accessibility + * mode. + */ + private Space mSpacer; + + private HostInterface mActivity; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + setQuickContactEnabled(true); + setAdjustSelectionBoundsEnabled(false); + setDarkTheme(false); + setPhotoPosition(ContactListItemView.getDefaultPhotoPosition(false /* opposite */)); + setUseCallableUri(true); + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + LogUtil.v( + "SearchFragment.onAttach", + activity.toString() + + " doesn't implement OnListFragmentScrolledListener. " + + "Ignoring."); + } + } + + @Override + public void onStart() { + super.onStart(); + if (isSearchMode()) { + getAdapter().setHasHeader(0, false); + } + + mActivity = (HostInterface) getActivity(); + + final Resources res = getResources(); + mActionBarHeight = mActivity.getActionBarHeight(); + mShadowHeight = res.getDrawable(R.drawable.search_shadow).getIntrinsicHeight(); + mPaddingTop = res.getDimensionPixelSize(R.dimen.search_list_padding_top); + mShowDialpadDuration = res.getInteger(R.integer.dialpad_slide_in_duration); + mHideDialpadDuration = res.getInteger(R.integer.dialpad_slide_out_duration); + + final ListView listView = getListView(); + + if (mEmptyView == null) { + if (this instanceof SmartDialSearchFragment) { + mEmptyView = new DialpadSearchEmptyContentView(getActivity()); + } else { + mEmptyView = new EmptyContentView(getActivity()); + } + ((ViewGroup) getListView().getParent()).addView(mEmptyView); + getListView().setEmptyView(mEmptyView); + setupEmptyView(); + } + + listView.setBackgroundColor(res.getColor(R.color.background_dialer_results)); + listView.setClipToPadding(false); + setVisibleScrollbarEnabled(false); + + //Turn of accessibility live region as the list constantly update itself and spam messages. + listView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); + ContentChangedFilter.addToParent(listView); + + listView.setOnScrollListener( + new OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (mActivityScrollListener != null) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); + } + } + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} + }); + if (mActivityOnTouchListener != null) { + listView.setOnTouchListener(mActivityOnTouchListener); + } + + updatePosition(false /* animate */); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + FabUtil.addBottomPaddingToListViewForFab(getListView(), getResources()); + } + + @Override + public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) { + Animator animator = null; + if (nextAnim != 0) { + animator = AnimatorInflater.loadAnimator(getActivity(), nextAnim); + } + if (animator != null) { + final View view = getView(); + final int oldLayerType = view.getLayerType(); + animator.addListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setLayerType(oldLayerType, null); + } + }); + } + return animator; + } + + @Override + protected void setSearchMode(boolean flag) { + super.setSearchMode(flag); + // This hides the "All contacts with phone numbers" header in the search fragment + final ContactEntryListAdapter adapter = getAdapter(); + if (adapter != null) { + adapter.setHasHeader(0, false); + } + } + + public void setAddToContactNumber(String addToContactNumber) { + mAddToContactNumber = addToContactNumber; + } + + /** + * Return true if phone number is prohibited by a value - + * (R.string.config_prohibited_phone_number_regexp) in the config files. False otherwise. + */ + public boolean checkForProhibitedPhoneNumber(String number) { + // Regular expression prohibiting manual phone call. Can be empty i.e. "no rule". + String prohibitedPhoneNumberRegexp = + getResources().getString(R.string.config_prohibited_phone_number_regexp); + + // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated + // test equipment. + if (number != null + && !TextUtils.isEmpty(prohibitedPhoneNumberRegexp) + && number.matches(prohibitedPhoneNumberRegexp)) { + LogUtil.i( + "SearchFragment.checkForProhibitedPhoneNumber", + "the phone number is prohibited explicitly by a rule"); + if (getActivity() != null) { + DialogFragment dialogFragment = + ErrorDialogFragment.newInstance(R.string.dialog_phone_call_prohibited_message); + dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog"); + } + + return true; + } + return false; + } + + @Override + protected ContactEntryListAdapter createListAdapter() { + DialerPhoneNumberListAdapter adapter = new DialerPhoneNumberListAdapter(getActivity()); + adapter.setDisplayPhotos(true); + adapter.setUseCallableUri(super.usesCallableUri()); + adapter.setListener(this); + return adapter; + } + + @Override + protected void onItemClick(int position, long id) { + final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); + final int shortcutType = adapter.getShortcutTypeFromPosition(position); + final OnPhoneNumberPickerActionListener listener; + final Intent intent; + final String number; + + LogUtil.i("SearchFragment.onItemClick", "shortcutType: " + shortcutType); + + switch (shortcutType) { + case DialerPhoneNumberListAdapter.SHORTCUT_INVALID: + super.onItemClick(position, id); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_DIRECT_CALL: + number = adapter.getQueryString(); + listener = getOnPhoneNumberPickerListener(); + if (listener != null && !checkForProhibitedPhoneNumber(number)) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = + getCallInitiationType(false /* isRemoteDirectory */); + callSpecificAppData.positionOfSelectedSearchResult = position; + callSpecificAppData.charactersInSearchString = + getQueryString() == null ? 0 : getQueryString().length(); + listener.onPickPhoneNumber(number, false /* isVideoCall */, callSpecificAppData); + } + break; + case DialerPhoneNumberListAdapter.SHORTCUT_CREATE_NEW_CONTACT: + number = + TextUtils.isEmpty(mAddToContactNumber) + ? adapter.getFormattedQueryString() + : mAddToContactNumber; + intent = IntentUtil.getNewContactIntent(number); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_ADD_TO_EXISTING_CONTACT: + number = + TextUtils.isEmpty(mAddToContactNumber) + ? adapter.getFormattedQueryString() + : mAddToContactNumber; + intent = IntentUtil.getAddToExistingContactIntent(number); + DialerUtils.startActivityWithErrorToast( + getActivity(), intent, R.string.add_contact_not_available); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_SEND_SMS_MESSAGE: + number = adapter.getFormattedQueryString(); + intent = IntentUtil.getSendSmsIntent(number); + DialerUtils.startActivityWithErrorToast(getActivity(), intent); + break; + case DialerPhoneNumberListAdapter.SHORTCUT_MAKE_VIDEO_CALL: + number = + TextUtils.isEmpty(mAddToContactNumber) ? adapter.getQueryString() : mAddToContactNumber; + listener = getOnPhoneNumberPickerListener(); + if (listener != null && !checkForProhibitedPhoneNumber(number)) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = + getCallInitiationType(false /* isRemoteDirectory */); + callSpecificAppData.positionOfSelectedSearchResult = position; + callSpecificAppData.charactersInSearchString = + getQueryString() == null ? 0 : getQueryString().length(); + listener.onPickPhoneNumber(number, true /* isVideoCall */, callSpecificAppData); + } + break; + } + } + + /** + * Updates the position and padding of the search fragment, depending on whether the dialpad is + * shown. This can be optionally animated. + */ + public void updatePosition(boolean animate) { + if (mActivity == null) { + // Activity will be set in onStart, and this method will be called again + return; + } + + // Use negative shadow height instead of 0 to account for the 9-patch's shadow. + int startTranslationValue = + mActivity.isDialpadShown() ? mActionBarHeight - mShadowHeight : -mShadowHeight; + int endTranslationValue = 0; + // Prevents ListView from being translated down after a rotation when the ActionBar is up. + if (animate || mActivity.isActionBarShowing()) { + endTranslationValue = mActivity.isDialpadShown() ? 0 : mActionBarHeight - mShadowHeight; + } + if (animate) { + // If the dialpad will be shown, then this animation involves sliding the list up. + final boolean slideUp = mActivity.isDialpadShown(); + + Interpolator interpolator = slideUp ? AnimUtils.EASE_IN : AnimUtils.EASE_OUT; + int duration = slideUp ? mShowDialpadDuration : mHideDialpadDuration; + getView().setTranslationY(startTranslationValue); + getView() + .animate() + .translationY(endTranslationValue) + .setInterpolator(interpolator) + .setDuration(duration) + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + if (!slideUp) { + resizeListView(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (slideUp) { + resizeListView(); + } + } + }); + + } else { + getView().setTranslationY(endTranslationValue); + resizeListView(); + } + + // There is padding which should only be applied when the dialpad is not shown. + int paddingTop = mActivity.isDialpadShown() ? 0 : mPaddingTop; + final ListView listView = getListView(); + listView.setPaddingRelative( + listView.getPaddingStart(), + paddingTop, + listView.getPaddingEnd(), + listView.getPaddingBottom()); + } + + public void resizeListView() { + if (mSpacer == null) { + return; + } + int spacerHeight = mActivity.isDialpadShown() ? mActivity.getDialpadHeight() : 0; + if (spacerHeight != mSpacer.getHeight()) { + final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpacer.getLayoutParams(); + lp.height = spacerHeight; + mSpacer.setLayoutParams(lp); + } + } + + @Override + protected void startLoading() { + if (getActivity() == null) { + return; + } + + if (PermissionsUtil.hasContactsPermissions(getActivity())) { + super.startLoading(); + } else if (TextUtils.isEmpty(getQueryString())) { + // Clear out any existing call shortcuts. + final DialerPhoneNumberListAdapter adapter = (DialerPhoneNumberListAdapter) getAdapter(); + adapter.disableAllShortcuts(); + } else { + // The contact list is not going to change (we have no results since permissions are + // denied), but the shortcuts might because of the different query, so update the + // list. + getAdapter().notifyDataSetChanged(); + } + + setupEmptyView(); + } + + public void setOnTouchListener(View.OnTouchListener onTouchListener) { + mActivityOnTouchListener = onTouchListener; + } + + @Override + protected View inflateView(LayoutInflater inflater, ViewGroup container) { + final LinearLayout parent = (LinearLayout) super.inflateView(inflater, container); + final int orientation = getResources().getConfiguration().orientation; + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + mSpacer = new Space(getActivity()); + parent.addView( + mSpacer, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0)); + } + return parent; + } + + protected void setupEmptyView() {} + + public interface HostInterface { + + boolean isActionBarShowing(); + + boolean isDialpadShown(); + + int getDialpadHeight(); + + int getActionBarHideOffset(); + + int getActionBarHeight(); + } +} diff --git a/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java new file mode 100644 index 000000000..566a15d53 --- /dev/null +++ b/java/com/android/dialer/app/list/SmartDialNumberListAdapter.java @@ -0,0 +1,117 @@ +/* + * 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.dialer.app.list; + +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; +import android.util.Log; +import com.android.contacts.common.list.ContactListItemView; +import com.android.dialer.app.dialpad.SmartDialCursorLoader; +import com.android.dialer.smartdial.SmartDialMatchPosition; +import com.android.dialer.smartdial.SmartDialNameMatcher; +import com.android.dialer.smartdial.SmartDialPrefix; +import com.android.dialer.util.CallUtil; +import java.util.ArrayList; + +/** List adapter to display the SmartDial search results. */ +public class SmartDialNumberListAdapter extends DialerPhoneNumberListAdapter { + + private static final String TAG = SmartDialNumberListAdapter.class.getSimpleName(); + private static final boolean DEBUG = false; + + @NonNull private final SmartDialNameMatcher mNameMatcher; + + public SmartDialNumberListAdapter(Context context) { + super(context); + mNameMatcher = new SmartDialNameMatcher("", SmartDialPrefix.getMap()); + setShortcutEnabled(SmartDialNumberListAdapter.SHORTCUT_DIRECT_CALL, false); + + if (DEBUG) { + Log.v(TAG, "Constructing List Adapter"); + } + } + + /** Sets query for the SmartDialCursorLoader. */ + public void configureLoader(SmartDialCursorLoader loader) { + if (DEBUG) { + Log.v(TAG, "Configure Loader with query" + getQueryString()); + } + + if (getQueryString() == null) { + loader.configureQuery(""); + mNameMatcher.setQuery(""); + } else { + loader.configureQuery(getQueryString()); + mNameMatcher.setQuery(PhoneNumberUtils.normalizeNumber(getQueryString())); + } + } + + /** + * Sets highlight options for a List item in the SmartDial search results. + * + * @param view ContactListItemView where the result will be displayed. + * @param cursor Object containing information of the associated List item. + */ + @Override + protected void setHighlight(ContactListItemView view, Cursor cursor) { + view.clearHighlightSequences(); + + if (mNameMatcher.matches(cursor.getString(PhoneQuery.DISPLAY_NAME))) { + final ArrayList nameMatches = mNameMatcher.getMatchPositions(); + for (SmartDialMatchPosition match : nameMatches) { + view.addNameHighlightSequence(match.start, match.end); + if (DEBUG) { + Log.v( + TAG, + cursor.getString(PhoneQuery.DISPLAY_NAME) + + " " + + mNameMatcher.getQuery() + + " " + + String.valueOf(match.start)); + } + } + } + + final SmartDialMatchPosition numberMatch = + mNameMatcher.matchesNumber(cursor.getString(PhoneQuery.PHONE_NUMBER)); + if (numberMatch != null) { + view.addNumberHighlightSequence(numberMatch.start, numberMatch.end); + } + } + + @Override + public void setQueryString(String queryString) { + final boolean showNumberShortcuts = !TextUtils.isEmpty(getFormattedQueryString()); + boolean changed = false; + changed |= setShortcutEnabled(SHORTCUT_CREATE_NEW_CONTACT, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_ADD_TO_EXISTING_CONTACT, showNumberShortcuts); + changed |= setShortcutEnabled(SHORTCUT_SEND_SMS_MESSAGE, showNumberShortcuts); + changed |= + setShortcutEnabled( + SHORTCUT_MAKE_VIDEO_CALL, showNumberShortcuts && CallUtil.isVideoEnabled(getContext())); + if (changed) { + notifyDataSetChanged(); + } + super.setQueryString(queryString); + } + + public void setShowEmptyListForNullQuery(boolean show) { + mNameMatcher.setShouldMatchEmptyQuery(!show); + } +} diff --git a/java/com/android/dialer/app/list/SmartDialSearchFragment.java b/java/com/android/dialer/app/list/SmartDialSearchFragment.java new file mode 100644 index 000000000..c783d3ac3 --- /dev/null +++ b/java/com/android/dialer/app/list/SmartDialSearchFragment.java @@ -0,0 +1,120 @@ +/* + * 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.dialer.app.list; + +import static android.Manifest.permission.CALL_PHONE; + +import android.app.Activity; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v13.app.FragmentCompat; +import com.android.contacts.common.list.ContactEntryListAdapter; +import com.android.dialer.app.R; +import com.android.dialer.app.dialpad.SmartDialCursorLoader; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.util.PermissionsUtil; + +/** Implements a fragment to load and display SmartDial search results. */ +public class SmartDialSearchFragment extends SearchFragment + implements EmptyContentView.OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + private static final String TAG = SmartDialSearchFragment.class.getSimpleName(); + + private static final int CALL_PHONE_PERMISSION_REQUEST_CODE = 1; + + /** Creates a SmartDialListAdapter to display and operate on search results. */ + @Override + protected ContactEntryListAdapter createListAdapter() { + SmartDialNumberListAdapter adapter = new SmartDialNumberListAdapter(getActivity()); + adapter.setUseCallableUri(super.usesCallableUri()); + adapter.setQuickContactEnabled(true); + adapter.setShowEmptyListForNullQuery(getShowEmptyListForNullQuery()); + // Set adapter's query string to restore previous instance state. + adapter.setQueryString(getQueryString()); + adapter.setListener(this); + return adapter; + } + + /** Creates a SmartDialCursorLoader object to load query results. */ + @Override + public Loader onCreateLoader(int id, Bundle args) { + // Smart dialing does not support Directory Load, falls back to normal search instead. + if (id == getDirectoryLoaderId()) { + return super.onCreateLoader(id, args); + } else { + final SmartDialNumberListAdapter adapter = (SmartDialNumberListAdapter) getAdapter(); + SmartDialCursorLoader loader = new SmartDialCursorLoader(super.getContext()); + loader.setShowEmptyListForNullQuery(getShowEmptyListForNullQuery()); + adapter.configureLoader(loader); + return loader; + } + } + + @Override + protected void setupEmptyView() { + if (mEmptyView != null && getActivity() != null) { + if (!PermissionsUtil.hasPermission(getActivity(), CALL_PHONE)) { + mEmptyView.setImage(R.drawable.empty_contacts); + mEmptyView.setActionLabel(R.string.permission_single_turn_on); + mEmptyView.setDescription(R.string.permission_place_call); + mEmptyView.setActionClickedListener(this); + } else { + mEmptyView.setImage(EmptyContentView.NO_IMAGE); + mEmptyView.setActionLabel(EmptyContentView.NO_LABEL); + mEmptyView.setDescription(EmptyContentView.NO_LABEL); + } + } + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + FragmentCompat.requestPermissions( + this, new String[] {CALL_PHONE}, CALL_PHONE_PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == CALL_PHONE_PERMISSION_REQUEST_CODE) { + setupEmptyView(); + } + } + + @Override + protected int getCallInitiationType(boolean isRemoteDirectory) { + return CallInitiationType.Type.SMART_DIAL; + } + + public boolean isShowingPermissionRequest() { + return mEmptyView != null && mEmptyView.isShowingContent(); + } + + @Override + public void setShowEmptyListForNullQuery(boolean show) { + if (getAdapter() != null) { + ((SmartDialNumberListAdapter) getAdapter()).setShowEmptyListForNullQuery(show); + } + super.setShowEmptyListForNullQuery(show); + } +} diff --git a/java/com/android/dialer/app/list/SpeedDialFragment.java b/java/com/android/dialer/app/list/SpeedDialFragment.java new file mode 100644 index 000000000..8e0f89028 --- /dev/null +++ b/java/com/android/dialer/app/list/SpeedDialFragment.java @@ -0,0 +1,512 @@ +/* + * 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.dialer.app.list; + +import static android.Manifest.permission.READ_CONTACTS; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.app.Activity; +import android.app.Fragment; +import android.app.LoaderManager; +import android.content.CursorLoader; +import android.content.Loader; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.Rect; +import android.net.Uri; +import android.os.Bundle; +import android.os.Trace; +import android.support.annotation.Nullable; +import android.support.v13.app.FragmentCompat; +import android.support.v4.util.LongSparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.view.animation.LayoutAnimationController; +import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.FrameLayout; +import android.widget.FrameLayout.LayoutParams; +import android.widget.ImageView; +import android.widget.ListView; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactTileLoaderFactory; +import com.android.contacts.common.list.ContactTileView; +import com.android.contacts.common.list.OnPhoneNumberPickerActionListener; +import com.android.dialer.app.R; +import com.android.dialer.app.list.ListsFragment.ListsPage; +import com.android.dialer.app.widget.EmptyContentView; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.callintent.nano.CallSpecificAppData; +import com.android.dialer.common.LogUtil; +import com.android.dialer.util.PermissionsUtil; +import com.android.dialer.util.ViewUtil; +import java.util.ArrayList; + +/** This fragment displays the user's favorite/frequent contacts in a grid. */ +public class SpeedDialFragment extends Fragment + implements ListsPage, + OnItemClickListener, + PhoneFavoritesTileAdapter.OnDataSetChangedForAnimationListener, + EmptyContentView.OnEmptyViewActionButtonClickedListener, + FragmentCompat.OnRequestPermissionsResultCallback { + + private static final int READ_CONTACTS_PERMISSION_REQUEST_CODE = 1; + + /** + * By default, the animation code assumes that all items in a list view are of the same height + * when animating new list items into view (e.g. from the bottom of the screen into view). This + * can cause incorrect translation offsets when a item that is larger or smaller than other list + * item is removed from the list. This key is used to provide the actual height of the removed + * object so that the actual translation appears correct to the user. + */ + private static final long KEY_REMOVED_ITEM_HEIGHT = Long.MAX_VALUE; + + private static final String TAG = "SpeedDialFragment"; + private static final boolean DEBUG = false; + /** Used with LoaderManager. */ + private static final int LOADER_ID_CONTACT_TILE = 1; + + private final LongSparseArray mItemIdTopMap = new LongSparseArray<>(); + private final LongSparseArray mItemIdLeftMap = new LongSparseArray<>(); + private final ContactTileView.Listener mContactTileAdapterListener = + new ContactTileAdapterListener(); + private final LoaderManager.LoaderCallbacks mContactTileLoaderListener = + new ContactTileLoaderListener(); + private final ScrollListener mScrollListener = new ScrollListener(); + private int mAnimationDuration; + private OnPhoneNumberPickerActionListener mPhoneNumberPickerActionListener; + private OnListFragmentScrolledListener mActivityScrollListener; + private PhoneFavoritesTileAdapter mContactTileAdapter; + private View mParentView; + private PhoneFavoriteListView mListView; + private View mContactTileFrame; + /** Layout used when there are no favorites. */ + private EmptyContentView mEmptyView; + + @Override + public void onCreate(Bundle savedState) { + if (DEBUG) { + LogUtil.d("SpeedDialFragment.onCreate", null); + } + Trace.beginSection(TAG + " onCreate"); + super.onCreate(savedState); + + // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter. + // We don't construct the resultant adapter at this moment since it requires LayoutInflater + // that will be available on onCreateView(). + mContactTileAdapter = + new PhoneFavoritesTileAdapter(getActivity(), mContactTileAdapterListener, this); + mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(getActivity())); + mAnimationDuration = getResources().getInteger(R.integer.fade_duration); + Trace.endSection(); + } + + @Override + public void onResume() { + Trace.beginSection(TAG + " onResume"); + super.onResume(); + if (mContactTileAdapter != null) { + mContactTileAdapter.refreshContactsPreferences(); + } + if (PermissionsUtil.hasContactsPermissions(getActivity())) { + if (getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE) == null) { + getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); + + } else { + getLoaderManager().getLoader(LOADER_ID_CONTACT_TILE).forceLoad(); + } + + mEmptyView.setDescription(R.string.speed_dial_empty); + mEmptyView.setActionLabel(R.string.speed_dial_empty_add_favorite_action); + } else { + mEmptyView.setDescription(R.string.permission_no_speeddial); + mEmptyView.setActionLabel(R.string.permission_single_turn_on); + } + Trace.endSection(); + } + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + Trace.beginSection(TAG + " onCreateView"); + mParentView = inflater.inflate(R.layout.speed_dial_fragment, container, false); + + mListView = (PhoneFavoriteListView) mParentView.findViewById(R.id.contact_tile_list); + mListView.setOnItemClickListener(this); + mListView.setVerticalScrollBarEnabled(false); + mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT); + mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); + mListView.getDragDropController().addOnDragDropListener(mContactTileAdapter); + + final ImageView dragShadowOverlay = + (ImageView) getActivity().findViewById(R.id.contact_tile_drag_shadow_overlay); + mListView.setDragShadowOverlay(dragShadowOverlay); + + mEmptyView = (EmptyContentView) mParentView.findViewById(R.id.empty_list_view); + mEmptyView.setImage(R.drawable.empty_speed_dial); + mEmptyView.setActionClickedListener(this); + + mContactTileFrame = mParentView.findViewById(R.id.contact_tile_frame); + + final LayoutAnimationController controller = + new LayoutAnimationController( + AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in)); + controller.setDelay(0); + mListView.setLayoutAnimation(controller); + mListView.setAdapter(mContactTileAdapter); + + mListView.setOnScrollListener(mScrollListener); + mListView.setFastScrollEnabled(false); + mListView.setFastScrollAlwaysVisible(false); + + //prevent content changes of the list from firing accessibility events. + mListView.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_NONE); + ContentChangedFilter.addToParent(mListView); + + Trace.endSection(); + return mParentView; + } + + public boolean hasFrequents() { + if (mContactTileAdapter == null) { + return false; + } + return mContactTileAdapter.getNumFrequents() > 0; + } + + /* package */ void setEmptyViewVisibility(final boolean visible) { + final int previousVisibility = mEmptyView.getVisibility(); + final int emptyViewVisibility = visible ? View.VISIBLE : View.GONE; + final int listViewVisibility = visible ? View.GONE : View.VISIBLE; + + if (previousVisibility != emptyViewVisibility) { + final FrameLayout.LayoutParams params = (LayoutParams) mContactTileFrame.getLayoutParams(); + params.height = visible ? LayoutParams.WRAP_CONTENT : LayoutParams.MATCH_PARENT; + mContactTileFrame.setLayoutParams(params); + mEmptyView.setVisibility(emptyViewVisibility); + mListView.setVisibility(listViewVisibility); + } + } + + @Override + public void onStart() { + super.onStart(); + + final Activity activity = getActivity(); + + try { + mActivityScrollListener = (OnListFragmentScrolledListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException( + activity.toString() + " must implement OnListFragmentScrolledListener"); + } + + try { + OnDragDropListener listener = (OnDragDropListener) activity; + mListView.getDragDropController().addOnDragDropListener(listener); + ((HostInterface) activity).setDragDropController(mListView.getDragDropController()); + } catch (ClassCastException e) { + throw new ClassCastException( + activity.toString() + " must implement OnDragDropListener and HostInterface"); + } + + try { + mPhoneNumberPickerActionListener = (OnPhoneNumberPickerActionListener) activity; + } catch (ClassCastException e) { + throw new ClassCastException( + activity.toString() + " must implement PhoneFavoritesFragment.listener"); + } + + // Use initLoader() instead of restartLoader() to refraining unnecessary reload. + // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will + // be called, on which we'll check if "all" contacts should be reloaded again or not. + if (PermissionsUtil.hasContactsPermissions(activity)) { + getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener); + } else { + setEmptyViewVisibility(true); + } + } + + /** + * {@inheritDoc} + * + *

This is only effective for elements provided by {@link #mContactTileAdapter}. {@link + * #mContactTileAdapter} has its own logic for click events. + */ + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final int contactTileAdapterCount = mContactTileAdapter.getCount(); + if (position <= contactTileAdapterCount) { + LogUtil.e( + "SpeedDialFragment.onItemClick", + "event for unexpected position. The position " + + position + + " is before \"all\" section. Ignored."); + } + } + + /** + * Cache the current view offsets into memory. Once a relayout of views in the ListView has + * happened due to a dataset change, the cached offsets are used to create animations that slide + * views from their previous positions to their new ones, to give the appearance that the views + * are sliding into their new positions. + */ + private void saveOffsets(int removedItemHeight) { + final int firstVisiblePosition = mListView.getFirstVisiblePosition(); + if (DEBUG) { + LogUtil.d("SpeedDialFragment.saveOffsets", "Child count : " + mListView.getChildCount()); + } + for (int i = 0; i < mListView.getChildCount(); i++) { + final View child = mListView.getChildAt(i); + final int position = firstVisiblePosition + i; + // Since we are getting the position from mListView and then querying + // mContactTileAdapter, its very possible that things are out of sync + // and we might index out of bounds. Let's make sure that this doesn't happen. + if (!mContactTileAdapter.isIndexInBound(position)) { + continue; + } + final long itemId = mContactTileAdapter.getItemId(position); + if (DEBUG) { + LogUtil.d( + "SpeedDialFragment.saveOffsets", + "Saving itemId: " + itemId + " for listview child " + i + " Top: " + child.getTop()); + } + mItemIdTopMap.put(itemId, child.getTop()); + mItemIdLeftMap.put(itemId, child.getLeft()); + } + mItemIdTopMap.put(KEY_REMOVED_ITEM_HEIGHT, removedItemHeight); + } + + /* + * Performs animations for the gridView + */ + private void animateGridView(final long... idsInPlace) { + if (mItemIdTopMap.size() == 0) { + // Don't do animations if the database is being queried for the first time and + // the previous item offsets have not been cached, or the user hasn't done anything + // (dragging, swiping etc) that requires an animation. + return; + } + + ViewUtil.doOnPreDraw( + mListView, + true, + new Runnable() { + @Override + public void run() { + + final int firstVisiblePosition = mListView.getFirstVisiblePosition(); + final AnimatorSet animSet = new AnimatorSet(); + final ArrayList animators = new ArrayList(); + for (int i = 0; i < mListView.getChildCount(); i++) { + final View child = mListView.getChildAt(i); + int position = firstVisiblePosition + i; + + // Since we are getting the position from mListView and then querying + // mContactTileAdapter, its very possible that things are out of sync + // and we might index out of bounds. Let's make sure that this doesn't happen. + if (!mContactTileAdapter.isIndexInBound(position)) { + continue; + } + + final long itemId = mContactTileAdapter.getItemId(position); + + if (containsId(idsInPlace, itemId)) { + animators.add(ObjectAnimator.ofFloat(child, "alpha", 0.0f, 1.0f)); + break; + } else { + Integer startTop = mItemIdTopMap.get(itemId); + Integer startLeft = mItemIdLeftMap.get(itemId); + final int top = child.getTop(); + final int left = child.getLeft(); + int deltaX = 0; + int deltaY = 0; + + if (startLeft != null) { + if (startLeft != left) { + deltaX = startLeft - left; + animators.add(ObjectAnimator.ofFloat(child, "translationX", deltaX, 0.0f)); + } + } + + if (startTop != null) { + if (startTop != top) { + deltaY = startTop - top; + animators.add(ObjectAnimator.ofFloat(child, "translationY", deltaY, 0.0f)); + } + } + + if (DEBUG) { + LogUtil.d( + "SpeedDialFragment.onPreDraw", + "Found itemId: " + + itemId + + " for listview child " + + i + + " Top: " + + top + + " Delta: " + + deltaY); + } + } + } + + if (animators.size() > 0) { + animSet.setDuration(mAnimationDuration).playTogether(animators); + animSet.start(); + } + + mItemIdTopMap.clear(); + mItemIdLeftMap.clear(); + } + }); + } + + private boolean containsId(long[] ids, long target) { + // Linear search on array is fine because this is typically only 0-1 elements long + for (int i = 0; i < ids.length; i++) { + if (ids[i] == target) { + return true; + } + } + return false; + } + + @Override + public void onDataSetChangedForAnimation(long... idsInPlace) { + animateGridView(idsInPlace); + } + + @Override + public void cacheOffsetsForDatasetChange() { + saveOffsets(0); + } + + @Override + public void onEmptyViewActionButtonClicked() { + final Activity activity = getActivity(); + if (activity == null) { + return; + } + + if (!PermissionsUtil.hasPermission(activity, READ_CONTACTS)) { + FragmentCompat.requestPermissions( + this, new String[] {READ_CONTACTS}, READ_CONTACTS_PERMISSION_REQUEST_CODE); + } else { + // Switch tabs + ((HostInterface) activity).showAllContactsTab(); + } + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + if (requestCode == READ_CONTACTS_PERMISSION_REQUEST_CODE) { + if (grantResults.length == 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { + PermissionsUtil.notifyPermissionGranted(getActivity(), READ_CONTACTS); + } + } + } + + @Override + public void onPageResume(@Nullable Activity activity) { + LogUtil.i("SpeedDialFragment.onPageResume", null); + } + + @Override + public void onPagePause(@Nullable Activity activity) { + LogUtil.i("SpeedDialFragment.onPagePause", null); + } + + public interface HostInterface { + + void setDragDropController(DragDropController controller); + + void showAllContactsTab(); + } + + private class ContactTileLoaderListener implements LoaderManager.LoaderCallbacks { + + @Override + public CursorLoader onCreateLoader(int id, Bundle args) { + if (DEBUG) { + LogUtil.d("ContactTileLoaderListener.onCreateLoader", null); + } + return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity()); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + if (DEBUG) { + LogUtil.d("ContactTileLoaderListener.onLoadFinished", null); + } + mContactTileAdapter.setContactCursor(data); + setEmptyViewVisibility(mContactTileAdapter.getCount() == 0); + } + + @Override + public void onLoaderReset(Loader loader) { + if (DEBUG) { + LogUtil.d("ContactTileLoaderListener.onLoaderReset", null); + } + } + } + + private class ContactTileAdapterListener implements ContactTileView.Listener { + + @Override + public void onContactSelected(Uri contactUri, Rect targetRect) { + if (mPhoneNumberPickerActionListener != null) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL; + mPhoneNumberPickerActionListener.onPickDataUri( + contactUri, false /* isVideoCall */, callSpecificAppData); + } + } + + @Override + public void onCallNumberDirectly(String phoneNumber) { + if (mPhoneNumberPickerActionListener != null) { + CallSpecificAppData callSpecificAppData = new CallSpecificAppData(); + callSpecificAppData.callInitiationType = CallInitiationType.Type.SPEED_DIAL; + mPhoneNumberPickerActionListener.onPickPhoneNumber( + phoneNumber, false /* isVideoCall */, callSpecificAppData); + } + } + } + + private class ScrollListener implements ListView.OnScrollListener { + + @Override + public void onScroll( + AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + if (mActivityScrollListener != null) { + mActivityScrollListener.onListFragmentScroll( + firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mActivityScrollListener.onListFragmentScrollStateChange(scrollState); + } + } +} diff --git a/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml new file mode 100644 index 000000000..247b34f4c --- /dev/null +++ b/java/com/android/dialer/app/manifests/activities/AndroidManifest.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/color/settings_text_color_primary.xml b/java/com/android/dialer/app/res/color/settings_text_color_primary.xml new file mode 100644 index 000000000..ba259088a --- /dev/null +++ b/java/com/android/dialer/app/res/color/settings_text_color_primary.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml b/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml new file mode 100644 index 000000000..2f7899272 --- /dev/null +++ b/java/com/android/dialer/app/res/color/settings_text_color_secondary.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png new file mode 100644 index 000000000..d6f6daaab Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png new file mode 100644 index 000000000..d3c0378f5 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png new file mode 100644 index 000000000..3e9232fc9 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png new file mode 100644 index 000000000..3cad4c660 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png new file mode 100644 index 000000000..bb72e890f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png new file mode 100644 index 000000000..14a33e39f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png new file mode 100644 index 000000000..70eb07378 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png new file mode 100644 index 000000000..9fb43b066 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png new file mode 100644 index 000000000..4e0d5649e Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png new file mode 100644 index 000000000..2cf41d598 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png new file mode 100644 index 000000000..043685fd9 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png new file mode 100644 index 000000000..86eecdd4a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png new file mode 100644 index 000000000..34310aa49 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png new file mode 100644 index 000000000..a36323ca9 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png new file mode 100644 index 000000000..4b67cf71a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png new file mode 100644 index 000000000..67f07e473 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png new file mode 100644 index 000000000..26a26f911 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png new file mode 100644 index 000000000..bf413f912 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png new file mode 100644 index 000000000..4d2ea05c4 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png new file mode 100644 index 000000000..ff698afc0 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png new file mode 100644 index 000000000..b27dfba06 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png new file mode 100644 index 000000000..57c9fa546 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png new file mode 100644 index 000000000..1ee6adf8d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png new file mode 100644 index 000000000..3a1a7a790 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png new file mode 100644 index 000000000..f3581d104 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png new file mode 100644 index 000000000..b09a6926d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png new file mode 100644 index 000000000..62e1f8a6d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png new file mode 100644 index 000000000..03643b20d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png new file mode 100644 index 000000000..47e32492c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png new file mode 100644 index 000000000..2bfe0c0cf Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png new file mode 100644 index 000000000..90b5238f3 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png new file mode 100644 index 000000000..7556637fc Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png new file mode 100644 index 000000000..03a62e15f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png new file mode 100644 index 000000000..e22e92c85 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png new file mode 100644 index 000000000..57d787163 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png new file mode 100644 index 000000000..3dc1c17f6 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png differ diff --git a/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png new file mode 100644 index 000000000..44b06f261 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png new file mode 100644 index 000000000..3cd59b35b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png new file mode 100644 index 000000000..2ce7eae37 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png new file mode 100644 index 000000000..98152e0d3 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png new file mode 100644 index 000000000..4c854e1a1 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png new file mode 100644 index 000000000..f6aa3f966 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png new file mode 100644 index 000000000..169cf2934 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png new file mode 100644 index 000000000..80c069557 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png new file mode 100644 index 000000000..c903fd1dd Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png new file mode 100644 index 000000000..56ac2a33a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png new file mode 100644 index 000000000..16a44a078 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png new file mode 100644 index 000000000..66df69eac Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png new file mode 100644 index 000000000..d2cbe4c92 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png new file mode 100644 index 000000000..81a67ba6f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png new file mode 100644 index 000000000..3597a5e82 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png new file mode 100644 index 000000000..2310c734a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png new file mode 100644 index 000000000..017e45ede Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png new file mode 100644 index 000000000..d7d5c588f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png new file mode 100644 index 000000000..b1f1c7efe Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png new file mode 100644 index 000000000..2272d478c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png new file mode 100644 index 000000000..270e4de2e Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png new file mode 100644 index 000000000..c1766b854 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png new file mode 100644 index 000000000..c61e948bb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png new file mode 100644 index 000000000..2c134ea10 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png new file mode 100644 index 000000000..74ccf14b8 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png new file mode 100644 index 000000000..501ee842e Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png new file mode 100644 index 000000000..e944fd70c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png new file mode 100644 index 000000000..d2af0ba20 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png new file mode 100644 index 000000000..d80fb2f5c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png new file mode 100644 index 000000000..4c671ecb4 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png new file mode 100644 index 000000000..41044b456 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png new file mode 100644 index 000000000..c6040c09e Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png new file mode 100644 index 000000000..ac6a69c14 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png new file mode 100644 index 000000000..e5aa7db05 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png new file mode 100644 index 000000000..10992ed70 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png new file mode 100644 index 000000000..7cfd4c7b8 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png new file mode 100644 index 000000000..0c33905cd Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png differ diff --git a/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png new file mode 100644 index 000000000..8665d8303 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png new file mode 100644 index 000000000..14ec04ba1 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png new file mode 100644 index 000000000..65b1de333 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png new file mode 100644 index 000000000..a3a76751b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png new file mode 100644 index 000000000..398a03cee Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png new file mode 100644 index 000000000..3513bd9fe Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png new file mode 100644 index 000000000..6f1366018 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png new file mode 100644 index 000000000..537fd4e8b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png new file mode 100644 index 000000000..be1ee4d07 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png new file mode 100644 index 000000000..aff140fcd Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png new file mode 100644 index 000000000..8975727e0 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png new file mode 100644 index 000000000..4d48ea9ea Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png new file mode 100644 index 000000000..d65f39d7c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png new file mode 100644 index 000000000..0ad839286 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png new file mode 100644 index 000000000..6b411cbc3 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png new file mode 100644 index 000000000..a9a83b329 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png new file mode 100644 index 000000000..efab8a74f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png new file mode 100644 index 000000000..3e6ec071b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png new file mode 100644 index 000000000..138f27cdb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png new file mode 100644 index 000000000..f49aed757 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png new file mode 100644 index 000000000..323981ccf Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png new file mode 100644 index 000000000..83167f4cd Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png new file mode 100644 index 000000000..a3c80e73d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png new file mode 100644 index 000000000..be81592ef Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png new file mode 100644 index 000000000..0e24fa45c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png new file mode 100644 index 000000000..2e27936a4 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png new file mode 100644 index 000000000..22a8783e7 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png new file mode 100644 index 000000000..2071f42f2 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png new file mode 100644 index 000000000..f7dfa21ac Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png new file mode 100644 index 000000000..36b5e2030 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png new file mode 100644 index 000000000..99d7fd51a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png new file mode 100644 index 000000000..468023d8a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png new file mode 100644 index 000000000..970329493 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png new file mode 100644 index 000000000..59126d706 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png new file mode 100644 index 000000000..2621bc15d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png new file mode 100644 index 000000000..2ed00343b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png new file mode 100644 index 000000000..5667ab368 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png differ diff --git a/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png new file mode 100644 index 000000000..8359a50e9 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png new file mode 100644 index 000000000..501d7f1e2 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png new file mode 100644 index 000000000..407d78c9c Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png new file mode 100644 index 000000000..fb2ea5f15 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png new file mode 100644 index 000000000..5f1cd45fb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png new file mode 100644 index 000000000..00e04e42b Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png new file mode 100644 index 000000000..0364ee015 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png new file mode 100644 index 000000000..9dff893e7 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png new file mode 100644 index 000000000..eb637920d Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png new file mode 100644 index 000000000..1657da4e2 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png new file mode 100644 index 000000000..f25cce695 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png new file mode 100644 index 000000000..7ac4d8b58 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png new file mode 100644 index 000000000..aa5879215 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png new file mode 100644 index 000000000..d07a1d057 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png new file mode 100644 index 000000000..779bc0620 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png new file mode 100644 index 000000000..07128dd82 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png new file mode 100644 index 000000000..d32281307 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png new file mode 100644 index 000000000..7c256b5d7 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png new file mode 100644 index 000000000..f699959cb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png new file mode 100644 index 000000000..7192ad487 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png new file mode 100644 index 000000000..6c68435fb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png new file mode 100644 index 000000000..8fff728bb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png new file mode 100644 index 000000000..547ef30aa Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png new file mode 100644 index 000000000..2722f23aa Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png new file mode 100644 index 000000000..9594619cb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png new file mode 100644 index 000000000..bfc72736a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png new file mode 100644 index 000000000..a35b3cd14 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png new file mode 100644 index 000000000..f3c830435 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png new file mode 100644 index 000000000..828a4879f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png new file mode 100644 index 000000000..bab4a4311 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png new file mode 100644 index 000000000..1c13101a8 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png new file mode 100644 index 000000000..ed3a17329 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png new file mode 100644 index 000000000..c04b8d117 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png new file mode 100644 index 000000000..28b8e936a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png new file mode 100644 index 000000000..5eb8b671f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png new file mode 100644 index 000000000..2e751a40f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png b/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png new file mode 100644 index 000000000..ff55620d0 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png b/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png new file mode 100644 index 000000000..bfeb0ff53 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png new file mode 100644 index 000000000..fbac1e40f Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png new file mode 100644 index 000000000..5893965e9 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png new file mode 100644 index 000000000..9361aa864 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png new file mode 100644 index 000000000..34cd3fd80 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png new file mode 100644 index 000000000..8243c2536 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png new file mode 100644 index 000000000..4ddee9ef0 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png new file mode 100644 index 000000000..2f250f64a Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png new file mode 100644 index 000000000..7f38d0963 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png new file mode 100644 index 000000000..72641c7ab Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png new file mode 100644 index 000000000..b7403ff22 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png new file mode 100644 index 000000000..2f2cb3d00 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png new file mode 100644 index 000000000..6591ed485 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png new file mode 100644 index 000000000..2a18de24e Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png new file mode 100644 index 000000000..660ac6585 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png new file mode 100644 index 000000000..5676f7041 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png new file mode 100644 index 000000000..30d141db5 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png new file mode 100644 index 000000000..be5c062b5 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png new file mode 100644 index 000000000..395652cdf Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png new file mode 100644 index 000000000..b94f4dfa1 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png new file mode 100644 index 000000000..e351c7beb Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png new file mode 100644 index 000000000..99a1842a2 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png new file mode 100644 index 000000000..820ff5066 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png new file mode 100644 index 000000000..4ab55abbd Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png new file mode 100644 index 000000000..82972b4e5 Binary files /dev/null and b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png differ diff --git a/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml b/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml new file mode 100644 index 000000000..35afbe025 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/floating_action_button.xml b/java/com/android/dialer/app/res/drawable/floating_action_button.xml new file mode 100644 index 000000000..0b9af5229 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/floating_action_button.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml new file mode 100644 index 000000000..87e0fbc6f --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml new file mode 100644 index 000000000..e6d5c4776 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml new file mode 100644 index 000000000..e90e83e8b --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml b/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml new file mode 100644 index 000000000..3b614cf0d --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_pause.xml b/java/com/android/dialer/app/res/drawable/ic_pause.xml new file mode 100644 index 000000000..5bea58192 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_pause.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml b/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml new file mode 100644 index 000000000..d7d935016 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_play_arrow.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_search_phone.xml b/java/com/android/dialer/app/res/drawable/ic_search_phone.xml new file mode 100644 index 000000000..5d449ee56 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_search_phone.xml @@ -0,0 +1,20 @@ + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml b/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml new file mode 100644 index 000000000..f07d0a889 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml b/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml new file mode 100644 index 000000000..456a0483e --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml @@ -0,0 +1,20 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml new file mode 100644 index 000000000..84cda0310 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml new file mode 100644 index 000000000..5e974c45a --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/java/com/android/dialer/app/res/drawable/oval_ripple.xml b/java/com/android/dialer/app/res/drawable/oval_ripple.xml new file mode 100644 index 000000000..abb002588 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/oval_ripple.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/overflow_menu.xml b/java/com/android/dialer/app/res/drawable/overflow_menu.xml new file mode 100644 index 000000000..81be5dcd5 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/overflow_menu.xml @@ -0,0 +1,20 @@ + + + diff --git a/java/com/android/dialer/app/res/drawable/rounded_corner.xml b/java/com/android/dialer/app/res/drawable/rounded_corner.xml new file mode 100644 index 000000000..97b58b6b1 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/rounded_corner.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml b/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml new file mode 100644 index 000000000..e47a6406c --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/seekbar_drawable.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml b/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml new file mode 100644 index 000000000..47d1152db --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml b/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml new file mode 100644 index 000000000..6271a8f86 --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/shadow_fade_left.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml b/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml new file mode 100644 index 000000000..86d37a9bc --- /dev/null +++ b/java/com/android/dialer/app/res/drawable/shadow_fade_up.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml b/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml new file mode 100644 index 000000000..8d8236a43 --- /dev/null +++ b/java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml b/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml new file mode 100644 index 000000000..5f8068067 --- /dev/null +++ b/java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml b/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml new file mode 100644 index 000000000..c6e186257 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml @@ -0,0 +1,47 @@ + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/all_contacts_activity.xml b/java/com/android/dialer/app/res/layout/all_contacts_activity.xml new file mode 100644 index 000000000..72f0a147f --- /dev/null +++ b/java/com/android/dialer/app/res/layout/all_contacts_activity.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml b/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml new file mode 100644 index 000000000..f59847825 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/all_contacts_fragment.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/blocked_number_footer.xml b/java/com/android/dialer/app/res/layout/blocked_number_footer.xml new file mode 100644 index 000000000..9e05cfbf4 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/blocked_number_footer.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml b/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml new file mode 100644 index 000000000..745b913cc --- /dev/null +++ b/java/com/android/dialer/app/res/layout/blocked_number_fragment.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/java/com/android/dialer/app/res/layout/blocked_number_header.xml b/java/com/android/dialer/app/res/layout/blocked_number_header.xml new file mode 100644 index 000000000..e34510b73 --- /dev/null +++ b/java/com/android/dialer/app/res/layout/blocked_number_header.xml @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + +