summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
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
Diffstat (limited to 'java')
-rw-r--r--java/com/android/contacts/common/AndroidManifest.xml39
-rw-r--r--java/com/android/contacts/common/Bindings.java52
-rw-r--r--java/com/android/contacts/common/ClipboardUtils.java55
-rw-r--r--java/com/android/contacts/common/Collapser.java95
-rw-r--r--java/com/android/contacts/common/ContactPhotoManager.java487
-rw-r--r--java/com/android/contacts/common/ContactPhotoManagerImpl.java1262
-rw-r--r--java/com/android/contacts/common/ContactPresenceIconUtil.java46
-rw-r--r--java/com/android/contacts/common/ContactStatusUtil.java44
-rw-r--r--java/com/android/contacts/common/ContactTileLoaderFactory.java64
-rw-r--r--java/com/android/contacts/common/ContactsUtils.java265
-rw-r--r--java/com/android/contacts/common/GeoUtil.java55
-rw-r--r--java/com/android/contacts/common/GroupMetaData.java76
-rw-r--r--java/com/android/contacts/common/MoreContactUtils.java251
-rw-r--r--java/com/android/contacts/common/bindings/ContactsCommonBindings.java25
-rw-r--r--java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java24
-rw-r--r--java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java27
-rw-r--r--java/com/android/contacts/common/compat/CallCompat.java45
-rw-r--r--java/com/android/contacts/common/compat/CallableCompat.java36
-rw-r--r--java/com/android/contacts/common/compat/ContactsCompat.java57
-rw-r--r--java/com/android/contacts/common/compat/DirectoryCompat.java51
-rw-r--r--java/com/android/contacts/common/compat/PhoneAccountCompat.java104
-rw-r--r--java/com/android/contacts/common/compat/PhoneCompat.java36
-rw-r--r--java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java174
-rw-r--r--java/com/android/contacts/common/compat/TelephonyManagerCompat.java213
-rw-r--r--java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java302
-rw-r--r--java/com/android/contacts/common/database/ContactUpdateUtils.java49
-rw-r--r--java/com/android/contacts/common/database/EmptyCursor.java84
-rw-r--r--java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java73
-rw-r--r--java/com/android/contacts/common/dialog/CallSubjectDialog.java607
-rw-r--r--java/com/android/contacts/common/dialog/ClearFrequentsDialog.java88
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java28
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java45
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java27
-rw-r--r--java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java29
-rw-r--r--java/com/android/contacts/common/format/FormatUtils.java181
-rw-r--r--java/com/android/contacts/common/format/TextHighlighter.java93
-rw-r--r--java/com/android/contacts/common/format/testing/SpannedTestUtils.java85
-rw-r--r--java/com/android/contacts/common/lettertiles/LetterTileDrawable.java382
-rw-r--r--java/com/android/contacts/common/list/AutoScrollListView.java125
-rw-r--r--java/com/android/contacts/common/list/ContactEntry.java57
-rw-r--r--java/com/android/contacts/common/list/ContactEntryListAdapter.java742
-rw-r--r--java/com/android/contacts/common/list/ContactEntryListFragment.java862
-rw-r--r--java/com/android/contacts/common/list/ContactListAdapter.java232
-rw-r--r--java/com/android/contacts/common/list/ContactListFilter.java297
-rw-r--r--java/com/android/contacts/common/list/ContactListFilterController.java170
-rw-r--r--java/com/android/contacts/common/list/ContactListItemView.java1513
-rw-r--r--java/com/android/contacts/common/list/ContactListPinnedHeaderView.java70
-rw-r--r--java/com/android/contacts/common/list/ContactTileView.java171
-rw-r--r--java/com/android/contacts/common/list/ContactsSectionIndexer.java119
-rw-r--r--java/com/android/contacts/common/list/DefaultContactListAdapter.java216
-rw-r--r--java/com/android/contacts/common/list/DirectoryListLoader.java201
-rw-r--r--java/com/android/contacts/common/list/DirectoryPartition.java179
-rw-r--r--java/com/android/contacts/common/list/IndexerListAdapter.java214
-rw-r--r--java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java39
-rw-r--r--java/com/android/contacts/common/list/PhoneNumberListAdapter.java583
-rw-r--r--java/com/android/contacts/common/list/PhoneNumberPickerFragment.java402
-rw-r--r--java/com/android/contacts/common/list/PinnedHeaderListAdapter.java159
-rw-r--r--java/com/android/contacts/common/list/PinnedHeaderListView.java563
-rw-r--r--java/com/android/contacts/common/list/ViewPagerTabStrip.java109
-rw-r--r--java/com/android/contacts/common/list/ViewPagerTabs.java317
-rw-r--r--java/com/android/contacts/common/location/CountryDetector.java221
-rw-r--r--java/com/android/contacts/common/location/UpdateCountryService.java104
-rw-r--r--java/com/android/contacts/common/model/AccountTypeManager.java813
-rw-r--r--java/com/android/contacts/common/model/BuilderWrapper.java53
-rw-r--r--java/com/android/contacts/common/model/CPOWrapper.java50
-rw-r--r--java/com/android/contacts/common/model/Contact.java384
-rw-r--r--java/com/android/contacts/common/model/ContactLoader.java998
-rw-r--r--java/com/android/contacts/common/model/RawContact.java351
-rw-r--r--java/com/android/contacts/common/model/account/AccountType.java501
-rw-r--r--java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java103
-rw-r--r--java/com/android/contacts/common/model/account/AccountWithDataSet.java229
-rw-r--r--java/com/android/contacts/common/model/account/BaseAccountType.java1890
-rw-r--r--java/com/android/contacts/common/model/account/ExchangeAccountType.java365
-rw-r--r--java/com/android/contacts/common/model/account/ExternalAccountType.java443
-rw-r--r--java/com/android/contacts/common/model/account/FallbackAccountType.java77
-rw-r--r--java/com/android/contacts/common/model/account/GoogleAccountType.java206
-rw-r--r--java/com/android/contacts/common/model/account/SamsungAccountType.java235
-rw-r--r--java/com/android/contacts/common/model/dataitem/DataItem.java258
-rw-r--r--java/com/android/contacts/common/model/dataitem/DataKind.java132
-rw-r--r--java/com/android/contacts/common/model/dataitem/EmailDataItem.java47
-rw-r--r--java/com/android/contacts/common/model/dataitem/EventDataItem.java62
-rw-r--r--java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java40
-rw-r--r--java/com/android/contacts/common/model/dataitem/IdentityDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/ImDataItem.java109
-rw-r--r--java/com/android/contacts/common/model/dataitem/NicknameDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/NoteDataItem.java35
-rw-r--r--java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java64
-rw-r--r--java/com/android/contacts/common/model/dataitem/PhoneDataItem.java76
-rw-r--r--java/com/android/contacts/common/model/dataitem/PhotoDataItem.java39
-rw-r--r--java/com/android/contacts/common/model/dataitem/RelationDataItem.java62
-rw-r--r--java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java40
-rw-r--r--java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java100
-rw-r--r--java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java68
-rw-r--r--java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java39
-rw-r--r--java/com/android/contacts/common/preference/ContactsPreferences.java269
-rw-r--r--java/com/android/contacts/common/preference/DisplayOrderPreference.java89
-rw-r--r--java/com/android/contacts/common/preference/SortOrderPreference.java89
-rw-r--r--java/com/android/contacts/common/res/color/popup_menu_color.xml20
-rw-r--r--java/com/android/contacts/common/res/color/tab_text_color.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.pngbin0 -> 1115 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.pngbin0 -> 612 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.pngbin0 -> 2477 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.pngbin0 -> 373 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.pngbin0 -> 609 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.pngbin0 -> 370 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.pngbin0 -> 389 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.pngbin0 -> 525 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.pngbin0 -> 485 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.pngbin0 -> 799 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.pngbin0 -> 1954 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.pngbin0 -> 1922 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.pngbin0 -> 220 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.pngbin0 -> 1439 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.pngbin0 -> 1416 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.pngbin0 -> 515 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.pngbin0 -> 1438 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.pngbin0 -> 1211 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.pngbin0 -> 1414 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.pngbin0 -> 167 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.pngbin0 -> 273 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.pngbin0 -> 289 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.pngbin0 -> 828 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.pngbin0 -> 413 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.pngbin0 -> 544 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.pngbin0 -> 370 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.pngbin0 -> 269 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.pngbin0 -> 3607 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.pngbin0 -> 154 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.pngbin0 -> 224 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.pngbin0 -> 159 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.pngbin0 -> 205 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.pngbin0 -> 267 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.pngbin0 -> 219 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.pngbin0 -> 234 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.pngbin0 -> 191 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.pngbin0 -> 258 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.pngbin0 -> 178 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.pngbin0 -> 234 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.pngbin0 -> 180 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.pngbin0 -> 186 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.pngbin0 -> 1666 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.pngbin0 -> 1034 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.pngbin0 -> 2486 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.pngbin0 -> 243 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.pngbin0 -> 234 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.pngbin0 -> 196 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.pngbin0 -> 255 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.pngbin0 -> 781 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.pngbin0 -> 578 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.pngbin0 -> 2040 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.pngbin0 -> 246 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.pngbin0 -> 266 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.pngbin0 -> 572 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.pngbin0 -> 290 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.pngbin0 -> 297 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.pngbin0 -> 320 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.pngbin0 -> 607 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.pngbin0 -> 1266 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.pngbin0 -> 1270 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.pngbin0 -> 171 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.pngbin0 -> 1052 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.pngbin0 -> 1021 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.pngbin0 -> 424 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.pngbin0 -> 1034 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.pngbin0 -> 1018 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.pngbin0 -> 130 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.pngbin0 -> 188 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.pngbin0 -> 204 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.pngbin0 -> 476 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.pngbin0 -> 299 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.pngbin0 -> 504 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.pngbin0 -> 265 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.pngbin0 -> 216 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.pngbin0 -> 2120 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.pngbin0 -> 151 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.pngbin0 -> 188 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.pngbin0 -> 155 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.pngbin0 -> 198 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.pngbin0 -> 199 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.pngbin0 -> 1659 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.pngbin0 -> 1005 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.pngbin0 -> 2478 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.pngbin0 -> 1451 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.pngbin0 -> 765 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.pngbin0 -> 2916 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.pngbin0 -> 420 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.pngbin0 -> 449 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.pngbin0 -> 814 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.pngbin0 -> 426 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.pngbin0 -> 461 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.pngbin0 -> 659 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.pngbin0 -> 655 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.pngbin0 -> 1034 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.pngbin0 -> 2650 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.pngbin0 -> 2632 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.pngbin0 -> 287 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.pngbin0 -> 1844 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.pngbin0 -> 1815 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.pngbin0 -> 593 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.pngbin0 -> 1830 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.pngbin0 -> 1607 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.pngbin0 -> 1827 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.pngbin0 -> 204 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.pngbin0 -> 312 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.pngbin0 -> 329 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.pngbin0 -> 1009 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.pngbin0 -> 439 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.pngbin0 -> 620 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.pngbin0 -> 405 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.pngbin0 -> 301 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.pngbin0 -> 4894 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.pngbin0 -> 245 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.pngbin0 -> 162 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.pngbin0 -> 163 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.pngbin0 -> 210 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.pngbin0 -> 267 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.pngbin0 -> 2100 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.pngbin0 -> 1376 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.pngbin0 -> 2541 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.pngbin0 -> 597 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.pngbin0 -> 647 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.pngbin0 -> 1465 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.pngbin0 -> 668 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.pngbin0 -> 604 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.pngbin0 -> 971 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.pngbin0 -> 953 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.pngbin0 -> 1546 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.pngbin0 -> 3338 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.pngbin0 -> 3381 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.pngbin0 -> 414 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.pngbin0 -> 2357 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.pngbin0 -> 2363 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.pngbin0 -> 1381 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.pngbin0 -> 2111 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.pngbin0 -> 2119 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.pngbin0 -> 2117 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.pngbin0 -> 269 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.pngbin0 -> 440 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.pngbin0 -> 464 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.pngbin0 -> 1517 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.pngbin0 -> 603 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.pngbin0 -> 837 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.pngbin0 -> 551 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.pngbin0 -> 398 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.pngbin0 -> 7976 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.pngbin0 -> 1140 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.pngbin0 -> 1147 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.pngbin0 -> 465 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.pngbin0 -> 2571 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.pngbin0 -> 1512 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.pngbin0 -> 2915 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.pngbin0 -> 778 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.pngbin0 -> 853 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.pngbin0 -> 1688 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.pngbin0 -> 612 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.pngbin0 -> 1311 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.pngbin0 -> 1279 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.pngbin0 -> 342 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.pngbin0 -> 610 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.pngbin0 -> 2135 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.pngbin0 -> 1579 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.pngbin0 -> 481 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.pngbin0 -> 11277 bytes
-rw-r--r--java/com/android/contacts/common/res/drawable/dialog_background_material.xml23
-rw-r--r--java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_back_arrow.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_call.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_message_24dp.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_more_vert.xml9
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_search_video_call.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_tab_all.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_tab_groups.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_tab_starred.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/ic_work_profile.xml16
-rw-r--r--java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml19
-rw-r--r--java/com/android/contacts/common/res/drawable/item_background_material_dark.xml23
-rw-r--r--java/com/android/contacts/common/res/drawable/item_background_material_light.xml23
-rw-r--r--java/com/android/contacts/common/res/drawable/list_item_activated_background.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml20
-rw-r--r--java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml7
-rw-r--r--java/com/android/contacts/common/res/drawable/unread_count_background.xml21
-rw-r--r--java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml22
-rw-r--r--java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml48
-rw-r--r--java/com/android/contacts/common/res/layout/account_filter_header.xml44
-rw-r--r--java/com/android/contacts/common/res/layout/account_selector_list_item.xml57
-rw-r--r--java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml56
-rw-r--r--java/com/android/contacts/common/res/layout/call_subject_history.xml33
-rw-r--r--java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml29
-rw-r--r--java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml27
-rw-r--r--java/com/android/contacts/common/res/layout/contact_list_card.xml39
-rw-r--r--java/com/android/contacts/common/res/layout/contact_list_content.xml61
-rw-r--r--java/com/android/contacts/common/res/layout/default_account_checkbox.xml36
-rw-r--r--java/com/android/contacts/common/res/layout/dialog_call_subject.xml159
-rw-r--r--java/com/android/contacts/common/res/layout/directory_header.xml55
-rw-r--r--java/com/android/contacts/common/res/layout/list_separator.xml27
-rw-r--r--java/com/android/contacts/common/res/layout/search_bar_expanded.xml62
-rw-r--r--java/com/android/contacts/common/res/layout/select_account_list_item.xml56
-rw-r--r--java/com/android/contacts/common/res/layout/unread_count_tab.xml43
-rw-r--r--java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.pngbin0 -> 3169 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.pngbin0 -> 2062 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.pngbin0 -> 4430 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.pngbin0 -> 7228 bytes
-rw-r--r--java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.pngbin0 -> 10065 bytes
-rw-r--r--java/com/android/contacts/common/res/values-ja/donottranslate_config.xml20
-rw-r--r--java/com/android/contacts/common/res/values-ko/donottranslate_config.xml17
-rw-r--r--java/com/android/contacts/common/res/values-land/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-sw600dp-land/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-sw600dp/dimens.xml29
-rw-r--r--java/com/android/contacts/common/res/values-sw600dp/integers.xml24
-rw-r--r--java/com/android/contacts/common/res/values-sw720dp-land/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-sw720dp/integers.xml22
-rw-r--r--java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml17
-rw-r--r--java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml17
-rw-r--r--java/com/android/contacts/common/res/values/animation_constants.xml19
-rw-r--r--java/com/android/contacts/common/res/values/attrs.xml83
-rw-r--r--java/com/android/contacts/common/res/values/colors.xml158
-rw-r--r--java/com/android/contacts/common/res/values/dimens.xml161
-rw-r--r--java/com/android/contacts/common/res/values/donottranslate_config.xml95
-rw-r--r--java/com/android/contacts/common/res/values/ids.xml30
-rw-r--r--java/com/android/contacts/common/res/values/integers.xml39
-rw-r--r--java/com/android/contacts/common/res/values/strings.xml798
-rw-r--r--java/com/android/contacts/common/res/values/styles.xml97
-rw-r--r--java/com/android/contacts/common/testing/InjectedServices.java65
-rw-r--r--java/com/android/contacts/common/util/AccountFilterUtil.java125
-rw-r--r--java/com/android/contacts/common/util/BitmapUtil.java167
-rw-r--r--java/com/android/contacts/common/util/CommonDateUtils.java37
-rw-r--r--java/com/android/contacts/common/util/Constants.java28
-rw-r--r--java/com/android/contacts/common/util/ContactDisplayUtils.java307
-rw-r--r--java/com/android/contacts/common/util/ContactListViewUtils.java89
-rw-r--r--java/com/android/contacts/common/util/ContactLoaderUtils.java78
-rw-r--r--java/com/android/contacts/common/util/DateUtils.java283
-rw-r--r--java/com/android/contacts/common/util/FabUtil.java71
-rw-r--r--java/com/android/contacts/common/util/MaterialColorMapUtils.java181
-rw-r--r--java/com/android/contacts/common/util/NameConverter.java242
-rw-r--r--java/com/android/contacts/common/util/SearchUtil.java198
-rw-r--r--java/com/android/contacts/common/util/StopWatch.java100
-rw-r--r--java/com/android/contacts/common/util/TelephonyManagerUtils.java45
-rw-r--r--java/com/android/contacts/common/util/TrafficStatsTags.java22
-rw-r--r--java/com/android/contacts/common/util/UriUtils.java90
-rw-r--r--java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java43
-rw-r--r--java/com/android/contacts/common/widget/FloatingActionButtonController.java226
-rw-r--r--java/com/android/contacts/common/widget/LayoutSuppressingImageView.java39
-rw-r--r--java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java297
-rw-r--r--java/com/android/dialer/animation/AnimUtils.java247
-rw-r--r--java/com/android/dialer/animation/AnimationListenerAdapter.java39
-rw-r--r--java/com/android/dialer/app/AndroidManifest.xml116
-rw-r--r--java/com/android/dialer/app/Bindings.java77
-rw-r--r--java/com/android/dialer/app/CallDetailActivity.java480
-rw-r--r--java/com/android/dialer/app/DialerApplication.java77
-rw-r--r--java/com/android/dialer/app/DialtactsActivity.java1484
-rw-r--r--java/com/android/dialer/app/FloatingActionButtonBehavior.java50
-rw-r--r--java/com/android/dialer/app/PhoneCallDetails.java207
-rw-r--r--java/com/android/dialer/app/SpecialCharSequenceMgr.java493
-rw-r--r--java/com/android/dialer/app/alert/AlertManager.java30
-rw-r--r--java/com/android/dialer/app/bindings/DialerBindings.java25
-rw-r--r--java/com/android/dialer/app/bindings/DialerBindingsFactory.java26
-rw-r--r--java/com/android/dialer/app/bindings/DialerBindingsStub.java48
-rw-r--r--java/com/android/dialer/app/calllog/BlockReportSpamListener.java212
-rw-r--r--java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java214
-rw-r--r--java/com/android/dialer/app/calllog/CallLogAdapter.java915
-rw-r--r--java/com/android/dialer/app/calllog/CallLogAlertManager.java90
-rw-r--r--java/com/android/dialer/app/calllog/CallLogAsync.java96
-rw-r--r--java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java376
-rw-r--r--java/com/android/dialer/app/calllog/CallLogFragment.java528
-rw-r--r--java/com/android/dialer/app/calllog/CallLogGroupBuilder.java274
-rw-r--r--java/com/android/dialer/app/calllog/CallLogListItemHelper.java277
-rw-r--r--java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java966
-rw-r--r--java/com/android/dialer/app/calllog/CallLogModalAlertManager.java74
-rw-r--r--java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java299
-rw-r--r--java/com/android/dialer/app/calllog/CallLogNotificationsService.java203
-rw-r--r--java/com/android/dialer/app/calllog/CallLogReceiver.java77
-rw-r--r--java/com/android/dialer/app/calllog/CallTypeHelper.java136
-rw-r--r--java/com/android/dialer/app/calllog/CallTypeIconsView.java221
-rw-r--r--java/com/android/dialer/app/calllog/ClearCallLogDialog.java98
-rw-r--r--java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java273
-rw-r--r--java/com/android/dialer/app/calllog/GroupingListAdapter.java153
-rw-r--r--java/com/android/dialer/app/calllog/IntentProvider.java198
-rw-r--r--java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java50
-rw-r--r--java/com/android/dialer/app/calllog/MissedCallNotifier.java330
-rw-r--r--java/com/android/dialer/app/calllog/PhoneAccountUtils.java104
-rw-r--r--java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java352
-rw-r--r--java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java75
-rw-r--r--java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java85
-rw-r--r--java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java132
-rw-r--r--java/com/android/dialer/app/calllog/VoicemailQueryHandler.java74
-rw-r--r--java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java105
-rw-r--r--java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java74
-rw-r--r--java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java116
-rw-r--r--java/com/android/dialer/app/contactinfo/ContactInfoCache.java357
-rw-r--r--java/com/android/dialer/app/contactinfo/ContactInfoRequest.java122
-rw-r--r--java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java129
-rw-r--r--java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java67
-rw-r--r--java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java57
-rw-r--r--java/com/android/dialer/app/dialpad/DialpadFragment.java1689
-rw-r--r--java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java161
-rw-r--r--java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java202
-rw-r--r--java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java56
-rw-r--r--java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java97
-rw-r--r--java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java271
-rw-r--r--java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java146
-rw-r--r--java/com/android/dialer/app/filterednumber/NumbersAdapter.java138
-rw-r--r--java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java56
-rw-r--r--java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java130
-rw-r--r--java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java47
-rw-r--r--java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java26
-rw-r--r--java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java53
-rw-r--r--java/com/android/dialer/app/list/AllContactsFragment.java209
-rw-r--r--java/com/android/dialer/app/list/BlockedListSearchAdapter.java84
-rw-r--r--java/com/android/dialer/app/list/BlockedListSearchFragment.java245
-rw-r--r--java/com/android/dialer/app/list/ContentChangedFilter.java56
-rw-r--r--java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java228
-rw-r--r--java/com/android/dialer/app/list/DragDropController.java106
-rw-r--r--java/com/android/dialer/app/list/ListsFragment.java587
-rw-r--r--java/com/android/dialer/app/list/OnDragDropListener.java58
-rw-r--r--java/com/android/dialer/app/list/OnListFragmentScrolledListener.java27
-rw-r--r--java/com/android/dialer/app/list/PhoneFavoriteListView.java315
-rw-r--r--java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java119
-rw-r--r--java/com/android/dialer/app/list/PhoneFavoriteTileView.java155
-rw-r--r--java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java627
-rw-r--r--java/com/android/dialer/app/list/RegularSearchFragment.java146
-rw-r--r--java/com/android/dialer/app/list/RegularSearchListAdapter.java126
-rw-r--r--java/com/android/dialer/app/list/RemoveView.java105
-rw-r--r--java/com/android/dialer/app/list/SearchFragment.java425
-rw-r--r--java/com/android/dialer/app/list/SmartDialNumberListAdapter.java117
-rw-r--r--java/com/android/dialer/app/list/SmartDialSearchFragment.java120
-rw-r--r--java/com/android/dialer/app/list/SpeedDialFragment.java512
-rw-r--r--java/com/android/dialer/app/manifests/activities/AndroidManifest.xml129
-rw-r--r--java/com/android/dialer/app/res/color/settings_text_color_primary.xml23
-rw-r--r--java/com/android/dialer/app/res/color/settings_text_color_secondary.xml23
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.pngbin0 -> 3538 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.pngbin0 -> 2461 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.pngbin0 -> 6041 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.pngbin0 -> 1028 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.pngbin0 -> 247 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.pngbin0 -> 538 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.pngbin0 -> 203 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.pngbin0 -> 242 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.pngbin0 -> 1649 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.pngbin0 -> 2305 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.pngbin0 -> 2419 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.pngbin0 -> 370 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_handle.pngbin0 -> 543 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.pngbin0 -> 1565 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.pngbin0 -> 134 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.pngbin0 -> 565 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.pngbin0 -> 858 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.pngbin0 -> 105 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.pngbin0 -> 299 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.pngbin0 -> 347 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.pngbin0 -> 195 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_remove.pngbin0 -> 884 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.pngbin0 -> 1084 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.pngbin0 -> 575 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.pngbin0 -> 397 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_star.pngbin0 -> 732 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.pngbin0 -> 1049 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.pngbin0 -> 1339 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.pngbin0 -> 1337 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.pngbin0 -> 1755 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.pngbin0 -> 1750 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.pngbin0 -> 478 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.pngbin0 -> 186 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.pngbin0 -> 365 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.pngbin0 -> 183 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.pngbin0 -> 960 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.pngbin0 -> 2463 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.pngbin0 -> 1778 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.pngbin0 -> 4119 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.pngbin0 -> 905 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.pngbin0 -> 181 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.pngbin0 -> 455 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.pngbin0 -> 134 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.pngbin0 -> 195 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.pngbin0 -> 1309 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.pngbin0 -> 1581 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.pngbin0 -> 1586 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.pngbin0 -> 271 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_handle.pngbin0 -> 454 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.pngbin0 -> 1086 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.pngbin0 -> 252 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.pngbin0 -> 112 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.pngbin0 -> 627 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.pngbin0 -> 83 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.pngbin0 -> 210 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.pngbin0 -> 262 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.pngbin0 -> 157 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_remove.pngbin0 -> 728 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.pngbin0 -> 801 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.pngbin0 -> 268 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_star.pngbin0 -> 531 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.pngbin0 -> 746 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.pngbin0 -> 948 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.pngbin0 -> 945 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.pngbin0 -> 1166 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.pngbin0 -> 1192 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.pngbin0 -> 221 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.pngbin0 -> 139 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.pngbin0 -> 251 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.pngbin0 -> 159 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.pngbin0 -> 948 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.pngbin0 -> 4860 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.pngbin0 -> 3352 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.pngbin0 -> 8689 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.pngbin0 -> 1699 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.pngbin0 -> 267 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.pngbin0 -> 627 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.pngbin0 -> 188 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.pngbin0 -> 271 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.pngbin0 -> 2150 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.pngbin0 -> 3154 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.pngbin0 -> 3298 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.pngbin0 -> 479 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.pngbin0 -> 681 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.pngbin0 -> 2237 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.pngbin0 -> 454 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.pngbin0 -> 158 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.pngbin0 -> 755 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.pngbin0 -> 996 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.pngbin0 -> 90 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.pngbin0 -> 368 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.pngbin0 -> 439 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.pngbin0 -> 220 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.pngbin0 -> 1237 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.pngbin0 -> 1376 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.pngbin0 -> 737 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.pngbin0 -> 496 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_star.pngbin0 -> 889 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.pngbin0 -> 1356 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.pngbin0 -> 1794 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.pngbin0 -> 1794 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.pngbin0 -> 2354 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.pngbin0 -> 2339 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.pngbin0 -> 487 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.pngbin0 -> 212 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.pngbin0 -> 455 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.pngbin0 -> 198 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.pngbin0 -> 965 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.pngbin0 -> 6226 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.pngbin0 -> 3686 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.pngbin0 -> 11039 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.pngbin0 -> 3042 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.pngbin0 -> 390 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.pngbin0 -> 1203 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.pngbin0 -> 266 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.pngbin0 -> 323 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.pngbin0 -> 2583 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.pngbin0 -> 3622 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.pngbin0 -> 3229 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.pngbin0 -> 676 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.pngbin0 -> 1431 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.pngbin0 -> 2945 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.pngbin0 -> 631 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.pngbin0 -> 216 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.pngbin0 -> 1112 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.pngbin0 -> 1340 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.pngbin0 -> 92 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.pngbin0 -> 488 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.pngbin0 -> 619 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.pngbin0 -> 283 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.pngbin0 -> 1942 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.pngbin0 -> 2090 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.pngbin0 -> 1107 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.pngbin0 -> 698 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.pngbin0 -> 1539 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.pngbin0 -> 1990 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.pngbin0 -> 2316 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.pngbin0 -> 2319 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.pngbin0 -> 2878 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.pngbin0 -> 2879 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.pngbin0 -> 625 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.pngbin0 -> 291 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.pngbin0 -> 654 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.pngbin0 -> 1148 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.pngbin0 -> 970 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.pngbin0 -> 8761 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.pngbin0 -> 5204 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.pngbin0 -> 3800 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.pngbin0 -> 489 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.pngbin0 -> 1344 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.pngbin0 -> 329 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.pngbin0 -> 1394 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.pngbin0 -> 887 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.pngbin0 -> 1687 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.pngbin0 -> 853 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.pngbin0 -> 305 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.pngbin0 -> 1458 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.pngbin0 -> 1752 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.pngbin0 -> 94 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.pngbin0 -> 636 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.pngbin0 -> 837 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.pngbin0 -> 343 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.pngbin0 -> 2281 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.pngbin0 -> 1478 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.pngbin0 -> 938 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.pngbin0 -> 1389 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.pngbin0 -> 971 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.pngbin0 -> 356 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.pngbin0 -> 878 bytes
-rw-r--r--java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml22
-rw-r--r--java/com/android/dialer/app/res/drawable/floating_action_button.xml25
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_pause.xml31
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_play_arrow.xml32
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_search_phone.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/oval_ripple.xml26
-rw-r--r--java/com/android/dialer/app/res/drawable/overflow_menu.xml20
-rw-r--r--java/com/android/dialer/app/res/drawable/rounded_corner.xml22
-rw-r--r--java/com/android/dialer/app/res/drawable/seekbar_drawable.xml63
-rw-r--r--java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml31
-rw-r--r--java/com/android/dialer/app/res/drawable/shadow_fade_left.xml24
-rw-r--r--java/com/android/dialer/app/res/drawable/shadow_fade_up.xml24
-rw-r--r--java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml90
-rw-r--r--java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml71
-rw-r--r--java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml47
-rw-r--r--java/com/android/dialer/app/res/layout/all_contacts_activity.xml26
-rw-r--r--java/com/android/dialer/app/res/layout/all_contacts_fragment.xml54
-rw-r--r--java/com/android/dialer/app/res/layout/blocked_number_footer.xml38
-rw-r--r--java/com/android/dialer/app/res/layout/blocked_number_fragment.xml30
-rw-r--r--java/com/android/dialer/app/res/layout/blocked_number_header.xml220
-rw-r--r--java/com/android/dialer/app/res/layout/blocked_number_item.xml72
-rw-r--r--java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml22
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail.xml32
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail_footer.xml52
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail_header.xml89
-rw-r--r--java/com/android/dialer/app/res/layout/call_detail_history_item.xml56
-rw-r--r--java/com/android/dialer/app/res/layout/call_log_alert_item.xml22
-rw-r--r--java/com/android/dialer/app/res/layout/call_log_fragment.xml48
-rw-r--r--java/com/android/dialer/app/res/layout/call_log_list_item.xml176
-rw-r--r--java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml230
-rw-r--r--java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml38
-rw-r--r--java/com/android/dialer/app/res/layout/dialpad_fragment.xml78
-rw-r--r--java/com/android/dialer/app/res/layout/dialtacts_activity.xml73
-rw-r--r--java/com/android/dialer/app/res/layout/empty_content_view.xml54
-rw-r--r--java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml56
-rw-r--r--java/com/android/dialer/app/res/layout/keyguard_preview.xml30
-rw-r--r--java/com/android/dialer/app/res/layout/lists_fragment.xml98
-rw-r--r--java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml128
-rw-r--r--java/com/android/dialer/app/res/layout/search_edittext.xml71
-rw-r--r--java/com/android/dialer/app/res/layout/speed_dial_fragment.xml51
-rw-r--r--java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml58
-rw-r--r--java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml115
-rw-r--r--java/com/android/dialer/app/res/menu/dialpad_options.xml30
-rw-r--r--java/com/android/dialer/app/res/menu/dialtacts_options.xml28
-rw-r--r--java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.pngbin0 -> 2780 bytes
-rw-r--r--java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.pngbin0 -> 1778 bytes
-rw-r--r--java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.pngbin0 -> 3939 bytes
-rw-r--r--java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.pngbin0 -> 6251 bytes
-rw-r--r--java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.pngbin0 -> 8793 bytes
-rw-r--r--java/com/android/dialer/app/res/values/animation_constants.xml30
-rw-r--r--java/com/android/dialer/app/res/values/attrs.xml21
-rw-r--r--java/com/android/dialer/app/res/values/colors.xml115
-rw-r--r--java/com/android/dialer/app/res/values/dimens.xml148
-rw-r--r--java/com/android/dialer/app/res/values/donottranslate_config.xml37
-rw-r--r--java/com/android/dialer/app/res/values/ids.xml28
-rw-r--r--java/com/android/dialer/app/res/values/strings.xml960
-rw-r--r--java/com/android/dialer/app/res/values/styles.xml279
-rw-r--r--java/com/android/dialer/app/res/xml/display_options_settings.xml31
-rw-r--r--java/com/android/dialer/app/res/xml/file_paths.xml24
-rw-r--r--java/com/android/dialer/app/res/xml/searchable.xml22
-rw-r--r--java/com/android/dialer/app/res/xml/sound_settings.xml46
-rw-r--r--java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java155
-rw-r--r--java/com/android/dialer/app/settings/DefaultRingtonePreference.java64
-rw-r--r--java/com/android/dialer/app/settings/DialerSettingsActivity.java187
-rw-r--r--java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java30
-rw-r--r--java/com/android/dialer/app/settings/SoundSettingsFragment.java242
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailAudioManager.java252
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailErrorManager.java129
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java449
-rw-r--r--java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java1050
-rw-r--r--java/com/android/dialer/app/voicemail/WiredHeadsetManager.java88
-rw-r--r--java/com/android/dialer/app/voicemail/error/AndroidManifest.xml5
-rw-r--r--java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java177
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java165
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java178
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java45
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatus.java260
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java114
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java25
-rw-r--r--java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java25
-rw-r--r--java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java428
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml114
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml72
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/dimens.xml12
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/strings.xml176
-rw-r--r--java/com/android/dialer/app/voicemail/error/res/values/styles.xml26
-rw-r--r--java/com/android/dialer/app/widget/ActionBarController.java247
-rw-r--r--java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java43
-rw-r--r--java/com/android/dialer/app/widget/EmptyContentView.java121
-rw-r--r--java/com/android/dialer/app/widget/SearchEditTextLayout.java324
-rw-r--r--java/com/android/dialer/backup/AndroidManifest.xml27
-rw-r--r--java/com/android/dialer/backup/DialerBackupAgent.java276
-rw-r--r--java/com/android/dialer/backup/DialerBackupUtils.java320
-rw-r--r--java/com/android/dialer/backup/proto/VoicemailInfo.java377
-rw-r--r--java/com/android/dialer/blocking/AndroidManifest.xml13
-rw-r--r--java/com/android/dialer/blocking/BlockNumberDialogFragment.java328
-rw-r--r--java/com/android/dialer/blocking/BlockReportSpamDialogs.java305
-rw-r--r--java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java110
-rw-r--r--java/com/android/dialer/blocking/BlockedNumbersMigrator.java159
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java428
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberCompat.java320
-rw-r--r--java/com/android/dialer/blocking/FilteredNumberProvider.java176
-rw-r--r--java/com/android/dialer/blocking/FilteredNumbersUtil.java380
-rw-r--r--java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java113
-rw-r--r--java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.pngbin0 -> 478 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.pngbin0 -> 240 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.pngbin0 -> 312 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.pngbin0 -> 335 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.pngbin0 -> 174 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.pngbin0 -> 240 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.pngbin0 -> 665 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.pngbin0 -> 272 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.pngbin0 -> 973 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.pngbin0 -> 340 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.pngbin0 -> 522 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.pngbin0 -> 1295 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.pngbin0 -> 450 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.pngbin0 -> 649 bytes
-rw-r--r--java/com/android/dialer/blocking/res/drawable/blocked_contact.xml36
-rw-r--r--java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml36
-rw-r--r--java/com/android/dialer/blocking/res/values/colors.xml24
-rw-r--r--java/com/android/dialer/blocking/res/values/dimens.xml18
-rw-r--r--java/com/android/dialer/blocking/res/values/strings.xml122
-rw-r--r--java/com/android/dialer/buildtype/BuildType.java62
-rw-r--r--java/com/android/dialer/buildtype/BuildTypeAccessor.java31
-rw-r--r--java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java30
-rw-r--r--java/com/android/dialer/callcomposer/AndroidManifest.xml28
-rw-r--r--java/com/android/dialer/callcomposer/CallComposerActivity.java728
-rw-r--r--java/com/android/dialer/callcomposer/CallComposerFragment.java125
-rw-r--r--java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java57
-rw-r--r--java/com/android/dialer/callcomposer/CameraComposerFragment.java378
-rw-r--r--java/com/android/dialer/callcomposer/GalleryComposerFragment.java256
-rw-r--r--java/com/android/dialer/callcomposer/GalleryCursorLoader.java54
-rw-r--r--java/com/android/dialer/callcomposer/GalleryGridAdapter.java118
-rw-r--r--java/com/android/dialer/callcomposer/GalleryGridItemData.java91
-rw-r--r--java/com/android/dialer/callcomposer/GalleryGridItemView.java126
-rw-r--r--java/com/android/dialer/callcomposer/MessageComposerFragment.java143
-rw-r--r--java/com/android/dialer/callcomposer/camera/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/callcomposer/camera/CameraManager.java822
-rw-r--r--java/com/android/dialer/callcomposer/camera/CameraPreview.java177
-rw-r--r--java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java125
-rw-r--r--java/com/android/dialer/callcomposer/camera/ImagePersistTask.java143
-rw-r--r--java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java120
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java28
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java482
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java97
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java179
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java816
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java153
-rw-r--r--java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml26
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java129
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifData.java89
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java374
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java24
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifParser.java846
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifReader.java81
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/ExifTag.java619
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/IfdData.java126
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/IfdId.java28
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java38
-rw-r--r--java/com/android/dialer/callcomposer/camera/exif/Rational.java70
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml16
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java107
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.pngbin0 -> 2690 bytes
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.pngbin0 -> 1851 bytes
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.pngbin0 -> 3636 bytes
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.pngbin0 -> 5449 bytes
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.pngbin0 -> 7354 bytes
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml26
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml121
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml4
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml22
-rw-r--r--java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml17
-rw-r--r--java/com/android/dialer/callcomposer/nano/CallComposerContact.java220
-rw-r--r--java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml30
-rw-r--r--java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml22
-rw-r--r--java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml22
-rw-r--r--java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml22
-rw-r--r--java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml37
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml147
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml33
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml38
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml79
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml57
-rw-r--r--java/com/android/dialer/callcomposer/res/layout/permission_view.xml52
-rw-r--r--java/com/android/dialer/callcomposer/res/values/colors.xml24
-rw-r--r--java/com/android/dialer/callcomposer/res/values/dimens.xml63
-rw-r--r--java/com/android/dialer/callcomposer/res/values/strings.xml42
-rw-r--r--java/com/android/dialer/callcomposer/res/values/styles.xml50
-rw-r--r--java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java124
-rw-r--r--java/com/android/dialer/callintent/CallIntentBuilder.java108
-rw-r--r--java/com/android/dialer/callintent/CallIntentParser.java54
-rw-r--r--java/com/android/dialer/callintent/Constants.java31
-rw-r--r--java/com/android/dialer/callintent/nano/CallInitiationType.java101
-rw-r--r--java/com/android/dialer/callintent/nano/CallSpecificAppData.java143
-rw-r--r--java/com/android/dialer/common/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/common/Assert.java185
-rw-r--r--java/com/android/dialer/common/AsyncTaskExecutor.java51
-rw-r--r--java/com/android/dialer/common/AsyncTaskExecutors.java91
-rw-r--r--java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java79
-rw-r--r--java/com/android/dialer/common/ConfigProvider.java27
-rw-r--r--java/com/android/dialer/common/ConfigProviderBindings.java68
-rw-r--r--java/com/android/dialer/common/ConfigProviderFactory.java26
-rw-r--r--java/com/android/dialer/common/DpUtil.java31
-rw-r--r--java/com/android/dialer/common/FallibleAsyncTask.java94
-rw-r--r--java/com/android/dialer/common/FragmentUtils.java98
-rw-r--r--java/com/android/dialer/common/LogUtil.java214
-rw-r--r--java/com/android/dialer/common/MathUtil.java57
-rw-r--r--java/com/android/dialer/common/NetworkUtil.java192
-rw-r--r--java/com/android/dialer/common/UiUtil.java41
-rw-r--r--java/com/android/dialer/common/res/values/strings.xml5
-rw-r--r--java/com/android/dialer/compat/ActivityCompat.java29
-rw-r--r--java/com/android/dialer/compat/AppCompatConstants.java33
-rw-r--r--java/com/android/dialer/compat/CompatUtils.java222
-rw-r--r--java/com/android/dialer/compat/PathInterpolatorCompat.java120
-rw-r--r--java/com/android/dialer/compat/SdkVersionOverride.java43
-rw-r--r--java/com/android/dialer/constants/Constants.java47
-rw-r--r--java/com/android/dialer/constants/ScheduledJobIds.java31
-rw-r--r--java/com/android/dialer/constants/aospdialer/ConstantsImpl.java37
-rw-r--r--java/com/android/dialer/database/CallLogQueryHandler.java369
-rw-r--r--java/com/android/dialer/database/Database.java49
-rw-r--r--java/com/android/dialer/database/DatabaseBindings.java25
-rw-r--r--java/com/android/dialer/database/DatabaseBindingsFactory.java26
-rw-r--r--java/com/android/dialer/database/DatabaseBindingsStub.java35
-rw-r--r--java/com/android/dialer/database/DialerDatabaseHelper.java1242
-rw-r--r--java/com/android/dialer/database/FilteredNumberContract.java137
-rw-r--r--java/com/android/dialer/database/VoicemailStatusQuery.java91
-rw-r--r--java/com/android/dialer/debug/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/debug/bindings/impl/DebugBindings.java32
-rw-r--r--java/com/android/dialer/debug/impl/AndroidManifest.xml18
-rw-r--r--java/com/android/dialer/debug/impl/DebugConnection.java55
-rw-r--r--java/com/android/dialer/debug/impl/DebugConnectionService.java103
-rw-r--r--java/com/android/dialer/dialpadview/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/dialpadview/DialpadKeyButton.java231
-rw-r--r--java/com/android/dialer/dialpadview/DialpadTextView.java71
-rw-r--r--java/com/android/dialer/dialpadview/DialpadView.java464
-rw-r--r--java/com/android/dialer/dialpadview/DigitsEditText.java57
-rw-r--r--java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml19
-rw-r--r--java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml22
-rw-r--r--java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml20
-rw-r--r--java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml19
-rw-r--r--java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml22
-rw-r--r--java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml20
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.pngbin0 -> 3273 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.pngbin0 -> 2798 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.pngbin0 -> 875 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.pngbin0 -> 207 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.pngbin0 -> 805 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.pngbin0 -> 623 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.pngbin0 -> 503 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.pngbin0 -> 1945 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.pngbin0 -> 1845 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.pngbin0 -> 698 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.pngbin0 -> 164 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.pngbin0 -> 669 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.pngbin0 -> 504 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.pngbin0 -> 424 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.pngbin0 -> 4872 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.pngbin0 -> 4092 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.pngbin0 -> 1266 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.pngbin0 -> 235 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.pngbin0 -> 1110 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.pngbin0 -> 787 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.pngbin0 -> 550 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.pngbin0 -> 8621 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.pngbin0 -> 7004 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.pngbin0 -> 2321 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.pngbin0 -> 309 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.pngbin0 -> 1745 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.pngbin0 -> 1578 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.pngbin0 -> 1384 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.pngbin0 -> 12782 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.pngbin0 -> 9900 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.pngbin0 -> 2921 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.pngbin0 -> 2128 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.pngbin0 -> 1829 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.pngbin0 -> 1785 bytes
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml18
-rw-r--r--java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml7
-rw-r--r--java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml44
-rw-r--r--java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml44
-rw-r--r--java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml33
-rw-r--r--java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml33
-rw-r--r--java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml44
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad.xml99
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml35
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml41
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml26
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml26
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml37
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml23
-rw-r--r--java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml153
-rw-r--r--java/com/android/dialer/dialpadview/res/values-land/dimens.xml27
-rw-r--r--java/com/android/dialer/dialpadview/res/values-land/styles.xml37
-rw-r--r--java/com/android/dialer/dialpadview/res/values/animation_constants.xml20
-rw-r--r--java/com/android/dialer/dialpadview/res/values/attrs.xml39
-rw-r--r--java/com/android/dialer/dialpadview/res/values/colors.xml27
-rw-r--r--java/com/android/dialer/dialpadview/res/values/dimens.xml48
-rw-r--r--java/com/android/dialer/dialpadview/res/values/strings.xml53
-rw-r--r--java/com/android/dialer/dialpadview/res/values/styles.xml118
-rw-r--r--java/com/android/dialer/disabled_lint_checks.txt1
-rw-r--r--java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java76
-rw-r--r--java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java127
-rw-r--r--java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java36
-rw-r--r--java/com/android/dialer/enrichedcall/EnrichedCallManager.java225
-rw-r--r--java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java84
-rw-r--r--java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java94
-rw-r--r--java/com/android/dialer/enrichedcall/Session.java63
-rw-r--r--java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java32
-rw-r--r--java/com/android/dialer/enrichedcall/extensions/StateExtension.java54
-rw-r--r--java/com/android/dialer/inject/ApplicationModule.java39
-rw-r--r--java/com/android/dialer/inject/DialerAppComponent.java29
-rw-r--r--java/com/android/dialer/interactions/AndroidManifest.xml20
-rw-r--r--java/com/android/dialer/interactions/ContactUpdateService.java48
-rw-r--r--java/com/android/dialer/interactions/PhoneNumberInteraction.java557
-rw-r--r--java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java107
-rw-r--r--java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml43
-rw-r--r--java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml32
-rw-r--r--java/com/android/dialer/interactions/res/values/strings.xml29
-rw-r--r--java/com/android/dialer/logging/Logger.java49
-rw-r--r--java/com/android/dialer/logging/LoggingBindings.java59
-rw-r--r--java/com/android/dialer/logging/LoggingBindingsFactory.java24
-rw-r--r--java/com/android/dialer/logging/LoggingBindingsStub.java36
-rw-r--r--java/com/android/dialer/logging/nano/ContactLookupResult.java91
-rw-r--r--java/com/android/dialer/logging/nano/ContactSource.java90
-rw-r--r--java/com/android/dialer/logging/nano/DialerImpression.java178
-rw-r--r--java/com/android/dialer/logging/nano/InteractionEvent.java95
-rw-r--r--java/com/android/dialer/logging/nano/ReportingLocation.java87
-rw-r--r--java/com/android/dialer/logging/nano/ScreenEvent.java104
-rw-r--r--java/com/android/dialer/multimedia/AutoValue_MultimediaData.java165
-rw-r--r--java/com/android/dialer/multimedia/MultimediaData.java100
-rw-r--r--java/com/android/dialer/p13n/inference/P13nRanking.java75
-rw-r--r--java/com/android/dialer/p13n/inference/protocol/P13nRanker.java75
-rw-r--r--java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java26
-rw-r--r--java/com/android/dialer/p13n/logging/P13nLogger.java35
-rw-r--r--java/com/android/dialer/p13n/logging/P13nLoggerFactory.java29
-rw-r--r--java/com/android/dialer/p13n/logging/P13nLogging.java60
-rw-r--r--java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java77
-rw-r--r--java/com/android/dialer/phonenumbercache/CallLogQuery.java107
-rw-r--r--java/com/android/dialer/phonenumbercache/ContactInfo.java165
-rw-r--r--java/com/android/dialer/phonenumbercache/ContactInfoHelper.java586
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java40
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCache.java50
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java26
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java26
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java29
-rw-r--r--java/com/android/dialer/phonenumbercache/PhoneQuery.java96
-rw-r--r--java/com/android/dialer/phonenumberutil/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java276
-rw-r--r--java/com/android/dialer/phonenumberutil/res/values/strings.xml27
-rw-r--r--java/com/android/dialer/proguard/UsedByReflection.java34
-rw-r--r--java/com/android/dialer/protos/ProtoParsers.java167
-rw-r--r--java/com/android/dialer/shortcuts/AndroidManifest.xml50
-rw-r--r--java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java161
-rw-r--r--java/com/android/dialer/shortcuts/CallContactActivity.java133
-rw-r--r--java/com/android/dialer/shortcuts/DialerShortcut.java190
-rw-r--r--java/com/android/dialer/shortcuts/DynamicShortcuts.java243
-rw-r--r--java/com/android/dialer/shortcuts/IconFactory.java112
-rw-r--r--java/com/android/dialer/shortcuts/PeriodicJobService.java118
-rw-r--r--java/com/android/dialer/shortcuts/PinnedShortcuts.java159
-rw-r--r--java/com/android/dialer/shortcuts/RefreshShortcutsTask.java71
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutInfoFactory.java100
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutRefresher.java86
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutUsageReporter.java132
-rw-r--r--java/com/android/dialer/shortcuts/Shortcuts.java34
-rw-r--r--java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java48
-rw-r--r--java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml39
-rw-r--r--java/com/android/dialer/shortcuts/res/values/colors.xml20
-rw-r--r--java/com/android/dialer/shortcuts/res/values/dimens.xml19
-rw-r--r--java/com/android/dialer/shortcuts/res/values/strings.xml37
-rw-r--r--java/com/android/dialer/shortcuts/res/values/themes.xml39
-rw-r--r--java/com/android/dialer/shortcuts/res/xml/shortcuts.xml31
-rw-r--r--java/com/android/dialer/simulator/Simulator.java27
-rw-r--r--java/com/android/dialer/simulator/impl/AndroidManifest.xml18
-rw-r--r--java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java160
-rw-r--r--java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java231
-rw-r--r--java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java184
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorActionProvider.java88
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorCallLog.java139
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorConnection.java56
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorConnectionService.java87
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorContacts.java319
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorModule.java34
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java47
-rw-r--r--java/com/android/dialer/simulator/impl/SimulatorVoicemail.java154
-rw-r--r--java/com/android/dialer/smartdial/LatinSmartDialMap.java784
-rw-r--r--java/com/android/dialer/smartdial/SmartDialMap.java60
-rw-r--r--java/com/android/dialer/smartdial/SmartDialMatchPosition.java70
-rw-r--r--java/com/android/dialer/smartdial/SmartDialNameMatcher.java434
-rw-r--r--java/com/android/dialer/smartdial/SmartDialPrefix.java605
-rw-r--r--java/com/android/dialer/spam/Spam.java49
-rw-r--r--java/com/android/dialer/spam/SpamBindings.java146
-rw-r--r--java/com/android/dialer/spam/SpamBindingsFactory.java26
-rw-r--r--java/com/android/dialer/spam/SpamBindingsStub.java92
-rw-r--r--java/com/android/dialer/telecom/TelecomUtil.java212
-rw-r--r--java/com/android/dialer/theme/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml14
-rw-r--r--java/com/android/dialer/theme/res/animator/activated_button_elevation.xml21
-rw-r--r--java/com/android/dialer/theme/res/animator/button_elevation.xml21
-rw-r--r--java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml75
-rw-r--r--java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml8
-rw-r--r--java/com/android/dialer/theme/res/values/colors.xml64
-rw-r--r--java/com/android/dialer/theme/res/values/dimens.xml28
-rw-r--r--java/com/android/dialer/theme/res/values/strings.xml27
-rw-r--r--java/com/android/dialer/theme/res/values/styles.xml56
-rw-r--r--java/com/android/dialer/theme/res/values/themes.xml21
-rw-r--r--java/com/android/dialer/util/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/util/CallUtil.java135
-rw-r--r--java/com/android/dialer/util/DialerUtils.java246
-rw-r--r--java/com/android/dialer/util/DrawableConverter.java97
-rw-r--r--java/com/android/dialer/util/ExpirableCache.java269
-rw-r--r--java/com/android/dialer/util/IntentUtil.java78
-rw-r--r--java/com/android/dialer/util/MoreStrings.java64
-rw-r--r--java/com/android/dialer/util/OrientationUtil.java30
-rw-r--r--java/com/android/dialer/util/PermissionsUtil.java121
-rw-r--r--java/com/android/dialer/util/SettingsUtil.java95
-rw-r--r--java/com/android/dialer/util/TouchPointManager.java60
-rw-r--r--java/com/android/dialer/util/TransactionSafeActivity.java64
-rw-r--r--java/com/android/dialer/util/ViewUtil.java129
-rw-r--r--java/com/android/dialer/util/res/values/strings.xml42
-rw-r--r--java/com/android/dialer/voicemailstatus/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java111
-rw-r--r--java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java96
-rw-r--r--java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java278
-rw-r--r--java/com/android/dialer/voicemailstatus/res/values/strings.xml41
-rw-r--r--java/com/android/dialer/widget/AndroidManifest.xml3
-rw-r--r--java/com/android/dialer/widget/ResizingTextEditText.java51
-rw-r--r--java/com/android/dialer/widget/ResizingTextTextView.java51
-rw-r--r--java/com/android/dialer/widget/res/values/attrs.xml23
-rw-r--r--java/com/android/incallui/AccelerometerListener.java173
-rw-r--r--java/com/android/incallui/AndroidManifest.xml121
-rw-r--r--java/com/android/incallui/AnswerScreenPresenter.java110
-rw-r--r--java/com/android/incallui/AnswerScreenPresenterStub.java44
-rw-r--r--java/com/android/incallui/AudioModeProvider.java69
-rw-r--r--java/com/android/incallui/Bindings.java52
-rw-r--r--java/com/android/incallui/CallButtonPresenter.java515
-rw-r--r--java/com/android/incallui/CallCardPresenter.java1110
-rw-r--r--java/com/android/incallui/CallerInfo.java573
-rw-r--r--java/com/android/incallui/CallerInfoAsyncQuery.java638
-rw-r--r--java/com/android/incallui/CallerInfoUtils.java279
-rw-r--r--java/com/android/incallui/ConferenceManagerFragment.java106
-rw-r--r--java/com/android/incallui/ConferenceManagerPresenter.java139
-rw-r--r--java/com/android/incallui/ConferenceParticipantListAdapter.java523
-rw-r--r--java/com/android/incallui/ContactInfoCache.java759
-rw-r--r--java/com/android/incallui/ContactsAsyncHelper.java269
-rw-r--r--java/com/android/incallui/ContactsPreferencesFactory.java56
-rw-r--r--java/com/android/incallui/DialpadFragment.java461
-rw-r--r--java/com/android/incallui/DialpadPresenter.java91
-rw-r--r--java/com/android/incallui/ExternalCallNotifier.java465
-rw-r--r--java/com/android/incallui/InCallActivity.java756
-rw-r--r--java/com/android/incallui/InCallActivityCommon.java820
-rw-r--r--java/com/android/incallui/InCallCameraManager.java173
-rw-r--r--java/com/android/incallui/InCallOrientationEventListener.java194
-rw-r--r--java/com/android/incallui/InCallPresenter.java1679
-rw-r--r--java/com/android/incallui/InCallServiceImpl.java99
-rw-r--r--java/com/android/incallui/InCallUIMaterialColorMapUtils.java67
-rw-r--r--java/com/android/incallui/Log.java145
-rw-r--r--java/com/android/incallui/ManageConferenceActivity.java86
-rw-r--r--java/com/android/incallui/NotificationBroadcastReceiver.java165
-rw-r--r--java/com/android/incallui/PostCharDialogFragment.java96
-rw-r--r--java/com/android/incallui/ProximitySensor.java292
-rw-r--r--java/com/android/incallui/StatusBarNotifier.java842
-rw-r--r--java/com/android/incallui/ThemeColorManager.java142
-rw-r--r--java/com/android/incallui/TransactionSafeFragmentActivity.java64
-rw-r--r--java/com/android/incallui/VideoCallPresenter.java1289
-rw-r--r--java/com/android/incallui/VideoPauseController.java416
-rw-r--r--java/com/android/incallui/answer/bindings/AnswerBindings.java29
-rw-r--r--java/com/android/incallui/answer/impl/AffordanceHolderLayout.java178
-rw-r--r--java/com/android/incallui/answer/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/answer/impl/AnswerFragment.java981
-rw-r--r--java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java127
-rw-r--r--java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java137
-rw-r--r--java/com/android/incallui/answer/impl/PillDrawable.java43
-rw-r--r--java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java136
-rw-r--r--java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java642
-rw-r--r--java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java505
-rw-r--r--java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml23
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java45
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java52
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java47
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java1149
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java496
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java268
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml19
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml6
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml115
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml97
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml20
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml21
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml20
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml27
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml5
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml14
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml7
-rw-r--r--java/com/android/incallui/answer/impl/answermethod/res/values/values.xml25
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java99
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java193
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java33
-rw-r--r--java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Classifier.java35
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ClassifierData.java96
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java37
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java23
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java35
-rw-r--r--java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java43
-rw-r--r--java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java42
-rw-r--r--java/com/android/incallui/answer/impl/classifier/FalsingManager.java140
-rw-r--r--java/com/android/incallui/answer/impl/classifier/GestureClassifier.java31
-rw-r--r--java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java115
-rw-r--r--java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java142
-rw-r--r--java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java45
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Point.java95
-rw-r--r--java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java51
-rw-r--r--java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java23
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java97
-rw-r--r--java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java28
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java147
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java33
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java40
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java39
-rw-r--r--java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java36
-rw-r--r--java/com/android/incallui/answer/impl/classifier/Stroke.java72
-rw-r--r--java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java28
-rw-r--r--java/com/android/incallui/answer/impl/hint/AndroidManifest.xml13
-rw-r--r--java/com/android/incallui/answer/impl/hint/AnswerHint.java46
-rw-r--r--java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java133
-rw-r--r--java/com/android/incallui/answer/impl/hint/DotAnswerHint.java283
-rw-r--r--java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java39
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventAnswerHint.java235
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java30
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java118
-rw-r--r--java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java67
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml4
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml4
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml5
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml30
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml36
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/values/dimens.xml12
-rw-r--r--java/com/android/incallui/answer/impl/hint/res/values/strings.xml5
-rw-r--r--java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml19
-rw-r--r--java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml9
-rw-r--r--java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml26
-rw-r--r--java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml14
-rw-r--r--java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml152
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml21
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml20
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml22
-rw-r--r--java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml21
-rw-r--r--java/com/android/incallui/answer/impl/res/values/dimens.xml25
-rw-r--r--java/com/android/incallui/answer/impl/res/values/strings.xml26
-rw-r--r--java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java293
-rw-r--r--java/com/android/incallui/answer/impl/utils/Interpolators.java30
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreen.java38
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java44
-rw-r--r--java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java23
-rw-r--r--java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java150
-rw-r--r--java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java37
-rw-r--r--java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java85
-rw-r--r--java/com/android/incallui/answerproximitysensor/PseudoScreenState.java66
-rw-r--r--java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java90
-rw-r--r--java/com/android/incallui/async/PausableExecutor.java56
-rw-r--r--java/com/android/incallui/async/PausableExecutorImpl.java40
-rw-r--r--java/com/android/incallui/audioroute/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java114
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 990 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 632 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 1297 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.pngbin0 -> 1979 bytes
-rw-r--r--java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml37
-rw-r--r--java/com/android/incallui/audioroute/res/values/strings.xml7
-rw-r--r--java/com/android/incallui/audioroute/res/values/styles.xml14
-rw-r--r--java/com/android/incallui/autoresizetext/AndroidManifest.xml25
-rw-r--r--java/com/android/incallui/autoresizetext/AutoResizeTextView.java316
-rw-r--r--java/com/android/incallui/autoresizetext/res/values/attrs.xml47
-rw-r--r--java/com/android/incallui/baseui/BaseFragment.java75
-rw-r--r--java/com/android/incallui/baseui/Presenter.java54
-rw-r--r--java/com/android/incallui/baseui/Ui.java20
-rw-r--r--java/com/android/incallui/bindings/ContactUtils.java33
-rw-r--r--java/com/android/incallui/bindings/DistanceHelper.java36
-rw-r--r--java/com/android/incallui/bindings/InCallUiBindings.java48
-rw-r--r--java/com/android/incallui/bindings/InCallUiBindingsFactory.java26
-rw-r--r--java/com/android/incallui/bindings/InCallUiBindingsStub.java81
-rw-r--r--java/com/android/incallui/bindings/PhoneNumberService.java77
-rw-r--r--java/com/android/incallui/call/CallList.java763
-rw-r--r--java/com/android/incallui/call/DialerCall.java1401
-rw-r--r--java/com/android/incallui/call/DialerCallDelegate.java25
-rw-r--r--java/com/android/incallui/call/DialerCallListener.java39
-rw-r--r--java/com/android/incallui/call/ExternalCallList.java136
-rw-r--r--java/com/android/incallui/call/InCallServiceListener.java40
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindings.java26
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java26
-rw-r--r--java/com/android/incallui/call/InCallUiLegacyBindingsStub.java24
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallback.java197
-rw-r--r--java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java279
-rw-r--r--java/com/android/incallui/call/TelecomAdapter.java160
-rw-r--r--java/com/android/incallui/call/VideoUtils.java151
-rw-r--r--java/com/android/incallui/commontheme/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/commontheme/res/animator/button_state.xml30
-rw-r--r--java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml22
-rw-r--r--java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml5
-rw-r--r--java/com/android/incallui/commontheme/res/color/incall_button_white.xml5
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.pngbin0 -> 1010 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.pngbin0 -> 682 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.pngbin0 -> 1362 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.pngbin0 -> 2259 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.pngbin0 -> 3156 bytes
-rw-r--r--java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml10
-rw-r--r--java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml10
-rw-r--r--java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml10
-rw-r--r--java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml21
-rw-r--r--java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml21
-rw-r--r--java/com/android/incallui/commontheme/res/values/colors.xml5
-rw-r--r--java/com/android/incallui/commontheme/res/values/dimens.xml22
-rw-r--r--java/com/android/incallui/commontheme/res/values/strings.xml35
-rw-r--r--java/com/android/incallui/commontheme/res/values/styles.xml58
-rw-r--r--java/com/android/incallui/contactgrid/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/contactgrid/BottomRow.java142
-rw-r--r--java/com/android/incallui/contactgrid/ContactGridManager.java315
-rw-r--r--java/com/android/incallui/contactgrid/TopRow.java168
-rw-r--r--java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml71
-rw-r--r--java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml26
-rw-r--r--java/com/android/incallui/contactgrid/res/values/ids.xml31
-rw-r--r--java/com/android/incallui/contactgrid/res/values/strings.xml69
-rw-r--r--java/com/android/incallui/hold/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/hold/OnHoldFragment.java102
-rw-r--r--java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml46
-rw-r--r--java/com/android/incallui/hold/res/values/strings.xml6
-rw-r--r--java/com/android/incallui/incall/bindings/InCallBindings.java28
-rw-r--r--java/com/android/incallui/incall/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java135
-rw-r--r--java/com/android/incallui/incall/impl/ButtonChooser.java114
-rw-r--r--java/com/android/incallui/incall/impl/ButtonChooserFactory.java100
-rw-r--r--java/com/android/incallui/incall/impl/ButtonController.java584
-rw-r--r--java/com/android/incallui/incall/impl/CheckableLabeledButton.java286
-rw-r--r--java/com/android/incallui/incall/impl/InCallButtonGridFragment.java137
-rw-r--r--java/com/android/incallui/incall/impl/InCallFragment.java501
-rw-r--r--java/com/android/incallui/incall/impl/InCallPagerAdapter.java59
-rw-r--r--java/com/android/incallui/incall/impl/MappedButtonConfig.java193
-rw-r--r--java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml31
-rw-r--r--java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.pngbin0 -> 708 bytes
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.pngbin0 -> 1259 bytes
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml22
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml30
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml12
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml12
-rw-r--r--java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml6
-rw-r--r--java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml15
-rw-r--r--java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml104
-rw-r--r--java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml77
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml4
-rw-r--r--java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml24
-rw-r--r--java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml7
-rw-r--r--java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml5
-rw-r--r--java/com/android/incallui/incall/impl/res/values/attrs.xml8
-rw-r--r--java/com/android/incallui/incall/impl/res/values/dimens.xml17
-rw-r--r--java/com/android/incallui/incall/impl/res/values/ids.xml6
-rw-r--r--java/com/android/incallui/incall/impl/res/values/strings.xml56
-rw-r--r--java/com/android/incallui/incall/impl/res/values/styles.xml23
-rw-r--r--java/com/android/incallui/incall/protocol/ContactPhotoType.java35
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonIds.java59
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java61
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUi.java50
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java67
-rw-r--r--java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java23
-rw-r--r--java/com/android/incallui/incall/protocol/InCallScreen.java53
-rw-r--r--java/com/android/incallui/incall/protocol/InCallScreenDelegate.java43
-rw-r--r--java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java23
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryCallState.java114
-rw-r--r--java/com/android/incallui/incall/protocol/PrimaryInfo.java112
-rw-r--r--java/com/android/incallui/incall/protocol/SecondaryInfo.java109
-rw-r--r--java/com/android/incallui/latencyreport/LatencyReport.java140
-rw-r--r--java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java105
-rw-r--r--java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java124
-rw-r--r--java/com/android/incallui/maps/StaticMapBinding.java51
-rw-r--r--java/com/android/incallui/maps/StaticMapFactory.java28
-rw-r--r--java/com/android/incallui/res/anim/activity_open_enter.xml43
-rw-r--r--java/com/android/incallui/res/anim/activity_open_exit.xml31
-rw-r--r--java/com/android/incallui/res/anim/decelerate_cubic.xml21
-rw-r--r--java/com/android/incallui/res/anim/decelerate_quint.xml21
-rw-r--r--java/com/android/incallui/res/anim/on_going_call.xml31
-rw-r--r--java/com/android/incallui/res/color/ota_title_color.xml21
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.pngbin0 -> 518 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.pngbin0 -> 454 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.pngbin0 -> 326 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.pngbin0 -> 225 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.pngbin0 -> 371 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.pngbin0 -> 650 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.pngbin0 -> 803 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.pngbin0 -> 1009 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.pngbin0 -> 946 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.pngbin0 -> 856 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.pngbin0 -> 577 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.pngbin0 -> 300 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.pngbin0 -> 458 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_question_mark.pngbin0 -> 845 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.pngbin0 -> 575 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_business.pngbin0 -> 3311 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_conference.pngbin0 -> 7037 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_no_image.pngbin0 -> 5362 bytes
-rw-r--r--java/com/android/incallui/res/drawable-hdpi/img_phone.pngbin0 -> 6157 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.pngbin0 -> 348 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.pngbin0 -> 315 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.pngbin0 -> 256 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.pngbin0 -> 178 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.pngbin0 -> 265 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.pngbin0 -> 401 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.pngbin0 -> 501 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.pngbin0 -> 638 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.pngbin0 -> 572 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.pngbin0 -> 548 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.pngbin0 -> 375 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.pngbin0 -> 211 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.pngbin0 -> 346 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_question_mark.pngbin0 -> 569 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_business.pngbin0 -> 2240 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_conference.pngbin0 -> 4629 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_no_image.pngbin0 -> 3509 bytes
-rw-r--r--java/com/android/incallui/res/drawable-mdpi/img_phone.pngbin0 -> 3798 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.pngbin0 -> 690 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.pngbin0 -> 534 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.pngbin0 -> 377 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.pngbin0 -> 261 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.pngbin0 -> 456 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.pngbin0 -> 806 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.pngbin0 -> 1017 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.pngbin0 -> 1313 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.pngbin0 -> 1218 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.pngbin0 -> 1098 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.pngbin0 -> 730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.pngbin0 -> 341 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.pngbin0 -> 584 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.pngbin0 -> 1094 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.pngbin0 -> 737 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_business.pngbin0 -> 4759 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_conference.pngbin0 -> 9517 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_no_image.pngbin0 -> 7369 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xhdpi/img_phone.pngbin0 -> 8189 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.pngbin0 -> 1029 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.pngbin0 -> 736 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.pngbin0 -> 461 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.pngbin0 -> 353 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.pngbin0 -> 675 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.pngbin0 -> 1198 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.pngbin0 -> 1524 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.pngbin0 -> 2045 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.pngbin0 -> 1900 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.pngbin0 -> 1675 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.pngbin0 -> 1051 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.pngbin0 -> 485 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.pngbin0 -> 842 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.pngbin0 -> 1686 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.pngbin0 -> 1107 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_business.pngbin0 -> 6499 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_conference.pngbin0 -> 16306 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_no_image.pngbin0 -> 9850 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxhdpi/img_phone.pngbin0 -> 10848 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.pngbin0 -> 1353 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.pngbin0 -> 929 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.pngbin0 -> 646 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.pngbin0 -> 444 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.pngbin0 -> 869 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.pngbin0 -> 638 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.pngbin0 -> 2304 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.pngbin0 -> 1478 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_business.pngbin0 -> 10730 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_conference.pngbin0 -> 19584 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.pngbin0 -> 16251 bytes
-rw-r--r--java/com/android/incallui/res/drawable-xxxhdpi/img_phone.pngbin0 -> 18635 bytes
-rw-r--r--java/com/android/incallui/res/drawable/img_conference_automirrored.xml21
-rw-r--r--java/com/android/incallui/res/drawable/img_no_image_automirrored.xml21
-rw-r--r--java/com/android/incallui/res/drawable/incall_background_gradient.xml8
-rw-r--r--java/com/android/incallui/res/drawable/spam_notification_icon.xml34
-rw-r--r--java/com/android/incallui/res/drawable/unknown_notification_icon.xml34
-rw-r--r--java/com/android/incallui/res/layout/activity_manage_conference.xml6
-rw-r--r--java/com/android/incallui/res/layout/caller_in_conference.xml119
-rw-r--r--java/com/android/incallui/res/layout/conference_manager_fragment.xml33
-rw-r--r--java/com/android/incallui/res/layout/incall_dialpad_fragment.xml24
-rw-r--r--java/com/android/incallui/res/layout/incall_screen.xml33
-rw-r--r--java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml28
-rw-r--r--java/com/android/incallui/res/values-sw360dp/dimens.xml32
-rw-r--r--java/com/android/incallui/res/values-w500dp-land/colors.xml21
-rw-r--r--java/com/android/incallui/res/values-w500dp-land/dimens.xml23
-rw-r--r--java/com/android/incallui/res/values/animation_constants.xml19
-rw-r--r--java/com/android/incallui/res/values/colors.xml92
-rw-r--r--java/com/android/incallui/res/values/config.xml23
-rw-r--r--java/com/android/incallui/res/values/dimens.xml66
-rw-r--r--java/com/android/incallui/res/values/strings.xml367
-rw-r--r--java/com/android/incallui/res/values/styles.xml80
-rw-r--r--java/com/android/incallui/ringtone/DialerRingtoneManager.java134
-rw-r--r--java/com/android/incallui/ringtone/InCallTonePlayer.java168
-rw-r--r--java/com/android/incallui/ringtone/ToneGeneratorFactory.java34
-rw-r--r--java/com/android/incallui/sessiondata/AndroidManifest.xml18
-rw-r--r--java/com/android/incallui/sessiondata/AvatarPresenter.java31
-rw-r--r--java/com/android/incallui/sessiondata/MultimediaFragment.java231
-rw-r--r--java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml22
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml42
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml50
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml59
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml43
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml61
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml62
-rw-r--r--java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml78
-rw-r--r--java/com/android/incallui/sessiondata/res/values/dimens.xml21
-rw-r--r--java/com/android/incallui/sessiondata/res/values/ids.xml23
-rw-r--r--java/com/android/incallui/sessiondata/res/values/styles.xml24
-rw-r--r--java/com/android/incallui/spam/NumberInCallHistoryTask.java107
-rw-r--r--java/com/android/incallui/spam/SpamCallListListener.java364
-rw-r--r--java/com/android/incallui/spam/SpamNotificationActivity.java483
-rw-r--r--java/com/android/incallui/spam/SpamNotificationService.java132
-rw-r--r--java/com/android/incallui/util/AccessibilityUtil.java35
-rw-r--r--java/com/android/incallui/util/TelecomCallUtil.java51
-rw-r--r--java/com/android/incallui/video/bindings/VideoBindings.java28
-rw-r--r--java/com/android/incallui/video/impl/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java62
-rw-r--r--java/com/android/incallui/video/impl/CheckableImageButton.java222
-rw-r--r--java/com/android/incallui/video/impl/SpeakerButtonController.java118
-rw-r--r--java/com/android/incallui/video/impl/SwitchOnHoldCallController.java91
-rw-r--r--java/com/android/incallui/video/impl/VideoCallFragment.java1215
-rw-r--r--java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml5
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.pngbin0 -> 1930 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.pngbin0 -> 3103 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.pngbin0 -> 3304 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.pngbin0 -> 4836 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.pngbin0 -> 4209 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.pngbin0 -> 4022 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.pngbin0 -> 5695 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.pngbin0 -> 1293 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.pngbin0 -> 1426 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.pngbin0 -> 1715 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.pngbin0 -> 2724 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.pngbin0 -> 2155 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.pngbin0 -> 1990 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.pngbin0 -> 3188 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.pngbin0 -> 2518 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.pngbin0 -> 4603 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.pngbin0 -> 4957 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.pngbin0 -> 7213 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.pngbin0 -> 6352 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.pngbin0 -> 6054 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.pngbin0 -> 8418 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.pngbin0 -> 4001 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.pngbin0 -> 9032 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.pngbin0 -> 8611 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.pngbin0 -> 13529 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.pngbin0 -> 11101 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.pngbin0 -> 10736 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.pngbin0 -> 15167 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.pngbin0 -> 2424 bytes
-rw-r--r--java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml10
-rw-r--r--java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml27
-rw-r--r--java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml6
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall.xml114
-rw-r--r--java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml111
-rw-r--r--java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml6
-rw-r--r--java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml33
-rw-r--r--java/com/android/incallui/video/impl/res/layout/videocall_controls.xml113
-rw-r--r--java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml115
-rw-r--r--java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml7
-rw-r--r--java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml7
-rw-r--r--java/com/android/incallui/video/impl/res/values/attrs.xml8
-rw-r--r--java/com/android/incallui/video/impl/res/values/dimens.xml10
-rw-r--r--java/com/android/incallui/video/impl/res/values/strings.xml28
-rw-r--r--java/com/android/incallui/video/impl/res/values/styles.xml11
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreen.java36
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java48
-rw-r--r--java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java23
-rw-r--r--java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java44
-rw-r--r--java/com/android/incallui/videosurface/impl/VideoScale.java147
-rw-r--r--java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java249
-rw-r--r--java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java29
-rw-r--r--java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java57
-rw-r--r--java/com/android/incallui/wifi/AndroidManifest.xml3
-rw-r--r--java/com/android/incallui/wifi/EnableWifiCallingPrompt.java82
-rw-r--r--java/com/android/incallui/wifi/res/values/strings.xml9
-rw-r--r--java/com/android/voicemailomtp/ActivationTask.java305
-rw-r--r--java/com/android/voicemailomtp/AndroidManifest.xml105
-rw-r--r--java/com/android/voicemailomtp/Assert.java62
-rw-r--r--java/com/android/voicemailomtp/DefaultOmtpEventHandler.java202
-rw-r--r--java/com/android/voicemailomtp/NeededForTesting.java25
-rw-r--r--java/com/android/voicemailomtp/OmtpConstants.java248
-rw-r--r--java/com/android/voicemailomtp/OmtpEvents.java156
-rw-r--r--java/com/android/voicemailomtp/OmtpService.java65
-rw-r--r--java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java423
-rw-r--r--java/com/android/voicemailomtp/SubscriptionInfoHelper.java75
-rw-r--r--java/com/android/voicemailomtp/TelephonyManagerStub.java80
-rw-r--r--java/com/android/voicemailomtp/TelephonyVvmConfigManager.java154
-rw-r--r--java/com/android/voicemailomtp/VisualVoicemailPreferences.java143
-rw-r--r--java/com/android/voicemailomtp/Voicemail.java330
-rw-r--r--java/com/android/voicemailomtp/VoicemailStatus.java158
-rw-r--r--java/com/android/voicemailomtp/VvmLog.java179
-rw-r--r--java/com/android/voicemailomtp/VvmPackageInstallReceiver.java70
-rw-r--r--java/com/android/voicemailomtp/VvmPhoneStateListener.java103
-rw-r--r--java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java219
-rw-r--r--java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java101
-rw-r--r--java/com/android/voicemailomtp/imap/ImapHelper.java711
-rw-r--r--java/com/android/voicemailomtp/imap/VoicemailPayload.java38
-rw-r--r--java/com/android/voicemailomtp/mail/Address.java541
-rw-r--r--java/com/android/voicemailomtp/mail/AuthenticationFailedException.java33
-rw-r--r--java/com/android/voicemailomtp/mail/Base64Body.java62
-rw-r--r--java/com/android/voicemailomtp/mail/Body.java25
-rw-r--r--java/com/android/voicemailomtp/mail/BodyPart.java24
-rw-r--r--java/com/android/voicemailomtp/mail/CertificateValidationException.java29
-rw-r--r--java/com/android/voicemailomtp/mail/FetchProfile.java84
-rw-r--r--java/com/android/voicemailomtp/mail/Fetchable.java23
-rw-r--r--java/com/android/voicemailomtp/mail/FixedLengthInputStream.java79
-rw-r--r--java/com/android/voicemailomtp/mail/Flag.java29
-rw-r--r--java/com/android/voicemailomtp/mail/MailTransport.java344
-rw-r--r--java/com/android/voicemailomtp/mail/MeetingInfo.java29
-rw-r--r--java/com/android/voicemailomtp/mail/Message.java144
-rw-r--r--java/com/android/voicemailomtp/mail/MessageDateComparator.java34
-rw-r--r--java/com/android/voicemailomtp/mail/MessagingException.java139
-rw-r--r--java/com/android/voicemailomtp/mail/Multipart.java62
-rw-r--r--java/com/android/voicemailomtp/mail/PackedString.java175
-rw-r--r--java/com/android/voicemailomtp/mail/Part.java51
-rw-r--r--java/com/android/voicemailomtp/mail/PeekableInputStream.java80
-rw-r--r--java/com/android/voicemailomtp/mail/TempDirectory.java41
-rw-r--r--java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java91
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java207
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeHeader.java161
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeMessage.java675
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeMultipart.java112
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeUtility.java416
-rw-r--r--java/com/android/voicemailomtp/mail/internet/TextBody.java63
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapConnection.java413
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapFolder.java784
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapStore.java176
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java335
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java144
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapElement.java120
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapList.java235
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java76
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java158
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java432
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java62
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapString.java192
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java123
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java125
-rw-r--r--java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java48
-rw-r--r--java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java48
-rw-r--r--java/com/android/voicemailomtp/mail/utils/LogUtils.java413
-rw-r--r--java/com/android/voicemailomtp/mail/utils/Utility.java80
-rw-r--r--java/com/android/voicemailomtp/permissions.xml21
-rw-r--r--java/com/android/voicemailomtp/protocol/CvvmProtocol.java59
-rw-r--r--java/com/android/voicemailomtp/protocol/OmtpProtocol.java37
-rw-r--r--java/com/android/voicemailomtp/protocol/ProtocolHelper.java43
-rw-r--r--java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java100
-rw-r--r--java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java47
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java271
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3Protocol.java301
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java326
-rw-r--r--java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml97
-rw-r--r--java/com/android/voicemailomtp/res/values/arrays.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/attrs.xml20
-rw-r--r--java/com/android/voicemailomtp/res/values/colors.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/config.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/dimens.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/ids.xml20
-rw-r--r--java/com/android/voicemailomtp/res/values/strings.xml86
-rw-r--r--java/com/android/voicemailomtp/res/values/styles.xml19
-rw-r--r--java/com/android/voicemailomtp/res/xml/voicemail_settings.xml27
-rw-r--r--java/com/android/voicemailomtp/res/xml/vvm_config.xml134
-rw-r--r--java/com/android/voicemailomtp/scheduling/BaseTask.java206
-rw-r--r--java/com/android/voicemailomtp/scheduling/BlockerTask.java55
-rw-r--r--java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java69
-rw-r--r--java/com/android/voicemailomtp/scheduling/Policy.java36
-rw-r--r--java/com/android/voicemailomtp/scheduling/PostponePolicy.java69
-rw-r--r--java/com/android/voicemailomtp/scheduling/RetryPolicy.java117
-rw-r--r--java/com/android/voicemailomtp/scheduling/Task.java133
-rw-r--r--java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java392
-rw-r--r--java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java77
-rw-r--r--java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java634
-rw-r--r--java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java222
-rw-r--r--java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java67
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java55
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java162
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpMessageSender.java89
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java119
-rw-r--r--java/com/android/voicemailomtp/sms/StatusMessage.java209
-rw-r--r--java/com/android/voicemailomtp/sms/StatusSmsFetcher.java162
-rw-r--r--java/com/android/voicemailomtp/sms/SyncMessage.java166
-rw-r--r--java/com/android/voicemailomtp/sms/Vvm3MessageSender.java56
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java1202
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java392
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java129
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java177
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java139
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java114
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java29
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java184
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java324
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java111
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java630
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java151
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java62
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java284
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java229
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java272
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java65
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java88
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java259
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java73
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java45
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java47
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java192
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java21
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java70
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java67
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java49
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java52
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java138
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java243
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java76
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java75
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java121
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java71
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java71
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java977
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj595
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java76
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java1009
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java35
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java30
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java123
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java37
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java207
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java454
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java87
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java96
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java148
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java268
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java62
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java877
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java207
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java454
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java96
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java148
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java127
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java570
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java86
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java882
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java207
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java454
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java96
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java148
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java1249
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java120
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java61
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java278
-rw-r--r--java/com/android/voicemailomtp/sync/SyncOneTask.java82
-rw-r--r--java/com/android/voicemailomtp/sync/SyncTask.java79
-rw-r--r--java/com/android/voicemailomtp/sync/UploadTask.java68
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java41
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java113
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java244
-rw-r--r--java/com/android/voicemailomtp/sync/VvmNetworkRequest.java118
-rw-r--r--java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java171
-rw-r--r--java/com/android/voicemailomtp/utils/IndentingPrintWriter.java160
-rw-r--r--java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java90
-rw-r--r--java/com/android/voicemailomtp/utils/VvmDumpHandler.java46
-rw-r--r--java/com/android/voicemailomtp/utils/XmlUtils.java245
1730 files changed, 162516 insertions, 0 deletions
diff --git a/java/com/android/contacts/common/AndroidManifest.xml b/java/com/android/contacts/common/AndroidManifest.xml
new file mode 100644
index 000000000..eae70cd30
--- /dev/null
+++ b/java/com/android/contacts/common/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.contacts.common">
+
+ <application>
+
+ <activity
+ android:name="com.android.contacts.common.dialog.CallSubjectDialog"
+ android:theme="@style/Theme.CallSubjectDialogTheme"
+ android:windowSoftInputMode="stateVisible|adjustResize">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ </intent-filter>
+ </activity>
+
+ <!-- Broadcast receiver that passively listens to location updates -->
+ <receiver android:name="com.android.contacts.common.location.CountryDetector$LocationChangedReceiver"/>
+
+ <!-- IntentService to update the user's current country -->
+ <service
+ android:exported="false"
+ android:name="com.android.contacts.common.location.UpdateCountryService"/>
+ </application>
+</manifest>
+
diff --git a/java/com/android/contacts/common/Bindings.java b/java/com/android/contacts/common/Bindings.java
new file mode 100644
index 000000000..29cf7950a
--- /dev/null
+++ b/java/com/android/contacts/common/Bindings.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import com.android.contacts.common.bindings.ContactsCommonBindings;
+import com.android.contacts.common.bindings.ContactsCommonBindingsFactory;
+import com.android.contacts.common.bindings.ContactsCommonBindingsStub;
+import java.util.Objects;
+
+/** Accessor for the contacts common bindings. */
+public class Bindings {
+
+ private static ContactsCommonBindings instance;
+
+ private Bindings() {}
+
+ public static ContactsCommonBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof ContactsCommonBindingsFactory) {
+ instance = ((ContactsCommonBindingsFactory) application).newContactsCommonBindings();
+ }
+
+ if (instance == null) {
+ instance = new ContactsCommonBindingsStub();
+ }
+ return instance;
+ }
+
+ public static void setForTesting(ContactsCommonBindings testInstance) {
+ instance = testInstance;
+ }
+}
diff --git a/java/com/android/contacts/common/ClipboardUtils.java b/java/com/android/contacts/common/ClipboardUtils.java
new file mode 100644
index 000000000..9345b0f9c
--- /dev/null
+++ b/java/com/android/contacts/common/ClipboardUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common;
+
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.text.TextUtils;
+import android.widget.Toast;
+
+public class ClipboardUtils {
+
+ private static final String TAG = "ClipboardUtils";
+
+ private ClipboardUtils() {}
+
+ /**
+ * Copy a text to clipboard.
+ *
+ * @param context Context
+ * @param label Label to show to the user describing this clip.
+ * @param text Text to copy.
+ * @param showToast If {@code true}, a toast is shown to the user.
+ */
+ public static void copyText(
+ Context context, CharSequence label, CharSequence text, boolean showToast) {
+ if (TextUtils.isEmpty(text)) {
+ return;
+ }
+
+ ClipboardManager clipboardManager =
+ (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = ClipData.newPlainText(label == null ? "" : label, text);
+ clipboardManager.setPrimaryClip(clipData);
+
+ if (showToast) {
+ String toastText = context.getString(R.string.toast_text_copied);
+ Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/Collapser.java b/java/com/android/contacts/common/Collapser.java
new file mode 100644
index 000000000..0b5c48bf2
--- /dev/null
+++ b/java/com/android/contacts/common/Collapser.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class used for collapsing data items into groups of similar items. The data items that should be
+ * collapsible should implement the Collapsible interface. The class also contains a utility
+ * function that takes an ArrayList of items and returns a list of the same items collapsed into
+ * groups.
+ */
+public final class Collapser {
+
+ /*
+ * The Collapser uses an n^2 algorithm so we don't want it to run on
+ * lists beyond a certain size. This specifies the maximum size to collapse.
+ */
+ private static final int MAX_LISTSIZE_TO_COLLAPSE = 20;
+
+ /*
+ * This utility class cannot be instantiated.
+ */
+ private Collapser() {}
+
+ /**
+ * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed if
+ * {@link Collapsible#shouldCollapseWith(Object)} returns true, and are collapsed through the
+ * {@Link Collapsible#collapseWith(Object)} function implemented by the data item.
+ *
+ * @param list List of Objects of type <T extends Collapsible<T>> to be collapsed.
+ */
+ public static <T extends Collapsible<T>> void collapseList(List<T> list, Context context) {
+
+ int listSize = list.size();
+ // The algorithm below is n^2 so don't run on long lists
+ if (listSize > MAX_LISTSIZE_TO_COLLAPSE) {
+ return;
+ }
+
+ for (int i = 0; i < listSize; i++) {
+ T iItem = list.get(i);
+ if (iItem != null) {
+ for (int j = i + 1; j < listSize; j++) {
+ T jItem = list.get(j);
+ if (jItem != null) {
+ if (iItem.shouldCollapseWith(jItem, context)) {
+ iItem.collapseWith(jItem);
+ list.set(j, null);
+ } else if (jItem.shouldCollapseWith(iItem, context)) {
+ jItem.collapseWith(iItem);
+ list.set(i, null);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Remove the null items
+ Iterator<T> itr = list.iterator();
+ while (itr.hasNext()) {
+ if (itr.next() == null) {
+ itr.remove();
+ }
+ }
+ }
+
+ /*
+ * Interface implemented by data types that can be collapsed into groups of similar data. This
+ * can be used for example to collapse similar contact data items into a single item.
+ */
+ public interface Collapsible<T> {
+
+ void collapseWith(T t);
+
+ boolean shouldCollapseWith(T t, Context context);
+ }
+}
diff --git a/java/com/android/contacts/common/ContactPhotoManager.java b/java/com/android/contacts/common/ContactPhotoManager.java
new file mode 100644
index 000000000..834471047
--- /dev/null
+++ b/java/com/android/contacts/common/ContactPhotoManager.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.ComponentCallbacks2;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.ImageView;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Asynchronously loads contact photos and maintains a cache of photos. */
+public abstract class ContactPhotoManager implements ComponentCallbacks2 {
+
+ /** Contact type constants used for default letter images */
+ public static final int TYPE_PERSON = LetterTileDrawable.TYPE_PERSON;
+
+ public static final int TYPE_BUSINESS = LetterTileDrawable.TYPE_BUSINESS;
+ public static final int TYPE_VOICEMAIL = LetterTileDrawable.TYPE_VOICEMAIL;
+ public static final int TYPE_DEFAULT = LetterTileDrawable.TYPE_DEFAULT;
+ /** Scale and offset default constants used for default letter images */
+ public static final float SCALE_DEFAULT = 1.0f;
+
+ public static final float OFFSET_DEFAULT = 0.0f;
+ public static final boolean IS_CIRCULAR_DEFAULT = false;
+ // TODO: Use LogUtil.isVerboseEnabled for DEBUG branches instead of a lint check.
+ // LINT.DoNotSubmitIf(true)
+ static final boolean DEBUG = false;
+ // LINT.DoNotSubmitIf(true)
+ static final boolean DEBUG_SIZES = false;
+ /** Uri-related constants used for default letter images */
+ private static final String DISPLAY_NAME_PARAM_KEY = "display_name";
+
+ private static final String IDENTIFIER_PARAM_KEY = "identifier";
+ private static final String CONTACT_TYPE_PARAM_KEY = "contact_type";
+ private static final String SCALE_PARAM_KEY = "scale";
+ private static final String OFFSET_PARAM_KEY = "offset";
+ private static final String IS_CIRCULAR_PARAM_KEY = "is_circular";
+ private static final String DEFAULT_IMAGE_URI_SCHEME = "defaultimage";
+ private static final Uri DEFAULT_IMAGE_URI = Uri.parse(DEFAULT_IMAGE_URI_SCHEME + "://");
+ public static final DefaultImageProvider DEFAULT_AVATAR = new LetterTileDefaultImageProvider();
+ private static ContactPhotoManager sInstance;
+
+ /**
+ * Given a {@link DefaultImageRequest}, returns an Uri that can be used to request a letter tile
+ * avatar when passed to the {@link ContactPhotoManager}. The internal implementation of this uri
+ * is not guaranteed to remain the same across application versions, so the actual uri should
+ * never be persisted in long-term storage and reused.
+ *
+ * @param request A {@link DefaultImageRequest} object with the fields configured to return a
+ * @return A Uri that when later passed to the {@link ContactPhotoManager} via {@link
+ * #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest)}, can be used to
+ * request a default contact image, drawn as a letter tile using the parameters as configured
+ * in the provided {@link DefaultImageRequest}
+ */
+ public static Uri getDefaultAvatarUriForContact(DefaultImageRequest request) {
+ final Builder builder = DEFAULT_IMAGE_URI.buildUpon();
+ if (request != null) {
+ if (!TextUtils.isEmpty(request.displayName)) {
+ builder.appendQueryParameter(DISPLAY_NAME_PARAM_KEY, request.displayName);
+ }
+ if (!TextUtils.isEmpty(request.identifier)) {
+ builder.appendQueryParameter(IDENTIFIER_PARAM_KEY, request.identifier);
+ }
+ if (request.contactType != TYPE_DEFAULT) {
+ builder.appendQueryParameter(CONTACT_TYPE_PARAM_KEY, String.valueOf(request.contactType));
+ }
+ if (request.scale != SCALE_DEFAULT) {
+ builder.appendQueryParameter(SCALE_PARAM_KEY, String.valueOf(request.scale));
+ }
+ if (request.offset != OFFSET_DEFAULT) {
+ builder.appendQueryParameter(OFFSET_PARAM_KEY, String.valueOf(request.offset));
+ }
+ if (request.isCircular != IS_CIRCULAR_DEFAULT) {
+ builder.appendQueryParameter(IS_CIRCULAR_PARAM_KEY, String.valueOf(request.isCircular));
+ }
+ }
+ return builder.build();
+ }
+
+ /**
+ * Adds a business contact type encoded fragment to the URL. Used to ensure photo URLS from Nearby
+ * Places can be identified as business photo URLs rather than URLs for personal contact photos.
+ *
+ * @param photoUrl The photo URL to modify.
+ * @return URL with the contact type parameter added and set to TYPE_BUSINESS.
+ */
+ public static String appendBusinessContactType(String photoUrl) {
+ Uri uri = Uri.parse(photoUrl);
+ Builder builder = uri.buildUpon();
+ builder.encodedFragment(String.valueOf(TYPE_BUSINESS));
+ return builder.build().toString();
+ }
+
+ /**
+ * Removes the contact type information stored in the photo URI encoded fragment.
+ *
+ * @param photoUri The photo URI to remove the contact type from.
+ * @return The photo URI with contact type removed.
+ */
+ public static Uri removeContactType(Uri photoUri) {
+ String encodedFragment = photoUri.getEncodedFragment();
+ if (!TextUtils.isEmpty(encodedFragment)) {
+ Builder builder = photoUri.buildUpon();
+ builder.encodedFragment(null);
+ return builder.build();
+ }
+ return photoUri;
+ }
+
+ /**
+ * Inspects a photo URI to determine if the photo URI represents a business.
+ *
+ * @param photoUri The URI to inspect.
+ * @return Whether the URI represents a business photo or not.
+ */
+ public static boolean isBusinessContactUri(Uri photoUri) {
+ if (photoUri == null) {
+ return false;
+ }
+
+ String encodedFragment = photoUri.getEncodedFragment();
+ return !TextUtils.isEmpty(encodedFragment)
+ && encodedFragment.equals(String.valueOf(TYPE_BUSINESS));
+ }
+
+ protected static DefaultImageRequest getDefaultImageRequestFromUri(Uri uri) {
+ final DefaultImageRequest request =
+ new DefaultImageRequest(
+ uri.getQueryParameter(DISPLAY_NAME_PARAM_KEY),
+ uri.getQueryParameter(IDENTIFIER_PARAM_KEY),
+ false);
+ try {
+ String contactType = uri.getQueryParameter(CONTACT_TYPE_PARAM_KEY);
+ if (!TextUtils.isEmpty(contactType)) {
+ request.contactType = Integer.valueOf(contactType);
+ }
+
+ String scale = uri.getQueryParameter(SCALE_PARAM_KEY);
+ if (!TextUtils.isEmpty(scale)) {
+ request.scale = Float.valueOf(scale);
+ }
+
+ String offset = uri.getQueryParameter(OFFSET_PARAM_KEY);
+ if (!TextUtils.isEmpty(offset)) {
+ request.offset = Float.valueOf(offset);
+ }
+
+ String isCircular = uri.getQueryParameter(IS_CIRCULAR_PARAM_KEY);
+ if (!TextUtils.isEmpty(isCircular)) {
+ request.isCircular = Boolean.valueOf(isCircular);
+ }
+ } catch (NumberFormatException e) {
+ LogUtil.w(
+ "ContactPhotoManager.getDefaultImageRequestFromUri",
+ "Invalid DefaultImageRequest image parameters provided, ignoring and using "
+ + "defaults.");
+ }
+
+ return request;
+ }
+
+ public static ContactPhotoManager getInstance(Context context) {
+ if (sInstance == null) {
+ Context applicationContext = context.getApplicationContext();
+ sInstance = createContactPhotoManager(applicationContext);
+ applicationContext.registerComponentCallbacks(sInstance);
+ if (PermissionsUtil.hasContactsPermissions(context)) {
+ sInstance.preloadPhotosInBackground();
+ }
+ }
+ return sInstance;
+ }
+
+ public static synchronized ContactPhotoManager createContactPhotoManager(Context context) {
+ return new ContactPhotoManagerImpl(context);
+ }
+
+ @VisibleForTesting
+ public static void injectContactPhotoManagerForTesting(ContactPhotoManager photoManager) {
+ sInstance = photoManager;
+ }
+
+ protected boolean isDefaultImageUri(Uri uri) {
+ return DEFAULT_IMAGE_URI_SCHEME.equals(uri.getScheme());
+ }
+
+ /**
+ * Load thumbnail image into the supplied image view. If the photo is already cached, it is
+ * displayed immediately. Otherwise a request is sent to load the photo from the database.
+ */
+ public abstract void loadThumbnail(
+ ImageView view,
+ long photoId,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider);
+
+ /**
+ * Calls {@link #loadThumbnail(ImageView, long, boolean, boolean, DefaultImageRequest,
+ * DefaultImageProvider)} using the {@link DefaultImageProvider} {@link #DEFAULT_AVATAR}.
+ */
+ public final void loadThumbnail(
+ ImageView view,
+ long photoId,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest) {
+ loadThumbnail(view, photoId, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
+ }
+
+ /**
+ * Load photo into the supplied image view. If the photo is already cached, it is displayed
+ * immediately. Otherwise a request is sent to load the photo from the location specified by the
+ * URI.
+ *
+ * @param view The target view
+ * @param photoUri The uri of the photo to load
+ * @param requestedExtent Specifies an approximate Max(width, height) of the targetView. This is
+ * useful if the source image can be a lot bigger that the target, so that the decoding is
+ * done using efficient sampling. If requestedExtent is specified, no sampling of the image is
+ * performed
+ * @param darkTheme Whether the background is dark. This is used for default avatars
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ * @param defaultProvider The provider of default avatars (this is used if photoUri doesn't refer
+ * to an existing image)
+ */
+ public abstract void loadPhoto(
+ ImageView view,
+ Uri photoUri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider);
+
+ /**
+ * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
+ * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and {@code null} display names and lookup
+ * keys.
+ *
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ */
+ public final void loadPhoto(
+ ImageView view,
+ Uri photoUri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest) {
+ loadPhoto(
+ view,
+ photoUri,
+ requestedExtent,
+ darkTheme,
+ isCircular,
+ defaultImageRequest,
+ DEFAULT_AVATAR);
+ }
+
+ /**
+ * Calls {@link #loadPhoto(ImageView, Uri, int, boolean, boolean, DefaultImageRequest,
+ * DefaultImageProvider)} with {@link #DEFAULT_AVATAR} and with the assumption, that the image is
+ * a thumbnail.
+ *
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ */
+ public final void loadDirectoryPhoto(
+ ImageView view,
+ Uri photoUri,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest) {
+ loadPhoto(view, photoUri, -1, darkTheme, isCircular, defaultImageRequest, DEFAULT_AVATAR);
+ }
+
+ /**
+ * Remove photo from the supplied image view. This also cancels current pending load request
+ * inside this photo manager.
+ */
+ public abstract void removePhoto(ImageView view);
+
+ /** Cancels all pending requests to load photos asynchronously. */
+ public abstract void cancelPendingRequests(View fragmentRootView);
+
+ /** Temporarily stops loading photos from the database. */
+ public abstract void pause();
+
+ /** Resumes loading photos from the database. */
+ public abstract void resume();
+
+ /**
+ * Marks all cached photos for reloading. We can continue using cache but should also make sure
+ * the photos haven't changed in the background and notify the views if so.
+ */
+ public abstract void refreshCache();
+
+ /** Initiates a background process that over time will fill up cache with preload photos. */
+ public abstract void preloadPhotosInBackground();
+
+ // ComponentCallbacks2
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {}
+
+ // ComponentCallbacks2
+ @Override
+ public void onLowMemory() {}
+
+ // ComponentCallbacks2
+ @Override
+ public void onTrimMemory(int level) {}
+
+ /**
+ * Contains fields used to contain contact details and other user-defined settings that might be
+ * used by the ContactPhotoManager to generate a default contact image. This contact image takes
+ * the form of a letter or bitmap drawn on top of a colored tile.
+ */
+ public static class DefaultImageRequest {
+
+ /**
+ * Used to indicate that a drawable that represents a contact without any contact details should
+ * be returned.
+ */
+ public static final DefaultImageRequest EMPTY_DEFAULT_IMAGE_REQUEST = new DefaultImageRequest();
+ /**
+ * Used to indicate that a drawable that represents a business without a business photo should
+ * be returned.
+ */
+ public static final DefaultImageRequest EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST =
+ new DefaultImageRequest(null, null, TYPE_BUSINESS, false);
+ /**
+ * Used to indicate that a circular drawable that represents a contact without any contact
+ * details should be returned.
+ */
+ public static final DefaultImageRequest EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST =
+ new DefaultImageRequest(null, null, true);
+ /**
+ * Used to indicate that a circular drawable that represents a business without a business photo
+ * should be returned.
+ */
+ public static final DefaultImageRequest EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST =
+ new DefaultImageRequest(null, null, TYPE_BUSINESS, true);
+ /** The contact's display name. The display name is used to */
+ public String displayName;
+ /**
+ * A unique and deterministic string that can be used to identify this contact. This is usually
+ * the contact's lookup key, but other contact details can be used as well, especially for
+ * non-local or temporary contacts that might not have a lookup key. This is used to determine
+ * the color of the tile.
+ */
+ public String identifier;
+ /**
+ * The type of this contact. This contact type may be used to decide the kind of image to use in
+ * the case where a unique letter cannot be generated from the contact's display name and
+ * identifier. See: {@link #TYPE_PERSON} {@link #TYPE_BUSINESS} {@link #TYPE_PERSON} {@link
+ * #TYPE_DEFAULT}
+ */
+ public int contactType = TYPE_DEFAULT;
+ /**
+ * The amount to scale the letter or bitmap to, as a ratio of its default size (from a range of
+ * 0.0f to 2.0f). The default value is 1.0f.
+ */
+ public float scale = SCALE_DEFAULT;
+ /**
+ * The amount to vertically offset the letter or image to within the tile. The provided offset
+ * must be within the range of -0.5f to 0.5f. If set to -0.5f, the letter will be shifted
+ * upwards by 0.5 times the height of the canvas it is being drawn on, which means it will be
+ * drawn with the center of the letter starting at the top edge of the canvas. If set to 0.5f,
+ * the letter will be shifted downwards by 0.5 times the height of the canvas it is being drawn
+ * on, which means it will be drawn with the center of the letter starting at the bottom edge of
+ * the canvas. The default is 0.0f, which means the letter is drawn in the exact vertical center
+ * of the tile.
+ */
+ public float offset = OFFSET_DEFAULT;
+ /** Whether or not to draw the default image as a circle, instead of as a square/rectangle. */
+ public boolean isCircular = false;
+
+ public DefaultImageRequest() {}
+
+ public DefaultImageRequest(String displayName, String identifier, boolean isCircular) {
+ this(displayName, identifier, TYPE_DEFAULT, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
+ }
+
+ public DefaultImageRequest(
+ String displayName, String identifier, int contactType, boolean isCircular) {
+ this(displayName, identifier, contactType, SCALE_DEFAULT, OFFSET_DEFAULT, isCircular);
+ }
+
+ public DefaultImageRequest(
+ String displayName,
+ String identifier,
+ int contactType,
+ float scale,
+ float offset,
+ boolean isCircular) {
+ this.displayName = displayName;
+ this.identifier = identifier;
+ this.contactType = contactType;
+ this.scale = scale;
+ this.offset = offset;
+ this.isCircular = isCircular;
+ }
+ }
+
+ public abstract static class DefaultImageProvider {
+
+ /**
+ * Applies the default avatar to the ImageView. Extent is an indicator for the size (width or
+ * height). If darkTheme is set, the avatar is one that looks better on dark background
+ *
+ * @param defaultImageRequest {@link DefaultImageRequest} object that specifies how a default
+ * letter tile avatar should be drawn.
+ */
+ public abstract void applyDefaultImage(
+ ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest);
+ }
+
+ /**
+ * A default image provider that applies a letter tile consisting of a colored background and a
+ * letter in the foreground as the default image for a contact. The color of the background and
+ * the type of letter is decided based on the contact's details.
+ */
+ private static class LetterTileDefaultImageProvider extends DefaultImageProvider {
+
+ public static Drawable getDefaultImageForContact(
+ Resources resources, DefaultImageRequest defaultImageRequest) {
+ final LetterTileDrawable drawable = new LetterTileDrawable(resources);
+ final int tileShape =
+ defaultImageRequest.isCircular
+ ? LetterTileDrawable.SHAPE_CIRCLE
+ : LetterTileDrawable.SHAPE_RECTANGLE;
+ if (defaultImageRequest != null) {
+ // If the contact identifier is null or empty, fallback to the
+ // displayName. In that case, use {@code null} for the contact's
+ // display name so that a default bitmap will be used instead of a
+ // letter
+ if (TextUtils.isEmpty(defaultImageRequest.identifier)) {
+ drawable.setCanonicalDialerLetterTileDetails(
+ null, defaultImageRequest.displayName, tileShape, defaultImageRequest.contactType);
+ } else {
+ drawable.setCanonicalDialerLetterTileDetails(
+ defaultImageRequest.displayName,
+ defaultImageRequest.identifier,
+ tileShape,
+ defaultImageRequest.contactType);
+ }
+ drawable.setScale(defaultImageRequest.scale);
+ drawable.setOffset(defaultImageRequest.offset);
+ }
+ return drawable;
+ }
+
+ @Override
+ public void applyDefaultImage(
+ ImageView view, int extent, boolean darkTheme, DefaultImageRequest defaultImageRequest) {
+ final Drawable drawable = getDefaultImageForContact(view.getResources(), defaultImageRequest);
+ view.setImageDrawable(drawable);
+ }
+ }
+}
+
diff --git a/java/com/android/contacts/common/ContactPhotoManagerImpl.java b/java/com/android/contacts/common/ContactPhotoManagerImpl.java
new file mode 100644
index 000000000..2e6ff9fdc
--- /dev/null
+++ b/java/com/android/contacts/common/ContactPhotoManagerImpl.java
@@ -0,0 +1,1262 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.app.ActivityManager;
+import android.content.ComponentCallbacks2;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.media.ThumbnailUtils;
+import android.net.TrafficStats;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Contacts.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.UiThread;
+import android.support.annotation.WorkerThread;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import android.text.TextUtils;
+import android.util.LruCache;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.TrafficStatsTags;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.SoftReference;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+class ContactPhotoManagerImpl extends ContactPhotoManager implements Callback {
+
+ private static final String LOADER_THREAD_NAME = "ContactPhotoLoader";
+
+ private static final int FADE_TRANSITION_DURATION = 200;
+
+ /**
+ * Type of message sent by the UI thread to itself to indicate that some photos need to be loaded.
+ */
+ private static final int MESSAGE_REQUEST_LOADING = 1;
+
+ /** Type of message sent by the loader thread to indicate that some photos have been loaded. */
+ private static final int MESSAGE_PHOTOS_LOADED = 2;
+
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static final String[] COLUMNS = new String[] {Photo._ID, Photo.PHOTO};
+
+ /**
+ * Dummy object used to indicate that a bitmap for a given key could not be stored in the cache.
+ */
+ private static final BitmapHolder BITMAP_UNAVAILABLE;
+ /** Cache size for {@link #mBitmapHolderCache} for devices with "large" RAM. */
+ private static final int HOLDER_CACHE_SIZE = 2000000;
+ /** Cache size for {@link #mBitmapCache} for devices with "large" RAM. */
+ private static final int BITMAP_CACHE_SIZE = 36864 * 48; // 1728K
+ /** Height/width of a thumbnail image */
+ private static int mThumbnailSize;
+
+ static {
+ BITMAP_UNAVAILABLE = new BitmapHolder(new byte[0], 0);
+ BITMAP_UNAVAILABLE.bitmapRef = new SoftReference<Bitmap>(null);
+ }
+
+ private final Context mContext;
+ /**
+ * An LRU cache for bitmap holders. The cache contains bytes for photos just as they come from the
+ * database. Each holder has a soft reference to the actual bitmap.
+ */
+ private final LruCache<Object, BitmapHolder> mBitmapHolderCache;
+ /** Cache size threshold at which bitmaps will not be preloaded. */
+ private final int mBitmapHolderCacheRedZoneBytes;
+ /**
+ * Level 2 LRU cache for bitmaps. This is a smaller cache that holds the most recently used
+ * bitmaps to save time on decoding them from bytes (the bytes are stored in {@link
+ * #mBitmapHolderCache}.
+ */
+ private final LruCache<Object, Bitmap> mBitmapCache;
+ /**
+ * A map from ImageView to the corresponding photo ID or uri, encapsulated in a request. The
+ * request may swapped out before the photo loading request is started.
+ */
+ private final ConcurrentHashMap<ImageView, Request> mPendingRequests =
+ new ConcurrentHashMap<ImageView, Request>();
+ /** Handler for messages sent to the UI thread. */
+ private final Handler mMainThreadHandler = new Handler(this);
+ /** For debug: How many times we had to reload cached photo for a stale entry */
+ private final AtomicInteger mStaleCacheOverwrite = new AtomicInteger();
+ /** For debug: How many times we had to reload cached photo for a fresh entry. Should be 0. */
+ private final AtomicInteger mFreshCacheOverwrite = new AtomicInteger();
+ /** {@code true} if ALL entries in {@link #mBitmapHolderCache} are NOT fresh. */
+ private volatile boolean mBitmapHolderCacheAllUnfresh = true;
+ /** Thread responsible for loading photos from the database. Created upon the first request. */
+ private LoaderThread mLoaderThread;
+ /** A gate to make sure we only send one instance of MESSAGE_PHOTOS_NEEDED at a time. */
+ private boolean mLoadingRequested;
+ /** Flag indicating if the image loading is paused. */
+ private boolean mPaused;
+ /** The user agent string to use when loading URI based photos. */
+ private String mUserAgent;
+
+ public ContactPhotoManagerImpl(Context context) {
+ mContext = context;
+
+ final ActivityManager am =
+ ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE));
+
+ final float cacheSizeAdjustment = (am.isLowRamDevice()) ? 0.5f : 1.0f;
+
+ final int bitmapCacheSize = (int) (cacheSizeAdjustment * BITMAP_CACHE_SIZE);
+ mBitmapCache =
+ new LruCache<Object, Bitmap>(bitmapCacheSize) {
+ @Override
+ protected int sizeOf(Object key, Bitmap value) {
+ return value.getByteCount();
+ }
+
+ @Override
+ protected void entryRemoved(
+ boolean evicted, Object key, Bitmap oldValue, Bitmap newValue) {
+ if (DEBUG) {
+ dumpStats();
+ }
+ }
+ };
+ final int holderCacheSize = (int) (cacheSizeAdjustment * HOLDER_CACHE_SIZE);
+ mBitmapHolderCache =
+ new LruCache<Object, BitmapHolder>(holderCacheSize) {
+ @Override
+ protected int sizeOf(Object key, BitmapHolder value) {
+ return value.bytes != null ? value.bytes.length : 0;
+ }
+
+ @Override
+ protected void entryRemoved(
+ boolean evicted, Object key, BitmapHolder oldValue, BitmapHolder newValue) {
+ if (DEBUG) {
+ dumpStats();
+ }
+ }
+ };
+ mBitmapHolderCacheRedZoneBytes = (int) (holderCacheSize * 0.75);
+ LogUtil.i(
+ "ContactPhotoManagerImpl.ContactPhotoManagerImpl", "cache adj: " + cacheSizeAdjustment);
+ if (DEBUG) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.ContactPhotoManagerImpl",
+ "Cache size: " + btk(mBitmapHolderCache.maxSize()) + " + " + btk(mBitmapCache.maxSize()));
+ }
+
+ mThumbnailSize =
+ context.getResources().getDimensionPixelSize(R.dimen.contact_browser_list_item_photo_size);
+
+ // Get a user agent string to use for URI photo requests.
+ mUserAgent = Bindings.get(context).getUserAgent();
+ if (mUserAgent == null) {
+ mUserAgent = "";
+ }
+ }
+
+ /** Converts bytes to K bytes, rounding up. Used only for debug log. */
+ private static String btk(int bytes) {
+ return ((bytes + 1023) / 1024) + "K";
+ }
+
+ private static final int safeDiv(int dividend, int divisor) {
+ return (divisor == 0) ? 0 : (dividend / divisor);
+ }
+
+ private static boolean isChildView(View parent, View potentialChild) {
+ return potentialChild.getParent() != null
+ && (potentialChild.getParent() == parent
+ || (potentialChild.getParent() instanceof ViewGroup
+ && isChildView(parent, (ViewGroup) potentialChild.getParent())));
+ }
+
+ /**
+ * If necessary, decodes bytes stored in the holder to Bitmap. As long as the bitmap is held
+ * either by {@link #mBitmapCache} or by a soft reference in the holder, it will not be necessary
+ * to decode the bitmap.
+ */
+ private static void inflateBitmap(BitmapHolder holder, int requestedExtent) {
+ final int sampleSize =
+ BitmapUtil.findOptimalSampleSize(holder.originalSmallerExtent, requestedExtent);
+ byte[] bytes = holder.bytes;
+ if (bytes == null || bytes.length == 0) {
+ return;
+ }
+
+ if (sampleSize == holder.decodedSampleSize) {
+ // Check the soft reference. If will be retained if the bitmap is also
+ // in the LRU cache, so we don't need to check the LRU cache explicitly.
+ if (holder.bitmapRef != null) {
+ holder.bitmap = holder.bitmapRef.get();
+ if (holder.bitmap != null) {
+ return;
+ }
+ }
+ }
+
+ try {
+ Bitmap bitmap = BitmapUtil.decodeBitmapFromBytes(bytes, sampleSize);
+
+ // TODO: As a temporary workaround while framework support is being added to
+ // clip non-square bitmaps into a perfect circle, manually crop the bitmap into
+ // into a square if it will be displayed as a thumbnail so that it can be cropped
+ // into a circle.
+ final int height = bitmap.getHeight();
+ final int width = bitmap.getWidth();
+
+ // The smaller dimension of a scaled bitmap can range from anywhere from 0 to just
+ // below twice the length of a thumbnail image due to the way we calculate the optimal
+ // sample size.
+ if (height != width && Math.min(height, width) <= mThumbnailSize * 2) {
+ final int dimension = Math.min(height, width);
+ bitmap = ThumbnailUtils.extractThumbnail(bitmap, dimension, dimension);
+ }
+ // make bitmap mutable and draw size onto it
+ if (DEBUG_SIZES) {
+ Bitmap original = bitmap;
+ bitmap = bitmap.copy(bitmap.getConfig(), true);
+ original.recycle();
+ Canvas canvas = new Canvas(bitmap);
+ Paint paint = new Paint();
+ paint.setTextSize(16);
+ paint.setColor(Color.BLUE);
+ paint.setStyle(Style.FILL);
+ canvas.drawRect(0.0f, 0.0f, 50.0f, 20.0f, paint);
+ paint.setColor(Color.WHITE);
+ paint.setAntiAlias(true);
+ canvas.drawText(bitmap.getWidth() + "/" + sampleSize, 0, 15, paint);
+ }
+
+ holder.decodedSampleSize = sampleSize;
+ holder.bitmap = bitmap;
+ holder.bitmapRef = new SoftReference<Bitmap>(bitmap);
+ if (DEBUG) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.inflateBitmap",
+ "inflateBitmap "
+ + btk(bytes.length)
+ + " -> "
+ + bitmap.getWidth()
+ + "x"
+ + bitmap.getHeight()
+ + ", "
+ + btk(bitmap.getByteCount()));
+ }
+ } catch (OutOfMemoryError e) {
+ // Do nothing - the photo will appear to be missing
+ }
+ }
+
+ /** Dump cache stats on logcat. */
+ private void dumpStats() {
+ if (!DEBUG) {
+ return;
+ }
+ {
+ int numHolders = 0;
+ int rawBytes = 0;
+ int bitmapBytes = 0;
+ int numBitmaps = 0;
+ for (BitmapHolder h : mBitmapHolderCache.snapshot().values()) {
+ numHolders++;
+ if (h.bytes != null) {
+ rawBytes += h.bytes.length;
+ }
+ Bitmap b = h.bitmapRef != null ? h.bitmapRef.get() : null;
+ if (b != null) {
+ numBitmaps++;
+ bitmapBytes += b.getByteCount();
+ }
+ }
+ LogUtil.d(
+ "ContactPhotoManagerImpl.dumpStats",
+ "L1: "
+ + btk(rawBytes)
+ + " + "
+ + btk(bitmapBytes)
+ + " = "
+ + btk(rawBytes + bitmapBytes)
+ + ", "
+ + numHolders
+ + " holders, "
+ + numBitmaps
+ + " bitmaps, avg: "
+ + btk(safeDiv(rawBytes, numHolders))
+ + ","
+ + btk(safeDiv(bitmapBytes, numBitmaps)));
+ LogUtil.d(
+ "ContactPhotoManagerImpl.dumpStats",
+ "L1 Stats: "
+ + mBitmapHolderCache.toString()
+ + ", overwrite: fresh="
+ + mFreshCacheOverwrite.get()
+ + " stale="
+ + mStaleCacheOverwrite.get());
+ }
+
+ {
+ int numBitmaps = 0;
+ int bitmapBytes = 0;
+ for (Bitmap b : mBitmapCache.snapshot().values()) {
+ numBitmaps++;
+ bitmapBytes += b.getByteCount();
+ }
+ LogUtil.d(
+ "ContactPhotoManagerImpl.dumpStats",
+ "L2: "
+ + btk(bitmapBytes)
+ + ", "
+ + numBitmaps
+ + " bitmaps"
+ + ", avg: "
+ + btk(safeDiv(bitmapBytes, numBitmaps)));
+ // We don't get from L2 cache, so L2 stats is meaningless.
+ }
+ }
+
+ @Override
+ public void onTrimMemory(int level) {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.onTrimMemory", "onTrimMemory: " + level);
+ }
+ if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
+ // Clear the caches. Note all pending requests will be removed too.
+ clear();
+ }
+ }
+
+ @Override
+ public void preloadPhotosInBackground() {
+ ensureLoaderThread();
+ mLoaderThread.requestPreloading();
+ }
+
+ @Override
+ public void loadThumbnail(
+ ImageView view,
+ long photoId,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider) {
+ if (photoId == 0) {
+ // No photo is needed
+ defaultProvider.applyDefaultImage(view, -1, darkTheme, defaultImageRequest);
+ mPendingRequests.remove(view);
+ } else {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.loadThumbnail", "loadPhoto request: " + photoId);
+ }
+ loadPhotoByIdOrUri(
+ view, Request.createFromThumbnailId(photoId, darkTheme, isCircular, defaultProvider));
+ }
+ }
+
+ @Override
+ public void loadPhoto(
+ ImageView view,
+ Uri photoUri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageRequest defaultImageRequest,
+ DefaultImageProvider defaultProvider) {
+ if (photoUri == null) {
+ // No photo is needed
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, defaultImageRequest);
+ mPendingRequests.remove(view);
+ } else {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.loadPhoto", "loadPhoto request: " + photoUri);
+ }
+ if (isDefaultImageUri(photoUri)) {
+ createAndApplyDefaultImageForUri(
+ view, photoUri, requestedExtent, darkTheme, isCircular, defaultProvider);
+ } else {
+ loadPhotoByIdOrUri(
+ view,
+ Request.createFromUri(
+ photoUri, requestedExtent, darkTheme, isCircular, defaultProvider));
+ }
+ }
+ }
+
+ private void createAndApplyDefaultImageForUri(
+ ImageView view,
+ Uri uri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageProvider defaultProvider) {
+ DefaultImageRequest request = getDefaultImageRequestFromUri(uri);
+ request.isCircular = isCircular;
+ defaultProvider.applyDefaultImage(view, requestedExtent, darkTheme, request);
+ }
+
+ private void loadPhotoByIdOrUri(ImageView view, Request request) {
+ boolean loaded = loadCachedPhoto(view, request, false);
+ if (loaded) {
+ mPendingRequests.remove(view);
+ } else {
+ mPendingRequests.put(view, request);
+ if (!mPaused) {
+ // Send a request to start loading photos
+ requestLoading();
+ }
+ }
+ }
+
+ @Override
+ public void removePhoto(ImageView view) {
+ view.setImageDrawable(null);
+ mPendingRequests.remove(view);
+ }
+
+ /**
+ * Cancels pending requests to load photos asynchronously for views inside {@param
+ * fragmentRootView}. If {@param fragmentRootView} is null, cancels all requests.
+ */
+ @Override
+ public void cancelPendingRequests(View fragmentRootView) {
+ if (fragmentRootView == null) {
+ mPendingRequests.clear();
+ return;
+ }
+ final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
+ while (iterator.hasNext()) {
+ final ImageView imageView = iterator.next().getKey();
+ // If an ImageView is orphaned (currently scrap) or a child of fragmentRootView, then
+ // we can safely remove its request.
+ if (imageView.getParent() == null || isChildView(fragmentRootView, imageView)) {
+ iterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public void refreshCache() {
+ if (mBitmapHolderCacheAllUnfresh) {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache -- no fresh entries.");
+ }
+ return;
+ }
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.refreshCache", "refreshCache");
+ }
+ mBitmapHolderCacheAllUnfresh = true;
+ for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
+ if (holder != BITMAP_UNAVAILABLE) {
+ holder.fresh = false;
+ }
+ }
+ }
+
+ /**
+ * Checks if the photo is present in cache. If so, sets the photo on the view.
+ *
+ * @return false if the photo needs to be (re)loaded from the provider.
+ */
+ @UiThread
+ private boolean loadCachedPhoto(ImageView view, Request request, boolean fadeIn) {
+ BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
+ if (holder == null) {
+ // The bitmap has not been loaded ==> show default avatar
+ request.applyDefaultImage(view, request.mIsCircular);
+ return false;
+ }
+
+ if (holder.bytes == null) {
+ request.applyDefaultImage(view, request.mIsCircular);
+ return holder.fresh;
+ }
+
+ Bitmap cachedBitmap = holder.bitmapRef == null ? null : holder.bitmapRef.get();
+ if (cachedBitmap == null) {
+ request.applyDefaultImage(view, request.mIsCircular);
+ return false;
+ }
+
+ final Drawable previousDrawable = view.getDrawable();
+ if (fadeIn && previousDrawable != null) {
+ final Drawable[] layers = new Drawable[2];
+ // Prevent cascade of TransitionDrawables.
+ if (previousDrawable instanceof TransitionDrawable) {
+ final TransitionDrawable previousTransitionDrawable = (TransitionDrawable) previousDrawable;
+ layers[0] =
+ previousTransitionDrawable.getDrawable(
+ previousTransitionDrawable.getNumberOfLayers() - 1);
+ } else {
+ layers[0] = previousDrawable;
+ }
+ layers[1] = getDrawableForBitmap(mContext.getResources(), cachedBitmap, request);
+ TransitionDrawable drawable = new TransitionDrawable(layers);
+ view.setImageDrawable(drawable);
+ drawable.startTransition(FADE_TRANSITION_DURATION);
+ } else {
+ view.setImageDrawable(getDrawableForBitmap(mContext.getResources(), cachedBitmap, request));
+ }
+
+ // Put the bitmap in the LRU cache. But only do this for images that are small enough
+ // (we require that at least six of those can be cached at the same time)
+ if (cachedBitmap.getByteCount() < mBitmapCache.maxSize() / 6) {
+ mBitmapCache.put(request.getKey(), cachedBitmap);
+ }
+
+ // Soften the reference
+ holder.bitmap = null;
+
+ return holder.fresh;
+ }
+
+ /**
+ * Given a bitmap, returns a drawable that is configured to display the bitmap based on the
+ * specified request.
+ */
+ private Drawable getDrawableForBitmap(Resources resources, Bitmap bitmap, Request request) {
+ if (request.mIsCircular) {
+ final RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(resources, bitmap);
+ drawable.setAntiAlias(true);
+ drawable.setCornerRadius(bitmap.getHeight() / 2);
+ return drawable;
+ } else {
+ return new BitmapDrawable(resources, bitmap);
+ }
+ }
+
+ public void clear() {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.clear", "clear");
+ }
+ mPendingRequests.clear();
+ mBitmapHolderCache.evictAll();
+ mBitmapCache.evictAll();
+ }
+
+ @Override
+ public void pause() {
+ mPaused = true;
+ }
+
+ @Override
+ public void resume() {
+ mPaused = false;
+ if (DEBUG) {
+ dumpStats();
+ }
+ if (!mPendingRequests.isEmpty()) {
+ requestLoading();
+ }
+ }
+
+ /**
+ * Sends a message to this thread itself to start loading images. If the current view contains
+ * multiple image views, all of those image views will get a chance to request their respective
+ * photos before any of those requests are executed. This allows us to load images in bulk.
+ */
+ private void requestLoading() {
+ if (!mLoadingRequested) {
+ mLoadingRequested = true;
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_REQUEST_LOADING);
+ }
+ }
+
+ /** Processes requests on the main thread. */
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_REQUEST_LOADING:
+ {
+ mLoadingRequested = false;
+ if (!mPaused) {
+ ensureLoaderThread();
+ mLoaderThread.requestLoading();
+ }
+ return true;
+ }
+
+ case MESSAGE_PHOTOS_LOADED:
+ {
+ if (!mPaused) {
+ processLoadedImages();
+ }
+ if (DEBUG) {
+ dumpStats();
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void ensureLoaderThread() {
+ if (mLoaderThread == null) {
+ mLoaderThread = new LoaderThread(mContext.getContentResolver());
+ mLoaderThread.start();
+ }
+ }
+
+ /**
+ * Goes over pending loading requests and displays loaded photos. If some of the photos still
+ * haven't been loaded, sends another request for image loading.
+ */
+ private void processLoadedImages() {
+ final Iterator<Entry<ImageView, Request>> iterator = mPendingRequests.entrySet().iterator();
+ while (iterator.hasNext()) {
+ final Entry<ImageView, Request> entry = iterator.next();
+ // TODO: Temporarily disable contact photo fading in, until issues with
+ // RoundedBitmapDrawables overlapping the default image drawables are resolved.
+ final boolean loaded = loadCachedPhoto(entry.getKey(), entry.getValue(), false);
+ if (loaded) {
+ iterator.remove();
+ }
+ }
+
+ softenCache();
+
+ if (!mPendingRequests.isEmpty()) {
+ requestLoading();
+ }
+ }
+
+ /**
+ * Removes strong references to loaded bitmaps to allow them to be garbage collected if needed.
+ * Some of the bitmaps will still be retained by {@link #mBitmapCache}.
+ */
+ private void softenCache() {
+ for (BitmapHolder holder : mBitmapHolderCache.snapshot().values()) {
+ holder.bitmap = null;
+ }
+ }
+
+ /** Stores the supplied bitmap in cache. */
+ private void cacheBitmap(Object key, byte[] bytes, boolean preloading, int requestedExtent) {
+ if (DEBUG) {
+ BitmapHolder prev = mBitmapHolderCache.get(key);
+ if (prev != null && prev.bytes != null) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.cacheBitmap",
+ "overwriting cache: key=" + key + (prev.fresh ? " FRESH" : " stale"));
+ if (prev.fresh) {
+ mFreshCacheOverwrite.incrementAndGet();
+ } else {
+ mStaleCacheOverwrite.incrementAndGet();
+ }
+ }
+ LogUtil.d(
+ "ContactPhotoManagerImpl.cacheBitmap",
+ "caching data: key=" + key + ", " + (bytes == null ? "<null>" : btk(bytes.length)));
+ }
+ BitmapHolder holder =
+ new BitmapHolder(bytes, bytes == null ? -1 : BitmapUtil.getSmallerExtentFromBytes(bytes));
+
+ // Unless this image is being preloaded, decode it right away while
+ // we are still on the background thread.
+ if (!preloading) {
+ inflateBitmap(holder, requestedExtent);
+ }
+
+ if (bytes != null) {
+ mBitmapHolderCache.put(key, holder);
+ if (mBitmapHolderCache.get(key) != holder) {
+ LogUtil.w("ContactPhotoManagerImpl.cacheBitmap", "bitmap too big to fit in cache.");
+ mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
+ }
+ } else {
+ mBitmapHolderCache.put(key, BITMAP_UNAVAILABLE);
+ }
+
+ mBitmapHolderCacheAllUnfresh = false;
+ }
+
+ /**
+ * Populates an array of photo IDs that need to be loaded. Also decodes bitmaps that we have
+ * already loaded
+ */
+ private void obtainPhotoIdsAndUrisToLoad(
+ Set<Long> photoIds, Set<String> photoIdsAsStrings, Set<Request> uris) {
+ photoIds.clear();
+ photoIdsAsStrings.clear();
+ uris.clear();
+
+ boolean jpegsDecoded = false;
+
+ /*
+ * Since the call is made from the loader thread, the map could be
+ * changing during the iteration. That's not really a problem:
+ * ConcurrentHashMap will allow those changes to happen without throwing
+ * exceptions. Since we may miss some requests in the situation of
+ * concurrent change, we will need to check the map again once loading
+ * is complete.
+ */
+ Iterator<Request> iterator = mPendingRequests.values().iterator();
+ while (iterator.hasNext()) {
+ Request request = iterator.next();
+ final BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
+ if (holder == BITMAP_UNAVAILABLE) {
+ continue;
+ }
+ if (holder != null
+ && holder.bytes != null
+ && holder.fresh
+ && (holder.bitmapRef == null || holder.bitmapRef.get() == null)) {
+ // This was previously loaded but we don't currently have the inflated Bitmap
+ inflateBitmap(holder, request.getRequestedExtent());
+ jpegsDecoded = true;
+ } else {
+ if (holder == null || !holder.fresh) {
+ if (request.isUriRequest()) {
+ uris.add(request);
+ } else {
+ photoIds.add(request.getId());
+ photoIdsAsStrings.add(String.valueOf(request.mId));
+ }
+ }
+ }
+ }
+
+ if (jpegsDecoded) {
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+ }
+ }
+
+ /** Maintains the state of a particular photo. */
+ private static class BitmapHolder {
+
+ final byte[] bytes;
+ final int originalSmallerExtent;
+
+ volatile boolean fresh;
+ Bitmap bitmap;
+ Reference<Bitmap> bitmapRef;
+ int decodedSampleSize;
+
+ public BitmapHolder(byte[] bytes, int originalSmallerExtent) {
+ this.bytes = bytes;
+ this.fresh = true;
+ this.originalSmallerExtent = originalSmallerExtent;
+ }
+ }
+
+ /**
+ * A holder for either a Uri or an id and a flag whether this was requested for the dark or light
+ * theme
+ */
+ private static final class Request {
+
+ private final long mId;
+ private final Uri mUri;
+ private final boolean mDarkTheme;
+ private final int mRequestedExtent;
+ private final DefaultImageProvider mDefaultProvider;
+ /** Whether or not the contact photo is to be displayed as a circle */
+ private final boolean mIsCircular;
+
+ private Request(
+ long id,
+ Uri uri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageProvider defaultProvider) {
+ mId = id;
+ mUri = uri;
+ mDarkTheme = darkTheme;
+ mIsCircular = isCircular;
+ mRequestedExtent = requestedExtent;
+ mDefaultProvider = defaultProvider;
+ }
+
+ public static Request createFromThumbnailId(
+ long id, boolean darkTheme, boolean isCircular, DefaultImageProvider defaultProvider) {
+ return new Request(id, null /* no URI */, -1, darkTheme, isCircular, defaultProvider);
+ }
+
+ public static Request createFromUri(
+ Uri uri,
+ int requestedExtent,
+ boolean darkTheme,
+ boolean isCircular,
+ DefaultImageProvider defaultProvider) {
+ return new Request(
+ 0 /* no ID */, uri, requestedExtent, darkTheme, isCircular, defaultProvider);
+ }
+
+ public boolean isUriRequest() {
+ return mUri != null;
+ }
+
+ public Uri getUri() {
+ return mUri;
+ }
+
+ public long getId() {
+ return mId;
+ }
+
+ public int getRequestedExtent() {
+ return mRequestedExtent;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + (int) (mId ^ (mId >>> 32));
+ result = prime * result + mRequestedExtent;
+ result = prime * result + ((mUri == null) ? 0 : mUri.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Request that = (Request) obj;
+ if (mId != that.mId) {
+ return false;
+ }
+ if (mRequestedExtent != that.mRequestedExtent) {
+ return false;
+ }
+ if (!UriUtils.areEqual(mUri, that.mUri)) {
+ return false;
+ }
+ // Don't compare equality of mDarkTheme because it is only used in the default contact
+ // photo case. When the contact does have a photo, the contact photo is the same
+ // regardless of mDarkTheme, so we shouldn't need to put the photo request on the queue
+ // twice.
+ return true;
+ }
+
+ public Object getKey() {
+ return mUri == null ? mId : mUri;
+ }
+
+ /**
+ * Applies the default image to the current view. If the request is URI-based, looks for the
+ * contact type encoded fragment to determine if this is a request for a business photo, in
+ * which case we will load the default business photo.
+ *
+ * @param view The current image view to apply the image to.
+ * @param isCircular Whether the image is circular or not.
+ */
+ public void applyDefaultImage(ImageView view, boolean isCircular) {
+ final DefaultImageRequest request;
+
+ if (isCircular) {
+ request =
+ ContactPhotoManager.isBusinessContactUri(mUri)
+ ? DefaultImageRequest.EMPTY_CIRCULAR_BUSINESS_IMAGE_REQUEST
+ : DefaultImageRequest.EMPTY_CIRCULAR_DEFAULT_IMAGE_REQUEST;
+ } else {
+ request =
+ ContactPhotoManager.isBusinessContactUri(mUri)
+ ? DefaultImageRequest.EMPTY_DEFAULT_BUSINESS_IMAGE_REQUEST
+ : DefaultImageRequest.EMPTY_DEFAULT_IMAGE_REQUEST;
+ }
+ mDefaultProvider.applyDefaultImage(view, mRequestedExtent, mDarkTheme, request);
+ }
+ }
+
+ /** The thread that performs loading of photos from the database. */
+ private class LoaderThread extends HandlerThread implements Callback {
+
+ private static final int BUFFER_SIZE = 1024 * 16;
+ private static final int MESSAGE_PRELOAD_PHOTOS = 0;
+ private static final int MESSAGE_LOAD_PHOTOS = 1;
+
+ /** A pause between preload batches that yields to the UI thread. */
+ private static final int PHOTO_PRELOAD_DELAY = 1000;
+
+ /** Number of photos to preload per batch. */
+ private static final int PRELOAD_BATCH = 25;
+
+ /**
+ * Maximum number of photos to preload. If the cache size is 2Mb and the expected average size
+ * of a photo is 4kb, then this number should be 2Mb/4kb = 500.
+ */
+ private static final int MAX_PHOTOS_TO_PRELOAD = 100;
+
+ private static final int PRELOAD_STATUS_NOT_STARTED = 0;
+ private static final int PRELOAD_STATUS_IN_PROGRESS = 1;
+ private static final int PRELOAD_STATUS_DONE = 2;
+ private final ContentResolver mResolver;
+ private final StringBuilder mStringBuilder = new StringBuilder();
+ private final Set<Long> mPhotoIds = new HashSet<>();
+ private final Set<String> mPhotoIdsAsStrings = new HashSet<>();
+ private final Set<Request> mPhotoUris = new HashSet<>();
+ private final List<Long> mPreloadPhotoIds = new ArrayList<>();
+ private Handler mLoaderThreadHandler;
+ private byte[] mBuffer;
+ private int mPreloadStatus = PRELOAD_STATUS_NOT_STARTED;
+
+ public LoaderThread(ContentResolver resolver) {
+ super(LOADER_THREAD_NAME);
+ mResolver = resolver;
+ }
+
+ public void ensureHandler() {
+ if (mLoaderThreadHandler == null) {
+ mLoaderThreadHandler = new Handler(getLooper(), this);
+ }
+ }
+
+ /**
+ * Kicks off preloading of the next batch of photos on the background thread. Preloading will
+ * happen after a delay: we want to yield to the UI thread as much as possible.
+ *
+ * <p>If preloading is already complete, does nothing.
+ */
+ public void requestPreloading() {
+ if (mPreloadStatus == PRELOAD_STATUS_DONE) {
+ return;
+ }
+
+ ensureHandler();
+ if (mLoaderThreadHandler.hasMessages(MESSAGE_LOAD_PHOTOS)) {
+ return;
+ }
+
+ mLoaderThreadHandler.sendEmptyMessageDelayed(MESSAGE_PRELOAD_PHOTOS, PHOTO_PRELOAD_DELAY);
+ }
+
+ /**
+ * Sends a message to this thread to load requested photos. Cancels a preloading request, if
+ * any: we don't want preloading to impede loading of the photos we need to display now.
+ */
+ public void requestLoading() {
+ ensureHandler();
+ mLoaderThreadHandler.removeMessages(MESSAGE_PRELOAD_PHOTOS);
+ mLoaderThreadHandler.sendEmptyMessage(MESSAGE_LOAD_PHOTOS);
+ }
+
+ /**
+ * Receives the above message, loads photos and then sends a message to the main thread to
+ * process them.
+ */
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_PRELOAD_PHOTOS:
+ preloadPhotosInBackground();
+ break;
+ case MESSAGE_LOAD_PHOTOS:
+ loadPhotosInBackground();
+ break;
+ }
+ return true;
+ }
+
+ /**
+ * The first time it is called, figures out which photos need to be preloaded. Each subsequent
+ * call preloads the next batch of photos and requests another cycle of preloading after a
+ * delay. The whole process ends when we either run out of photos to preload or fill up cache.
+ */
+ @WorkerThread
+ private void preloadPhotosInBackground() {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+ return;
+ }
+
+ if (mPreloadStatus == PRELOAD_STATUS_DONE) {
+ return;
+ }
+
+ if (mPreloadStatus == PRELOAD_STATUS_NOT_STARTED) {
+ queryPhotosForPreload();
+ if (mPreloadPhotoIds.isEmpty()) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ } else {
+ mPreloadStatus = PRELOAD_STATUS_IN_PROGRESS;
+ }
+ requestPreloading();
+ return;
+ }
+
+ if (mBitmapHolderCache.size() > mBitmapHolderCacheRedZoneBytes) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ return;
+ }
+
+ mPhotoIds.clear();
+ mPhotoIdsAsStrings.clear();
+
+ int count = 0;
+ int preloadSize = mPreloadPhotoIds.size();
+ while (preloadSize > 0 && mPhotoIds.size() < PRELOAD_BATCH) {
+ preloadSize--;
+ count++;
+ Long photoId = mPreloadPhotoIds.get(preloadSize);
+ mPhotoIds.add(photoId);
+ mPhotoIdsAsStrings.add(photoId.toString());
+ mPreloadPhotoIds.remove(preloadSize);
+ }
+
+ loadThumbnails(true);
+
+ if (preloadSize == 0) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ }
+
+ LogUtil.v(
+ "ContactPhotoManagerImpl.preloadPhotosInBackground",
+ "preloaded " + count + " photos. cached bytes: " + mBitmapHolderCache.size());
+
+ requestPreloading();
+ }
+
+ @WorkerThread
+ private void queryPhotosForPreload() {
+ Cursor cursor = null;
+ try {
+ Uri uri =
+ Contacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
+ .appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY, String.valueOf(MAX_PHOTOS_TO_PRELOAD))
+ .build();
+ cursor =
+ mResolver.query(
+ uri,
+ new String[] {Contacts.PHOTO_ID},
+ Contacts.PHOTO_ID + " NOT NULL AND " + Contacts.PHOTO_ID + "!=0",
+ null,
+ Contacts.STARRED + " DESC, " + Contacts.LAST_TIME_CONTACTED + " DESC");
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ // Insert them in reverse order, because we will be taking
+ // them from the end of the list for loading.
+ mPreloadPhotoIds.add(0, cursor.getLong(0));
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ @WorkerThread
+ private void loadPhotosInBackground() {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+ return;
+ }
+ obtainPhotoIdsAndUrisToLoad(mPhotoIds, mPhotoIdsAsStrings, mPhotoUris);
+ loadThumbnails(false);
+ loadUriBasedPhotos();
+ requestPreloading();
+ }
+
+ /** Loads thumbnail photos with ids */
+ @WorkerThread
+ private void loadThumbnails(boolean preloading) {
+ if (mPhotoIds.isEmpty()) {
+ return;
+ }
+
+ // Remove loaded photos from the preload queue: we don't want
+ // the preloading process to load them again.
+ if (!preloading && mPreloadStatus == PRELOAD_STATUS_IN_PROGRESS) {
+ for (Long id : mPhotoIds) {
+ mPreloadPhotoIds.remove(id);
+ }
+ if (mPreloadPhotoIds.isEmpty()) {
+ mPreloadStatus = PRELOAD_STATUS_DONE;
+ }
+ }
+
+ mStringBuilder.setLength(0);
+ mStringBuilder.append(Photo._ID + " IN(");
+ for (int i = 0; i < mPhotoIds.size(); i++) {
+ if (i != 0) {
+ mStringBuilder.append(',');
+ }
+ mStringBuilder.append('?');
+ }
+ mStringBuilder.append(')');
+
+ Cursor cursor = null;
+ try {
+ if (DEBUG) {
+ LogUtil.d(
+ "ContactPhotoManagerImpl.loadThumbnails",
+ "loading " + TextUtils.join(",", mPhotoIdsAsStrings));
+ }
+ cursor =
+ mResolver.query(
+ Data.CONTENT_URI,
+ COLUMNS,
+ mStringBuilder.toString(),
+ mPhotoIdsAsStrings.toArray(EMPTY_STRING_ARRAY),
+ null);
+
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ Long id = cursor.getLong(0);
+ byte[] bytes = cursor.getBlob(1);
+ cacheBitmap(id, bytes, preloading, -1);
+ mPhotoIds.remove(id);
+ }
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ // Remaining photos were not found in the contacts database (but might be in profile).
+ for (Long id : mPhotoIds) {
+ if (ContactsContract.isProfileId(id)) {
+ Cursor profileCursor = null;
+ try {
+ profileCursor =
+ mResolver.query(
+ ContentUris.withAppendedId(Data.CONTENT_URI, id), COLUMNS, null, null, null);
+ if (profileCursor != null && profileCursor.moveToFirst()) {
+ cacheBitmap(profileCursor.getLong(0), profileCursor.getBlob(1), preloading, -1);
+ } else {
+ // Couldn't load a photo this way either.
+ cacheBitmap(id, null, preloading, -1);
+ }
+ } finally {
+ if (profileCursor != null) {
+ profileCursor.close();
+ }
+ }
+ } else {
+ // Not a profile photo and not found - mark the cache accordingly
+ cacheBitmap(id, null, preloading, -1);
+ }
+ }
+
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+ }
+
+ /**
+ * Loads photos referenced with Uris. Those can be remote thumbnails (from directory searches),
+ * display photos etc
+ */
+ @WorkerThread
+ private void loadUriBasedPhotos() {
+ for (Request uriRequest : mPhotoUris) {
+ // Keep the original URI and use this to key into the cache. Failure to do so will
+ // result in an image being continually reloaded into cache if the original URI
+ // has a contact type encodedFragment (eg nearby places business photo URLs).
+ Uri originalUri = uriRequest.getUri();
+
+ // Strip off the "contact type" we added to the URI to ensure it was identifiable as
+ // a business photo -- there is no need to pass this on to the server.
+ Uri uri = ContactPhotoManager.removeContactType(originalUri);
+
+ if (mBuffer == null) {
+ mBuffer = new byte[BUFFER_SIZE];
+ }
+ try {
+ if (DEBUG) {
+ LogUtil.d("ContactPhotoManagerImpl.loadUriBasedPhotos", "loading " + uri);
+ }
+ final String scheme = uri.getScheme();
+ InputStream is = null;
+ if (scheme.equals("http") || scheme.equals("https")) {
+ TrafficStats.setThreadStatsTag(TrafficStatsTags.CONTACT_PHOTO_DOWNLOAD_TAG);
+ final HttpURLConnection connection =
+ (HttpURLConnection) new URL(uri.toString()).openConnection();
+
+ // Include the user agent if it is specified.
+ if (!TextUtils.isEmpty(mUserAgent)) {
+ connection.setRequestProperty("User-Agent", mUserAgent);
+ }
+ try {
+ is = connection.getInputStream();
+ } catch (IOException e) {
+ connection.disconnect();
+ is = null;
+ }
+ TrafficStats.clearThreadStatsTag();
+ } else {
+ is = mResolver.openInputStream(uri);
+ }
+ if (is != null) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ int size;
+ while ((size = is.read(mBuffer)) != -1) {
+ baos.write(mBuffer, 0, size);
+ }
+ } finally {
+ is.close();
+ }
+ cacheBitmap(originalUri, baos.toByteArray(), false, uriRequest.getRequestedExtent());
+ mMainThreadHandler.sendEmptyMessage(MESSAGE_PHOTOS_LOADED);
+ } else {
+ LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri);
+ cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
+ }
+ } catch (final Exception | OutOfMemoryError ex) {
+ LogUtil.v("ContactPhotoManagerImpl.loadUriBasedPhotos", "cannot load photo " + uri, ex);
+ cacheBitmap(originalUri, null, false, uriRequest.getRequestedExtent());
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/ContactPresenceIconUtil.java b/java/com/android/contacts/common/ContactPresenceIconUtil.java
new file mode 100644
index 000000000..eeaf652a8
--- /dev/null
+++ b/java/com/android/contacts/common/ContactPresenceIconUtil.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.StatusUpdates;
+
+/** Define the contact present show policy in Contacts */
+public class ContactPresenceIconUtil {
+
+ /**
+ * Get the presence icon resource according the status.
+ *
+ * @return null means don't show the status icon.
+ */
+ public static Drawable getPresenceIcon(Context context, int status) {
+ // We don't show the offline status in Contacts
+ switch (status) {
+ case StatusUpdates.AVAILABLE:
+ case StatusUpdates.IDLE:
+ case StatusUpdates.AWAY:
+ case StatusUpdates.DO_NOT_DISTURB:
+ case StatusUpdates.INVISIBLE:
+ return context.getResources().getDrawable(StatusUpdates.getPresenceIconResourceId(status));
+ case StatusUpdates.OFFLINE:
+ // The undefined status is treated as OFFLINE in getPresenceIconResourceId();
+ default:
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/ContactStatusUtil.java b/java/com/android/contacts/common/ContactStatusUtil.java
new file mode 100644
index 000000000..97d84c876
--- /dev/null
+++ b/java/com/android/contacts/common/ContactStatusUtil.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.StatusUpdates;
+
+/** Provides static function to get default contact status message. */
+public class ContactStatusUtil {
+
+ private static final String TAG = "ContactStatusUtil";
+
+ public static String getStatusString(Context context, int presence) {
+ Resources resources = context.getResources();
+ switch (presence) {
+ case StatusUpdates.AVAILABLE:
+ return resources.getString(R.string.status_available);
+ case StatusUpdates.IDLE:
+ case StatusUpdates.AWAY:
+ return resources.getString(R.string.status_away);
+ case StatusUpdates.DO_NOT_DISTURB:
+ return resources.getString(R.string.status_busy);
+ case StatusUpdates.OFFLINE:
+ case StatusUpdates.INVISIBLE:
+ default:
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/ContactTileLoaderFactory.java b/java/com/android/contacts/common/ContactTileLoaderFactory.java
new file mode 100644
index 000000000..d71472ef8
--- /dev/null
+++ b/java/com/android/contacts/common/ContactTileLoaderFactory.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.VisibleForTesting;
+
+/**
+ * Used to create {@link CursorLoader} which finds contacts information from the strequents table.
+ *
+ * <p>Only returns contacts with phone numbers.
+ */
+public final class ContactTileLoaderFactory {
+
+ /**
+ * The _ID field returned for strequent items actually contains data._id instead of contacts._id
+ * because the query is performed on the data table. In order to obtain the contact id for
+ * strequent items, use Phone.contact_id instead.
+ */
+ @VisibleForTesting
+ public static final String[] COLUMNS_PHONE_ONLY =
+ new String[] {
+ Contacts._ID,
+ Contacts.DISPLAY_NAME_PRIMARY,
+ Contacts.STARRED,
+ Contacts.PHOTO_URI,
+ Contacts.LOOKUP_KEY,
+ Phone.NUMBER,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.IS_SUPER_PRIMARY,
+ Contacts.PINNED,
+ Phone.CONTACT_ID,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ };
+
+ public static CursorLoader createStrequentPhoneOnlyLoader(Context context) {
+ Uri uri =
+ Contacts.CONTENT_STREQUENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
+ .build();
+
+ return new CursorLoader(context, uri, COLUMNS_PHONE_ONLY, null, null, null);
+ }
+}
diff --git a/java/com/android/contacts/common/ContactsUtils.java b/java/com/android/contacts/common/ContactsUtils.java
new file mode 100644
index 000000000..60af44b9a
--- /dev/null
+++ b/java/com/android/contacts/common/ContactsUtils.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.DisplayPhoto;
+import android.support.annotation.IntDef;
+import android.text.TextUtils;
+import android.util.Pair;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.ImDataItem;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+public class ContactsUtils {
+
+ // Telecomm related schemes are in CallUtil
+ public static final String SCHEME_IMTO = "imto";
+ public static final String SCHEME_MAILTO = "mailto";
+ public static final String SCHEME_SMSTO = "smsto";
+ public static final long USER_TYPE_CURRENT = 0;
+ public static final long USER_TYPE_WORK = 1;
+ private static final String TAG = "ContactsUtils";
+ private static final int DEFAULT_THUMBNAIL_SIZE = 96;
+ private static int sThumbnailSize = -1;
+
+ /**
+ * This looks up the provider name defined in ProviderNames from the predefined IM protocol id.
+ * This is used for interacting with the IM application.
+ *
+ * @param protocol the protocol ID
+ * @return the provider name the IM app uses for the given protocol, or null if no provider is
+ * defined for the given protocol
+ * @hide
+ */
+ public static String lookupProviderNameFromId(int protocol) {
+ switch (protocol) {
+ case Im.PROTOCOL_GOOGLE_TALK:
+ return ProviderNames.GTALK;
+ case Im.PROTOCOL_AIM:
+ return ProviderNames.AIM;
+ case Im.PROTOCOL_MSN:
+ return ProviderNames.MSN;
+ case Im.PROTOCOL_YAHOO:
+ return ProviderNames.YAHOO;
+ case Im.PROTOCOL_ICQ:
+ return ProviderNames.ICQ;
+ case Im.PROTOCOL_JABBER:
+ return ProviderNames.JABBER;
+ case Im.PROTOCOL_SKYPE:
+ return ProviderNames.SKYPE;
+ case Im.PROTOCOL_QQ:
+ return ProviderNames.QQ;
+ }
+ return null;
+ }
+
+ /**
+ * Test if the given {@link CharSequence} contains any graphic characters, first checking {@link
+ * TextUtils#isEmpty(CharSequence)} to handle null.
+ */
+ public static boolean isGraphic(CharSequence str) {
+ return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
+ }
+
+ /** Returns true if two objects are considered equal. Two null references are equal here. */
+ public static boolean areObjectsEqual(Object a, Object b) {
+ return a == b || (a != null && a.equals(b));
+ }
+
+ /** Returns true if two {@link Intent}s are both null, or have the same action. */
+ public static final boolean areIntentActionEqual(Intent a, Intent b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ return TextUtils.equals(a.getAction(), b.getAction());
+ }
+
+ public static boolean areGroupWritableAccountsAvailable(Context context) {
+ final List<AccountWithDataSet> accounts =
+ AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+ return !accounts.isEmpty();
+ }
+
+ /**
+ * Returns the size (width and height) of thumbnail pictures as configured in the provider. This
+ * can safely be called from the UI thread, as the provider can serve this without performing a
+ * database access
+ */
+ public static int getThumbnailSize(Context context) {
+ if (sThumbnailSize == -1) {
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(
+ DisplayPhoto.CONTENT_MAX_DIMENSIONS_URI,
+ new String[] {DisplayPhoto.THUMBNAIL_MAX_DIM},
+ null,
+ null,
+ null);
+ if (c != null) {
+ try {
+ if (c.moveToFirst()) {
+ sThumbnailSize = c.getInt(0);
+ }
+ } finally {
+ c.close();
+ }
+ }
+ }
+ return sThumbnailSize != -1 ? sThumbnailSize : DEFAULT_THUMBNAIL_SIZE;
+ }
+
+ private static Intent getCustomImIntent(ImDataItem im, int protocol) {
+ String host = im.getCustomProtocol();
+ final String data = im.getData();
+ if (TextUtils.isEmpty(data)) {
+ return null;
+ }
+ if (protocol != Im.PROTOCOL_CUSTOM) {
+ // Try bringing in a well-known host for specific protocols
+ host = ContactsUtils.lookupProviderNameFromId(protocol);
+ }
+ if (TextUtils.isEmpty(host)) {
+ return null;
+ }
+ final String authority = host.toLowerCase();
+ final Uri imUri =
+ new Uri.Builder().scheme(SCHEME_IMTO).authority(authority).appendPath(data).build();
+ final Intent intent = new Intent(Intent.ACTION_SENDTO, imUri);
+ return intent;
+ }
+
+ /**
+ * Returns the proper Intent for an ImDatItem. If available, a secondary intent is stored in the
+ * second Pair slot
+ */
+ public static Pair<Intent, Intent> buildImIntent(Context context, ImDataItem im) {
+ Intent intent = null;
+ Intent secondaryIntent = null;
+ final boolean isEmail = im.isCreatedFromEmail();
+
+ if (!isEmail && !im.isProtocolValid()) {
+ return new Pair<>(null, null);
+ }
+
+ final String data = im.getData();
+ if (TextUtils.isEmpty(data)) {
+ return new Pair<>(null, null);
+ }
+
+ final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
+
+ if (protocol == Im.PROTOCOL_GOOGLE_TALK) {
+ final int chatCapability = im.getChatCapability();
+ if ((chatCapability & Im.CAPABILITY_HAS_CAMERA) != 0) {
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+ secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
+ } else if ((chatCapability & Im.CAPABILITY_HAS_VOICE) != 0) {
+ // Allow Talking and Texting
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+ secondaryIntent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?call"));
+ } else {
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("xmpp:" + data + "?message"));
+ }
+ } else {
+ // Build an IM Intent
+ intent = getCustomImIntent(im, protocol);
+ }
+ return new Pair<>(intent, secondaryIntent);
+ }
+
+ /**
+ * Determine UserType from directory id and contact id.
+ *
+ * <p>3 types of query
+ *
+ * <p>1. 2 profile query: content://com.android.contacts/phone_lookup_enterprise/1234567890
+ * personal and work contact are mixed into one cursor. no directory id. contact_id indicates if
+ * it's work contact
+ *
+ * <p>2. work local query:
+ * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000000 either
+ * directory_id or contact_id is enough to identify work contact
+ *
+ * <p>3. work remote query:
+ * content://com.android.contacts/phone_lookup_enterprise/1234567890?directory=1000000003
+ * contact_id is random. only directory_id is available
+ *
+ * <p>Summary: If directory_id is not null, always use directory_id to identify work contact.
+ * (which is the case here) Otherwise, use contact_id.
+ *
+ * @param directoryId directory id of ContactsProvider query
+ * @param contactId contact id
+ * @return UserType indicates the user type of the contact. A directory id or contact id larger
+ * than a thredshold indicates that the contact is stored in Work Profile, but not in current
+ * user. It's a contract by ContactsProvider and check by Contacts.isEnterpriseDirectoryId and
+ * Contacts.isEnterpriseContactId. Currently, only 2 kinds of users can be detected from the
+ * directoryId and contactId as ContactsProvider can only access current and work user's
+ * contacts
+ */
+ public static @UserType long determineUserType(Long directoryId, Long contactId) {
+ // First check directory id
+ if (directoryId != null) {
+ return DirectoryCompat.isEnterpriseDirectoryId(directoryId)
+ ? USER_TYPE_WORK
+ : USER_TYPE_CURRENT;
+ }
+ // Only check contact id if directory id is null
+ if (contactId != null && contactId != 0L && ContactsCompat.isEnterpriseContactId(contactId)) {
+ return USER_TYPE_WORK;
+ } else {
+ return USER_TYPE_CURRENT;
+ }
+ }
+
+ // TODO find a proper place for the canonical version of these
+ public interface ProviderNames {
+
+ String YAHOO = "Yahoo";
+ String GTALK = "GTalk";
+ String MSN = "MSN";
+ String ICQ = "ICQ";
+ String AIM = "AIM";
+ String XMPP = "XMPP";
+ String JABBER = "JABBER";
+ String SKYPE = "SKYPE";
+ String QQ = "QQ";
+ }
+
+ /**
+ * UserType indicates the user type of the contact. If the contact is from Work User (Work Profile
+ * in Android Multi-User System), it's {@link #USER_TYPE_WORK}, otherwise, {@link
+ * #USER_TYPE_CURRENT}. Please note that current user can be in work profile, where the dialer is
+ * running inside Work Profile.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({USER_TYPE_CURRENT, USER_TYPE_WORK})
+ public @interface UserType {}
+}
diff --git a/java/com/android/contacts/common/GeoUtil.java b/java/com/android/contacts/common/GeoUtil.java
new file mode 100644
index 000000000..50b0cd9e3
--- /dev/null
+++ b/java/com/android/contacts/common/GeoUtil.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common;
+
+import android.app.Application;
+import android.content.Context;
+import com.android.contacts.common.location.CountryDetector;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber;
+import com.google.i18n.phonenumbers.geocoding.PhoneNumberOfflineGeocoder;
+import java.util.Locale;
+
+/** Static methods related to Geo. */
+public class GeoUtil {
+
+ /**
+ * Returns the country code of the country the user is currently in. Before calling this method,
+ * make sure that {@link CountryDetector#initialize(Context)} has already been called in {@link
+ * Application#onCreate()}.
+ *
+ * @return The ISO 3166-1 two letters country code of the country the user is in.
+ */
+ public static String getCurrentCountryIso(Context context) {
+ // The {@link CountryDetector} should never return null so this is safe to return as-is.
+ return CountryDetector.getInstance(context).getCurrentCountryIso();
+ }
+
+ public static String getGeocodedLocationFor(Context context, String phoneNumber) {
+ final PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+ final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+ try {
+ final Phonenumber.PhoneNumber structuredPhoneNumber =
+ phoneNumberUtil.parse(phoneNumber, getCurrentCountryIso(context));
+ final Locale locale = context.getResources().getConfiguration().locale;
+ return geocoder.getDescriptionForNumber(structuredPhoneNumber, locale);
+ } catch (NumberParseException e) {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/GroupMetaData.java b/java/com/android/contacts/common/GroupMetaData.java
new file mode 100644
index 000000000..b34f1d629
--- /dev/null
+++ b/java/com/android/contacts/common/GroupMetaData.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.contacts.common;
+
+/**
+ * Meta-data for a contact group. We load all groups associated with the contact's constituent
+ * accounts.
+ */
+public final class GroupMetaData {
+
+ private String mAccountName;
+ private String mAccountType;
+ private String mDataSet;
+ private long mGroupId;
+ private String mTitle;
+ private boolean mDefaultGroup;
+ private boolean mFavorites;
+
+ public GroupMetaData(
+ String accountName,
+ String accountType,
+ String dataSet,
+ long groupId,
+ String title,
+ boolean defaultGroup,
+ boolean favorites) {
+ this.mAccountName = accountName;
+ this.mAccountType = accountType;
+ this.mDataSet = dataSet;
+ this.mGroupId = groupId;
+ this.mTitle = title;
+ this.mDefaultGroup = defaultGroup;
+ this.mFavorites = favorites;
+ }
+
+ public String getAccountName() {
+ return mAccountName;
+ }
+
+ public String getAccountType() {
+ return mAccountType;
+ }
+
+ public String getDataSet() {
+ return mDataSet;
+ }
+
+ public long getGroupId() {
+ return mGroupId;
+ }
+
+ public String getTitle() {
+ return mTitle;
+ }
+
+ public boolean isDefaultGroup() {
+ return mDefaultGroup;
+ }
+
+ public boolean isFavorites() {
+ return mFavorites;
+ }
+}
diff --git a/java/com/android/contacts/common/MoreContactUtils.java b/java/com/android/contacts/common/MoreContactUtils.java
new file mode 100644
index 000000000..028f89971
--- /dev/null
+++ b/java/com/android/contacts/common/MoreContactUtils.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.TextView;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+
+/** Shared static contact utility methods. */
+public class MoreContactUtils {
+
+ private static final String WAIT_SYMBOL_AS_STRING = String.valueOf(PhoneNumberUtils.WAIT);
+
+ /**
+ * Returns true if two data with mimetypes which represent values in contact entries are
+ * considered equal for collapsing in the GUI. For caller-id, use {@link
+ * android.telephony.PhoneNumberUtils#compare(android.content.Context, String, String)} instead
+ */
+ public static boolean shouldCollapse(
+ CharSequence mimetype1, CharSequence data1, CharSequence mimetype2, CharSequence data2) {
+ // different mimetypes? don't collapse
+ if (!TextUtils.equals(mimetype1, mimetype2)) {
+ return false;
+ }
+
+ // exact same string? good, bail out early
+ if (TextUtils.equals(data1, data2)) {
+ return true;
+ }
+
+ // so if either is null, these two must be different
+ if (data1 == null || data2 == null) {
+ return false;
+ }
+
+ // if this is not about phone numbers, we know this is not a match (of course, some
+ // mimetypes could have more sophisticated matching is the future, e.g. addresses)
+ if (!TextUtils.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, mimetype1)) {
+ return false;
+ }
+
+ return shouldCollapsePhoneNumbers(data1.toString(), data2.toString());
+ }
+
+ // TODO: Move this to PhoneDataItem.shouldCollapse override
+ private static boolean shouldCollapsePhoneNumbers(String number1, String number2) {
+ // Work around to address b/20724444. We want to distinguish between #555, *555 and 555.
+ // This makes no attempt to distinguish between 555 and 55*5, since 55*5 is an improbable
+ // number. PhoneNumberUtil already distinguishes between 555 and 55#5.
+ if (number1.contains("#") != number2.contains("#")
+ || number1.contains("*") != number2.contains("*")) {
+ return false;
+ }
+
+ // Now do the full phone number thing. split into parts, separated by waiting symbol
+ // and compare them individually
+ final String[] dataParts1 = number1.split(WAIT_SYMBOL_AS_STRING);
+ final String[] dataParts2 = number2.split(WAIT_SYMBOL_AS_STRING);
+ if (dataParts1.length != dataParts2.length) {
+ return false;
+ }
+ final PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ for (int i = 0; i < dataParts1.length; i++) {
+ // Match phone numbers represented by keypad letters, in which case prefer the
+ // phone number with letters.
+ final String dataPart1 = PhoneNumberUtils.convertKeypadLettersToDigits(dataParts1[i]);
+ final String dataPart2 = dataParts2[i];
+
+ // substrings equal? shortcut, don't parse
+ if (TextUtils.equals(dataPart1, dataPart2)) {
+ continue;
+ }
+
+ // do a full parse of the numbers
+ final PhoneNumberUtil.MatchType result = util.isNumberMatch(dataPart1, dataPart2);
+ switch (result) {
+ case NOT_A_NUMBER:
+ // don't understand the numbers? let's play it safe
+ return false;
+ case NO_MATCH:
+ return false;
+ case EXACT_MATCH:
+ break;
+ case NSN_MATCH:
+ try {
+ // For NANP phone numbers, match when one has +1 and the other does not.
+ // In this case, prefer the +1 version.
+ if (util.parse(dataPart1, null).getCountryCode() == 1) {
+ // At this point, the numbers can be either case 1 or 2 below....
+ //
+ // case 1)
+ // +14155551212 <--- country code 1
+ // 14155551212 <--- 1 is trunk prefix, not country code
+ //
+ // and
+ //
+ // case 2)
+ // +14155551212
+ // 4155551212
+ //
+ // From b/7519057, case 2 needs to be equal. But also that bug, case 3
+ // below should not be equal.
+ //
+ // case 3)
+ // 14155551212
+ // 4155551212
+ //
+ // So in order to make sure transitive equality is valid, case 1 cannot
+ // be equal. Otherwise, transitive equality breaks and the following
+ // would all be collapsed:
+ // 4155551212 |
+ // 14155551212 |----> +14155551212
+ // +14155551212 |
+ //
+ // With transitive equality, the collapsed values should be:
+ // 4155551212 | 14155551212
+ // 14155551212 |----> +14155551212
+ // +14155551212 |
+
+ // Distinguish between case 1 and 2 by checking for trunk prefix '1'
+ // at the start of number 2.
+ if (dataPart2.trim().charAt(0) == '1') {
+ // case 1
+ return false;
+ }
+ break;
+ }
+ } catch (NumberParseException e) {
+ // This is the case where the first number does not have a country code.
+ // examples:
+ // (123) 456-7890 & 123-456-7890 (collapse)
+ // 0049 (8092) 1234 & +49/80921234 (unit test says do not collapse)
+
+ // Check the second number. If it also does not have a country code, then
+ // we should collapse. If it has a country code, then it's a different
+ // number and we should not collapse (this conclusion is based on an
+ // existing unit test).
+ try {
+ util.parse(dataPart2, null);
+ } catch (NumberParseException e2) {
+ // Number 2 also does not have a country. Collapse.
+ break;
+ }
+ }
+ return false;
+ case SHORT_NSN_MATCH:
+ return false;
+ default:
+ throw new IllegalStateException("Unknown result value from phone number " + "library");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns the {@link android.graphics.Rect} with left, top, right, and bottom coordinates that
+ * are equivalent to the given {@link android.view.View}'s bounds. This is equivalent to how the
+ * target {@link android.graphics.Rect} is calculated in {@link
+ * android.provider.ContactsContract.QuickContact#showQuickContact}.
+ */
+ public static Rect getTargetRectFromView(View view) {
+ final int[] pos = new int[2];
+ view.getLocationOnScreen(pos);
+
+ final Rect rect = new Rect();
+ rect.left = pos[0];
+ rect.top = pos[1];
+ rect.right = pos[0] + view.getWidth();
+ rect.bottom = pos[1] + view.getHeight();
+ return rect;
+ }
+
+ /**
+ * Returns a header view based on the R.layout.list_separator, where the containing {@link
+ * android.widget.TextView} is set using the given textResourceId.
+ */
+ public static TextView createHeaderView(Context context, int textResourceId) {
+ final TextView textView = (TextView) View.inflate(context, R.layout.list_separator, null);
+ textView.setText(context.getString(textResourceId));
+ return textView;
+ }
+
+ /**
+ * Set the top padding on the header view dynamically, based on whether the header is in the first
+ * row or not.
+ */
+ public static void setHeaderViewBottomPadding(
+ Context context, TextView textView, boolean isFirstRow) {
+ final int topPadding;
+ if (isFirstRow) {
+ topPadding =
+ (int)
+ context
+ .getResources()
+ .getDimension(R.dimen.frequently_contacted_title_top_margin_when_first_row);
+ } else {
+ topPadding =
+ (int) context.getResources().getDimension(R.dimen.frequently_contacted_title_top_margin);
+ }
+ textView.setPaddingRelative(
+ textView.getPaddingStart(),
+ topPadding,
+ textView.getPaddingEnd(),
+ textView.getPaddingBottom());
+ }
+
+ /**
+ * Returns the intent to launch for the given invitable account type and contact lookup URI. This
+ * will return null if the account type is not invitable (i.e. there is no {@link
+ * AccountType#getInviteContactActivityClassName()} or {@link
+ * AccountType#syncAdapterPackageName}).
+ */
+ public static Intent getInvitableIntent(AccountType accountType, Uri lookupUri) {
+ String syncAdapterPackageName = accountType.syncAdapterPackageName;
+ String className = accountType.getInviteContactActivityClassName();
+ if (TextUtils.isEmpty(syncAdapterPackageName) || TextUtils.isEmpty(className)) {
+ return null;
+ }
+ Intent intent = new Intent();
+ intent.setClassName(syncAdapterPackageName, className);
+
+ intent.setAction(ContactsContract.Intents.INVITE_CONTACT);
+
+ // Data is the lookup URI.
+ intent.setData(lookupUri);
+ return intent;
+ }
+}
diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindings.java b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java
new file mode 100644
index 000000000..44be53b3f
--- /dev/null
+++ b/java/com/android/contacts/common/bindings/ContactsCommonBindings.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.bindings;
+
+import android.support.annotation.Nullable;
+
+/** Allows the container application to customize the contacts common library. */
+public interface ContactsCommonBindings {
+
+ /** Builds a user agent string for the current application. */
+ @Nullable
+ String getUserAgent();
+}
diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java
new file mode 100644
index 000000000..8958ad997
--- /dev/null
+++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsFactory.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.bindings;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the contacts common
+ * module to get references to the ContactsCommonBindings.
+ */
+public interface ContactsCommonBindingsFactory {
+
+ ContactsCommonBindings newContactsCommonBindings();
+}
diff --git a/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java
new file mode 100644
index 000000000..f2e21b18e
--- /dev/null
+++ b/java/com/android/contacts/common/bindings/ContactsCommonBindingsStub.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.bindings;
+
+import android.support.annotation.Nullable;
+
+/** Default implementation for contacts common bindings. */
+public class ContactsCommonBindingsStub implements ContactsCommonBindings {
+
+ @Override
+ @Nullable
+ public String getUserAgent() {
+ return null;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/CallCompat.java b/java/com/android/contacts/common/compat/CallCompat.java
new file mode 100644
index 000000000..641f7b1bd
--- /dev/null
+++ b/java/com/android/contacts/common/compat/CallCompat.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+
+/** Compatibility utilities for android.telecom.Call */
+public class CallCompat {
+
+ public static boolean canPullExternalCall(@NonNull android.telecom.Call call) {
+ return VERSION.SDK_INT >= VERSION_CODES.N_MR1
+ && ((call.getDetails().getCallCapabilities() & Details.CAPABILITY_CAN_PULL_CALL)
+ == Details.CAPABILITY_CAN_PULL_CALL);
+ }
+
+ /** android.telecom.Call.Details */
+ public static class Details {
+
+ public static final int PROPERTY_IS_EXTERNAL_CALL = Call.Details.PROPERTY_IS_EXTERNAL_CALL;
+ public static final int PROPERTY_ENTERPRISE_CALL = Call.Details.PROPERTY_ENTERPRISE_CALL;
+ public static final int CAPABILITY_CAN_PULL_CALL = Call.Details.CAPABILITY_CAN_PULL_CALL;
+ public static final int CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO =
+ Call.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO;
+
+ public static final String EXTRA_ANSWERING_DROPS_FOREGROUND_CALL =
+ "android.telecom.extra.ANSWERING_DROPS_FG_CALL";
+ }
+}
diff --git a/java/com/android/contacts/common/compat/CallableCompat.java b/java/com/android/contacts/common/compat/CallableCompat.java
new file mode 100644
index 000000000..5e86f518e
--- /dev/null
+++ b/java/com/android/contacts/common/compat/CallableCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+
+public class CallableCompat {
+
+ // TODO: Use N APIs
+ private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+ Uri.withAppendedPath(Callable.CONTENT_URI, "filter_enterprise");
+
+ public static Uri getContentFilterUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return ENTERPRISE_CONTENT_FILTER_URI;
+ }
+ return Callable.CONTENT_FILTER_URI;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/ContactsCompat.java b/java/com/android/contacts/common/compat/ContactsCompat.java
new file mode 100644
index 000000000..39d0b55d3
--- /dev/null
+++ b/java/com/android/contacts/common/compat/ContactsCompat.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import com.android.dialer.compat.CompatUtils;
+
+/** Compatibility class for {@link ContactsContract.Contacts} */
+public class ContactsCompat {
+
+ // TODO: Use N APIs
+ private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+ Uri.withAppendedPath(Contacts.CONTENT_URI, "filter_enterprise");
+ // Copied from ContactsContract.Contacts#ENTERPRISE_CONTACT_ID_BASE, which is hidden.
+ private static final long ENTERPRISE_CONTACT_ID_BASE = 1000000000;
+
+ /** Not instantiable. */
+ private ContactsCompat() {}
+
+ public static Uri getContentUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return ENTERPRISE_CONTENT_FILTER_URI;
+ }
+ return Contacts.CONTENT_FILTER_URI;
+ }
+
+ /**
+ * Return {@code true} if a contact ID is from the contacts provider on the enterprise profile.
+ */
+ public static boolean isEnterpriseContactId(long contactId) {
+ if (CompatUtils.isLollipopCompatible()) {
+ return Contacts.isEnterpriseContactId(contactId);
+ } else {
+ // copied from ContactsContract.Contacts.isEnterpriseContactId
+ return (contactId >= ENTERPRISE_CONTACT_ID_BASE)
+ && (contactId < ContactsContract.Profile.MIN_ID);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/compat/DirectoryCompat.java b/java/com/android/contacts/common/compat/DirectoryCompat.java
new file mode 100644
index 000000000..85f4a4202
--- /dev/null
+++ b/java/com/android/contacts/common/compat/DirectoryCompat.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Directory;
+
+public class DirectoryCompat {
+
+ public static Uri getContentUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return Directory.ENTERPRISE_CONTENT_URI;
+ }
+ return Directory.CONTENT_URI;
+ }
+
+ public static boolean isInvisibleDirectory(long directoryId) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return (directoryId == Directory.LOCAL_INVISIBLE
+ || directoryId == Directory.ENTERPRISE_LOCAL_INVISIBLE);
+ }
+ return directoryId == Directory.LOCAL_INVISIBLE;
+ }
+
+ public static boolean isRemoteDirectoryId(long directoryId) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return Directory.isRemoteDirectoryId(directoryId);
+ }
+ return !(directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE);
+ }
+
+ public static boolean isEnterpriseDirectoryId(long directoryId) {
+ return VERSION.SDK_INT >= VERSION_CODES.N && Directory.isEnterpriseDirectoryId(directoryId);
+ }
+}
diff --git a/java/com/android/contacts/common/compat/PhoneAccountCompat.java b/java/com/android/contacts/common/compat/PhoneAccountCompat.java
new file mode 100644
index 000000000..6a24ec033
--- /dev/null
+++ b/java/com/android/contacts/common/compat/PhoneAccountCompat.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.contacts.common.compat;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.util.Log;
+import com.android.dialer.compat.CompatUtils;
+
+/** Compatiblity class for {@link android.telecom.PhoneAccount} */
+public class PhoneAccountCompat {
+
+ private static final String TAG = PhoneAccountCompat.class.getSimpleName();
+
+ /**
+ * Gets the {@link Icon} associated with the given {@link PhoneAccount}
+ *
+ * @param phoneAccount the PhoneAccount from which to retrieve the Icon
+ * @return the Icon, or null
+ */
+ @Nullable
+ public static Icon getIcon(@Nullable PhoneAccount phoneAccount) {
+ if (phoneAccount == null) {
+ return null;
+ }
+
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return phoneAccount.getIcon();
+ }
+
+ return null;
+ }
+
+ /**
+ * Builds and returns an icon {@code Drawable} to represent this {@code PhoneAccount} in a user
+ * interface.
+ *
+ * @param phoneAccount the PhoneAccount from which to build the icon.
+ * @param context A {@code Context} to use for loading Drawables.
+ * @return An icon for this PhoneAccount, or null
+ */
+ @Nullable
+ public static Drawable createIconDrawable(
+ @Nullable PhoneAccount phoneAccount, @Nullable Context context) {
+ if (phoneAccount == null || context == null) {
+ return null;
+ }
+
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return createIconDrawableMarshmallow(phoneAccount, context);
+ }
+
+ if (CompatUtils.isLollipopMr1Compatible()) {
+ return createIconDrawableLollipopMr1(phoneAccount, context);
+ }
+ return null;
+ }
+
+ @Nullable
+ private static Drawable createIconDrawableMarshmallow(
+ PhoneAccount phoneAccount, Context context) {
+ Icon accountIcon = getIcon(phoneAccount);
+ if (accountIcon == null) {
+ return null;
+ }
+ return accountIcon.loadDrawable(context);
+ }
+
+ @Nullable
+ private static Drawable createIconDrawableLollipopMr1(
+ PhoneAccount phoneAccount, Context context) {
+ try {
+ return (Drawable)
+ PhoneAccount.class
+ .getMethod("createIconDrawable", Context.class)
+ .invoke(phoneAccount, context);
+ } catch (ReflectiveOperationException e) {
+ return null;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when attempting to call "
+ + "android.telecom.PhoneAccount#createIconDrawable",
+ t);
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/compat/PhoneCompat.java b/java/com/android/contacts/common/compat/PhoneCompat.java
new file mode 100644
index 000000000..31db7b537
--- /dev/null
+++ b/java/com/android/contacts/common/compat/PhoneCompat.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+
+public class PhoneCompat {
+
+ // TODO: Use N APIs
+ private static final Uri ENTERPRISE_CONTENT_FILTER_URI =
+ Uri.withAppendedPath(Phone.CONTENT_URI, "filter_enterprise");
+
+ public static Uri getContentFilterUri() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return ENTERPRISE_CONTENT_FILTER_URI;
+ }
+ return Phone.CONTENT_FILTER_URI;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
new file mode 100644
index 000000000..960b340d8
--- /dev/null
+++ b/java/com/android/contacts/common/compat/PhoneNumberUtilsCompat.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.telephony.PhoneNumberUtils;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import com.android.dialer.compat.CompatUtils;
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+
+/**
+ * This class contains static utility methods extracted from PhoneNumberUtils, and the methods were
+ * added in API level 23. In this way, we could enable the corresponding functionality for pre-M
+ * devices. We need maintain this class and keep it synced with PhoneNumberUtils. Another thing to
+ * keep in mind is that we use com.google.i18n rather than com.android.i18n in here, so we need make
+ * sure the application behavior is preserved.
+ */
+public class PhoneNumberUtilsCompat {
+
+ /** Not instantiable. */
+ private PhoneNumberUtilsCompat() {}
+
+ public static String normalizeNumber(String phoneNumber) {
+ if (CompatUtils.isLollipopCompatible()) {
+ return PhoneNumberUtils.normalizeNumber(phoneNumber);
+ } else {
+ return normalizeNumberInternal(phoneNumber);
+ }
+ }
+
+ /** Implementation copied from {@link PhoneNumberUtils#normalizeNumber} */
+ private static String normalizeNumberInternal(String phoneNumber) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ return "";
+ }
+ StringBuilder sb = new StringBuilder();
+ int len = phoneNumber.length();
+ for (int i = 0; i < len; i++) {
+ char c = phoneNumber.charAt(i);
+ // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.)
+ int digit = Character.digit(c, 10);
+ if (digit != -1) {
+ sb.append(digit);
+ } else if (sb.length() == 0 && c == '+') {
+ sb.append(c);
+ } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+ return normalizeNumber(PhoneNumberUtils.convertKeypadLettersToDigits(phoneNumber));
+ }
+ }
+ return sb.toString();
+ }
+
+ public static String formatNumber(
+ String phoneNumber, String phoneNumberE164, String defaultCountryIso) {
+ if (CompatUtils.isLollipopCompatible()) {
+ return PhoneNumberUtils.formatNumber(phoneNumber, phoneNumberE164, defaultCountryIso);
+ } else {
+ // This method was deprecated in API level 21, so it's only used on pre-L SDKs.
+ return PhoneNumberUtils.formatNumber(phoneNumber);
+ }
+ }
+
+ public static CharSequence createTtsSpannable(CharSequence phoneNumber) {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return PhoneNumberUtils.createTtsSpannable(phoneNumber);
+ } else {
+ return createTtsSpannableInternal(phoneNumber);
+ }
+ }
+
+ public static TtsSpan createTtsSpan(String phoneNumber) {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return PhoneNumberUtils.createTtsSpan(phoneNumber);
+ } else if (CompatUtils.isLollipopCompatible()) {
+ return createTtsSpanLollipop(phoneNumber);
+ } else {
+ return null;
+ }
+ }
+
+ /** Copied from {@link PhoneNumberUtils#createTtsSpannable} */
+ private static CharSequence createTtsSpannableInternal(CharSequence phoneNumber) {
+ if (phoneNumber == null) {
+ return null;
+ }
+ Spannable spannable = Spannable.Factory.getInstance().newSpannable(phoneNumber);
+ addTtsSpanInternal(spannable, 0, spannable.length());
+ return spannable;
+ }
+
+ /** Compat method for addTtsSpan, see {@link PhoneNumberUtils#addTtsSpan} */
+ public static void addTtsSpan(Spannable s, int start, int endExclusive) {
+ if (CompatUtils.isMarshmallowCompatible()) {
+ PhoneNumberUtils.addTtsSpan(s, start, endExclusive);
+ } else {
+ addTtsSpanInternal(s, start, endExclusive);
+ }
+ }
+
+ /** Copied from {@link PhoneNumberUtils#addTtsSpan} */
+ private static void addTtsSpanInternal(Spannable s, int start, int endExclusive) {
+ s.setSpan(
+ createTtsSpan(s.subSequence(start, endExclusive).toString()),
+ start,
+ endExclusive,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+
+ /** Copied from {@link PhoneNumberUtils#createTtsSpan} */
+ private static TtsSpan createTtsSpanLollipop(String phoneNumberString) {
+ if (phoneNumberString == null) {
+ return null;
+ }
+
+ // Parse the phone number
+ final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+ PhoneNumber phoneNumber = null;
+ try {
+ // Don't supply a defaultRegion so this fails for non-international numbers because
+ // we don't want to TalkBalk to read a country code (e.g. +1) if it is not already
+ // present
+ phoneNumber = phoneNumberUtil.parse(phoneNumberString, /* defaultRegion */ null);
+ } catch (NumberParseException ignored) {
+ }
+
+ // Build a telephone tts span
+ final TtsSpan.TelephoneBuilder builder = new TtsSpan.TelephoneBuilder();
+ if (phoneNumber == null) {
+ // Strip separators otherwise TalkBack will be silent
+ // (this behavior was observed with TalkBalk 4.0.2 from their alpha channel)
+ builder.setNumberParts(splitAtNonNumerics(phoneNumberString));
+ } else {
+ if (phoneNumber.hasCountryCode()) {
+ builder.setCountryCode(Integer.toString(phoneNumber.getCountryCode()));
+ }
+ builder.setNumberParts(Long.toString(phoneNumber.getNationalNumber()));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Split a phone number using spaces, ignoring anything that is not a digit
+ *
+ * @param number A {@code CharSequence} before splitting, e.g., "+20(123)-456#"
+ * @return A {@code String} after splitting, e.g., "20 123 456".
+ */
+ private static String splitAtNonNumerics(CharSequence number) {
+ StringBuilder sb = new StringBuilder(number.length());
+ for (int i = 0; i < number.length(); i++) {
+ sb.append(PhoneNumberUtils.isISODigit(number.charAt(i)) ? number.charAt(i) : " ");
+ }
+ // It is very important to remove extra spaces. At time of writing, any leading or trailing
+ // spaces, or any sequence of more than one space, will confuse TalkBack and cause the TTS
+ // span to be non-functional!
+ return sb.toString().replaceAll(" +", " ").trim();
+ }
+}
diff --git a/java/com/android/contacts/common/compat/TelephonyManagerCompat.java b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java
new file mode 100644
index 000000000..c8665af51
--- /dev/null
+++ b/java/com/android/contacts/common/compat/TelephonyManagerCompat.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.compat;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import java.lang.reflect.InvocationTargetException;
+
+public class TelephonyManagerCompat {
+
+ // TODO: Use public API for these constants when available
+ public static final String EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE =
+ "android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE";
+ public static final String EVENT_HANDOVER_TO_WIFI_FAILED =
+ "android.telephony.event.EVENT_HANDOVER_TO_WIFI_FAILED";
+ public static final String EVENT_CALL_REMOTELY_HELD = "android.telecom.event.CALL_REMOTELY_HELD";
+ public static final String EVENT_CALL_REMOTELY_UNHELD =
+ "android.telecom.event.CALL_REMOTELY_UNHELD";
+
+ public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager";
+
+ /**
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @return true if the current device is "voice capable".
+ * <p>"Voice capable" means that this device supports circuit-switched (i.e. voice) phone
+ * calls over the telephony network, and is allowed to display the in-call UI while a cellular
+ * voice call is active. This will be false on "data only" devices which can't make voice
+ * calls and don't support any in-call UI.
+ * <p>Note: the meaning of this flag is subtly different from the
+ * PackageManager.FEATURE_TELEPHONY system feature, which is available on any device with a
+ * telephony radio, even if the device is data-only.
+ */
+ public static boolean isVoiceCapable(@Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return false;
+ }
+ if (CompatUtils.isLollipopMr1Compatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isVoiceCapable")) {
+ // isVoiceCapable was unhidden in L-MR1
+ return telephonyManager.isVoiceCapable();
+ }
+ final int phoneType = telephonyManager.getPhoneType();
+ return phoneType == TelephonyManager.PHONE_TYPE_CDMA
+ || phoneType == TelephonyManager.PHONE_TYPE_GSM;
+ }
+
+ /**
+ * Returns the number of phones available. Returns 1 for Single standby mode (Single SIM
+ * functionality) Returns 2 for Dual standby mode.(Dual SIM functionality)
+ *
+ * <p>Returns 1 if the method or telephonyManager is not available.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ */
+ public static int getPhoneCount(@Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return 1;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getPhoneCount")) {
+ return telephonyManager.getPhoneCount();
+ }
+ return 1;
+ }
+
+ /**
+ * Returns the unique device ID of a subscription, for example, the IMEI for GSM and the MEID for
+ * CDMA phones. Return null if device ID is not available.
+ *
+ * <p>Requires Permission: {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @param slotId of which deviceID is returned
+ */
+ public static String getDeviceId(@Nullable TelephonyManager telephonyManager, int slotId) {
+ if (telephonyManager == null) {
+ return null;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getDeviceId", Integer.class)) {
+ return telephonyManager.getDeviceId(slotId);
+ }
+ return null;
+ }
+
+ /**
+ * Whether the phone supports TTY mode.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @return {@code true} if the device supports TTY mode, and {@code false} otherwise.
+ */
+ public static boolean isTtyModeSupported(@Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return false;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isTtyModeSupported")) {
+ return telephonyManager.isTtyModeSupported();
+ }
+ return false;
+ }
+
+ /**
+ * Whether the phone supports hearing aid compatibility.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @return {@code true} if the device supports hearing aid compatibility, and {@code false}
+ * otherwise.
+ */
+ public static boolean isHearingAidCompatibilitySupported(
+ @Nullable TelephonyManager telephonyManager) {
+ if (telephonyManager == null) {
+ return false;
+ }
+ if (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELEPHONY_MANAGER_CLASS, "isHearingAidCompatibilitySupported")) {
+ return telephonyManager.isHearingAidCompatibilitySupported();
+ }
+ return false;
+ }
+
+ /**
+ * Returns the URI for the per-account voicemail ringtone set in Phone settings.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+ * retrieve the voicemail ringtone.
+ * @return The URI for the ringtone to play when receiving a voicemail from a specific
+ * PhoneAccount.
+ */
+ @Nullable
+ public static Uri getVoicemailRingtoneUri(
+ TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) {
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ return null;
+ }
+ return telephonyManager.getVoicemailRingtoneUri(accountHandle);
+ }
+
+ /**
+ * Returns whether vibration is set for voicemail notification in Phone settings.
+ *
+ * @param telephonyManager The telephony manager instance to use for method calls.
+ * @param accountHandle The handle for the {@link android.telecom.PhoneAccount} for which to
+ * retrieve the voicemail vibration setting.
+ * @return {@code true} if the vibration is set for this PhoneAccount, {@code false} otherwise.
+ */
+ public static boolean isVoicemailVibrationEnabled(
+ TelephonyManager telephonyManager, PhoneAccountHandle accountHandle) {
+ return VERSION.SDK_INT < VERSION_CODES.N
+ || telephonyManager.isVoicemailVibrationEnabled(accountHandle);
+ }
+
+ /**
+ * This method uses a new system API to enable or disable visual voicemail. TODO: restrict
+ * to N MR1, not needed in future SDK.
+ */
+ public static void setVisualVoicemailEnabled(
+ TelephonyManager telephonyManager, PhoneAccountHandle handle, boolean enabled) {
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ Assert.fail("setVisualVoicemailEnabled called on pre-NMR1");
+ }
+ try {
+ TelephonyManager.class
+ .getMethod("setVisualVoicemailEnabled", PhoneAccountHandle.class, boolean.class)
+ .invoke(telephonyManager, handle, enabled);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e);
+ }
+ }
+
+ /**
+ * This method uses a new system API to check if visual voicemail is enabled TODO: restrict
+ * to N MR1, not needed in future SDK.
+ */
+ public static boolean isVisualVoicemailEnabled(
+ TelephonyManager telephonyManager, PhoneAccountHandle handle) {
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ Assert.fail("isVisualVoicemailEnabled called on pre-NMR1");
+ }
+ try {
+ return (boolean)
+ TelephonyManager.class
+ .getMethod("isVisualVoicemailEnabled", PhoneAccountHandle.class)
+ .invoke(telephonyManager, handle);
+ } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ LogUtil.e("TelephonyManagerCompat.setVisualVoicemailEnabled", "failed", e);
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
new file mode 100644
index 000000000..5687f6fbf
--- /dev/null
+++ b/java/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.compat.telecom;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.dialer.compat.CompatUtils;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Compatibility class for {@link android.telecom.TelecomManager}. */
+public class TelecomManagerCompat {
+
+ public static final String TELECOM_MANAGER_CLASS = "android.telecom.TelecomManager";
+
+ // TODO: remove once this is available in android.telecom.Call
+ // b/33779976
+ public static final String EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS =
+ "android.telecom.extra.LAST_EMERGENCY_CALLBACK_TIME_MILLIS";
+
+ /**
+ * Places a new outgoing call to the provided address using the system telecom service with the
+ * specified intent.
+ *
+ * @param activity {@link Activity} used to start another activity for the given intent
+ * @param telecomManager the {@link TelecomManager} used to place a call, if possible
+ * @param intent the intent for the call
+ */
+ public static void placeCall(
+ @Nullable Activity activity,
+ @Nullable TelecomManager telecomManager,
+ @Nullable Intent intent) {
+ if (activity == null || telecomManager == null || intent == null) {
+ return;
+ }
+ if (CompatUtils.isMarshmallowCompatible()) {
+ telecomManager.placeCall(intent.getData(), intent.getExtras());
+ return;
+ }
+ activity.startActivityForResult(intent, 0);
+ }
+
+ /**
+ * Get the URI for running an adn query.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @param accountHandle The handle for the account to derive an adn query URI for or {@code null}
+ * to return a URI which will use the default account.
+ * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount} for
+ * the the content retrieve.
+ */
+ public static Uri getAdnUriForPhoneAccount(
+ @Nullable TelecomManager telecomManager, PhoneAccountHandle accountHandle) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getAdnUriForPhoneAccount", PhoneAccountHandle.class))) {
+ return telecomManager.getAdnUriForPhoneAccount(accountHandle);
+ }
+ return Uri.parse("content://icc/adn");
+ }
+
+ /**
+ * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
+ * calls. The returned list includes only those accounts which have been explicitly enabled by the
+ * user.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @return A list of PhoneAccountHandle objects.
+ */
+ public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(
+ @Nullable TelecomManager telecomManager) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getCallCapablePhoneAccounts"))) {
+ return telecomManager.getCallCapablePhoneAccounts();
+ }
+ return new ArrayList<>();
+ }
+
+ /**
+ * Used to determine the currently selected default dialer package.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @return package name for the default dialer package or null if no package has been selected as
+ * the default dialer.
+ */
+ @Nullable
+ public static String getDefaultDialerPackage(@Nullable TelecomManager telecomManager) {
+ if (telecomManager != null && CompatUtils.isDefaultDialerCompatible()) {
+ return telecomManager.getDefaultDialerPackage();
+ }
+ return null;
+ }
+
+ /**
+ * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with
+ * the specified {@code uriScheme}. This PhoneAccount will always be a member of the list which is
+ * returned from invoking {@link TelecomManager#getCallCapablePhoneAccounts()}. The specific
+ * account returned depends on the following priorities:
+ *
+ * <p>1. If the user-selected default PhoneAccount supports the specified scheme, it will be
+ * returned. 2. If there exists only one PhoneAccount that supports the specified scheme, it will
+ * be returned.
+ *
+ * <p>If no PhoneAccount fits the criteria above, this method will return {@code null}.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @param uriScheme The URI scheme.
+ * @return The {@link PhoneAccountHandle} corresponding to the account to be used.
+ */
+ @Nullable
+ public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
+ @Nullable TelecomManager telecomManager, @Nullable String uriScheme) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getDefaultOutgoingPhoneAccount", String.class))) {
+ return telecomManager.getDefaultOutgoingPhoneAccount(uriScheme);
+ }
+ return null;
+ }
+
+ /**
+ * Return the line 1 phone number for given phone account.
+ *
+ * @param telecomManager the {@link TelecomManager} to use in the event that {@link
+ * TelecomManager#getLine1Number(PhoneAccountHandle)} is available
+ * @param telephonyManager the {@link TelephonyManager} to use if TelecomManager#getLine1Number is
+ * unavailable
+ * @param phoneAccountHandle the phoneAccountHandle upon which to check the line one number
+ * @return the line one number
+ */
+ @Nullable
+ public static String getLine1Number(
+ @Nullable TelecomManager telecomManager,
+ @Nullable TelephonyManager telephonyManager,
+ @Nullable PhoneAccountHandle phoneAccountHandle) {
+ if (telecomManager != null && CompatUtils.isMarshmallowCompatible()) {
+ return telecomManager.getLine1Number(phoneAccountHandle);
+ }
+ if (telephonyManager != null) {
+ return telephonyManager.getLine1Number();
+ }
+ return null;
+ }
+
+ /**
+ * Return whether a given phone number is the configured voicemail number for a particular phone
+ * account.
+ *
+ * @param telecomManager the {@link TelecomManager} to use for checking the number.
+ * @param accountHandle The handle for the account to check the voicemail number against
+ * @param number The number to look up.
+ */
+ public static boolean isVoiceMailNumber(
+ @Nullable TelecomManager telecomManager,
+ @Nullable PhoneAccountHandle accountHandle,
+ @Nullable String number) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS,
+ "isVoiceMailNumber",
+ PhoneAccountHandle.class,
+ String.class))) {
+ return telecomManager.isVoiceMailNumber(accountHandle, number);
+ }
+ return PhoneNumberUtils.isVoiceMailNumber(number);
+ }
+
+ /**
+ * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes
+ * resources which can be used in a user interface.
+ *
+ * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+ * @param account The {@link PhoneAccountHandle}.
+ * @return The {@link PhoneAccount} object or null if it doesn't exist.
+ */
+ @Nullable
+ public static PhoneAccount getPhoneAccount(
+ @Nullable TelecomManager telecomManager, @Nullable PhoneAccountHandle accountHandle) {
+ if (telecomManager != null
+ && (CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getPhoneAccount", PhoneAccountHandle.class))) {
+ return telecomManager.getPhoneAccount(accountHandle);
+ }
+ return null;
+ }
+
+ /**
+ * Return the voicemail number for a given phone account.
+ *
+ * @param telecomManager The {@link TelecomManager} object to use for retrieving the voicemail
+ * number if accountHandle is specified.
+ * @param telephonyManager The {@link TelephonyManager} object to use for retrieving the voicemail
+ * number if accountHandle is null.
+ * @param accountHandle The handle for the phone account.
+ * @return The voicemail number for the phone account, and {@code null} if one has not been
+ * configured.
+ */
+ @Nullable
+ public static String getVoiceMailNumber(
+ @Nullable TelecomManager telecomManager,
+ @Nullable TelephonyManager telephonyManager,
+ @Nullable PhoneAccountHandle accountHandle) {
+ if (telecomManager != null
+ && (CompatUtils.isMethodAvailable(
+ TELECOM_MANAGER_CLASS, "getVoiceMailNumber", PhoneAccountHandle.class))) {
+ return telecomManager.getVoiceMailNumber(accountHandle);
+ } else if (telephonyManager != null) {
+ return telephonyManager.getVoiceMailNumber();
+ }
+ return null;
+ }
+
+ /**
+ * Processes the specified dial string as an MMI code. MMI codes are any sequence of characters
+ * entered into the dialpad that contain a "*" or "#". Some of these sequences launch special
+ * behavior through handled by Telephony.
+ *
+ * @param telecomManager The {@link TelecomManager} object to use for handling MMI.
+ * @param dialString The digits to dial.
+ * @return {@code true} if the digits were processed as an MMI code, {@code false} otherwise.
+ */
+ public static boolean handleMmi(
+ @Nullable TelecomManager telecomManager,
+ @Nullable String dialString,
+ @Nullable PhoneAccountHandle accountHandle) {
+ if (telecomManager == null || TextUtils.isEmpty(dialString)) {
+ return false;
+ }
+ if (CompatUtils.isMarshmallowCompatible()) {
+ return telecomManager.handleMmi(dialString, accountHandle);
+ }
+
+ Object handleMmiResult =
+ CompatUtils.invokeMethod(
+ telecomManager,
+ "handleMmi",
+ new Class<?>[] {PhoneAccountHandle.class, String.class},
+ new Object[] {accountHandle, dialString});
+ if (handleMmiResult != null) {
+ return (boolean) handleMmiResult;
+ }
+
+ return telecomManager.handleMmi(dialString);
+ }
+
+ /**
+ * Silences the ringer if a ringing call exists. Noop if {@link TelecomManager#silenceRinger()} is
+ * unavailable.
+ *
+ * @param telecomManager the TelecomManager to use to silence the ringer.
+ */
+ public static void silenceRinger(@Nullable TelecomManager telecomManager) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "silenceRinger"))) {
+ telecomManager.silenceRinger();
+ }
+ }
+
+ /**
+ * Returns the current SIM call manager. Apps must be prepared for this method to return null,
+ * indicating that there currently exists no registered SIM call manager.
+ *
+ * @param telecomManager the {@link TelecomManager} to use to fetch the SIM call manager.
+ * @return The phone account handle of the current sim call manager.
+ */
+ @Nullable
+ public static PhoneAccountHandle getSimCallManager(TelecomManager telecomManager) {
+ if (telecomManager != null
+ && (CompatUtils.isMarshmallowCompatible()
+ || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "getSimCallManager"))) {
+ return telecomManager.getSimCallManager();
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/contacts/common/database/ContactUpdateUtils.java b/java/com/android/contacts/common/database/ContactUpdateUtils.java
new file mode 100644
index 000000000..1a9febc07
--- /dev/null
+++ b/java/com/android/contacts/common/database/ContactUpdateUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.database;
+
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+/** Static methods to update contact information. */
+public class ContactUpdateUtils {
+
+ private static final String TAG = ContactUpdateUtils.class.getSimpleName();
+
+ public static void setSuperPrimary(Context context, long dataId) {
+ if (dataId == -1) {
+ Log.e(TAG, "Invalid arguments for setSuperPrimary request");
+ return;
+ }
+
+ // Update the primary values in the data record.
+ ContentValues values = new ContentValues(2);
+ values.put(ContactsContract.Data.IS_SUPER_PRIMARY, 1);
+ values.put(ContactsContract.Data.IS_PRIMARY, 1);
+
+ context
+ .getContentResolver()
+ .update(
+ ContentUris.withAppendedId(ContactsContract.Data.CONTENT_URI, dataId),
+ values,
+ null,
+ null);
+ }
+}
diff --git a/java/com/android/contacts/common/database/EmptyCursor.java b/java/com/android/contacts/common/database/EmptyCursor.java
new file mode 100644
index 000000000..c2b24cdf7
--- /dev/null
+++ b/java/com/android/contacts/common/database/EmptyCursor.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.database;
+
+import android.database.AbstractCursor;
+import android.database.CursorIndexOutOfBoundsException;
+
+/**
+ * A cursor that is empty.
+ *
+ * <p>If you want an empty cursor, this class is better than a MatrixCursor because it has less
+ * overhead.
+ */
+public final class EmptyCursor extends AbstractCursor {
+
+ private String[] mColumns;
+
+ public EmptyCursor(String[] columns) {
+ this.mColumns = columns;
+ }
+
+ @Override
+ public int getCount() {
+ return 0;
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumns;
+ }
+
+ @Override
+ public String getString(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public short getShort(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public int getInt(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public long getLong(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public float getFloat(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public double getDouble(int column) {
+ throw cursorException();
+ }
+
+ @Override
+ public boolean isNull(int column) {
+ throw cursorException();
+ }
+
+ private CursorIndexOutOfBoundsException cursorException() {
+ return new CursorIndexOutOfBoundsException("Operation not permitted on an empty cursor.");
+ }
+}
diff --git a/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java
new file mode 100644
index 000000000..d5e61354a
--- /dev/null
+++ b/java/com/android/contacts/common/database/NoNullCursorAsyncQueryHandler.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.database;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.net.Uri;
+
+/**
+ * An {@AsyncQueryHandler} that will never return a null cursor.
+ *
+ * <p>Instead, will return a {@link Cursor} with 0 records.
+ */
+public abstract class NoNullCursorAsyncQueryHandler extends AsyncQueryHandler {
+
+ public NoNullCursorAsyncQueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ public void startQuery(
+ int token,
+ Object cookie,
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String orderBy) {
+ final CookieWithProjection projectionCookie = new CookieWithProjection(cookie, projection);
+ super.startQuery(token, projectionCookie, uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ @Override
+ protected final void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ CookieWithProjection projectionCookie = (CookieWithProjection) cookie;
+
+ super.onQueryComplete(token, projectionCookie.originalCookie, cursor);
+
+ if (cursor == null) {
+ cursor = new EmptyCursor(projectionCookie.projection);
+ }
+ onNotNullableQueryComplete(token, projectionCookie.originalCookie, cursor);
+ }
+
+ protected abstract void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor);
+
+ /** Class to add projection to an existing cookie. */
+ private static class CookieWithProjection {
+
+ public final Object originalCookie;
+ public final String[] projection;
+
+ public CookieWithProjection(Object cookie, String[] projection) {
+ this.originalCookie = cookie;
+ this.projection = projection;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/dialog/CallSubjectDialog.java b/java/com/android/contacts/common/dialog/CallSubjectDialog.java
new file mode 100644
index 000000000..d2e3a2357
--- /dev/null
+++ b/java/com/android/contacts/common/dialog/CallSubjectDialog.java
@@ -0,0 +1,607 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.dialog;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.ViewUtil;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements a dialog which prompts for a call subject for an outgoing call. The dialog includes a
+ * pop up list of historical call subjects.
+ */
+public class CallSubjectDialog extends Activity {
+
+ public static final String PREF_KEY_SUBJECT_HISTORY_COUNT = "subject_history_count";
+ public static final String PREF_KEY_SUBJECT_HISTORY_ITEM = "subject_history_item";
+ /** Activity intent argument bundle keys: */
+ public static final String ARG_PHOTO_ID = "PHOTO_ID";
+
+ public static final String ARG_PHOTO_URI = "PHOTO_URI";
+ public static final String ARG_CONTACT_URI = "CONTACT_URI";
+ public static final String ARG_NAME_OR_NUMBER = "NAME_OR_NUMBER";
+ public static final String ARG_IS_BUSINESS = "IS_BUSINESS";
+ public static final String ARG_NUMBER = "NUMBER";
+ public static final String ARG_DISPLAY_NUMBER = "DISPLAY_NUMBER";
+ public static final String ARG_NUMBER_LABEL = "NUMBER_LABEL";
+ public static final String ARG_PHONE_ACCOUNT_HANDLE = "PHONE_ACCOUNT_HANDLE";
+ private static final int CALL_SUBJECT_LIMIT = 16;
+ private static final int CALL_SUBJECT_HISTORY_SIZE = 5;
+ private int mAnimationDuration;
+ private Charset mMessageEncoding;
+ private View mBackgroundView;
+ private View mDialogView;
+ private QuickContactBadge mContactPhoto;
+ private TextView mNameView;
+ private TextView mNumberView;
+ private EditText mCallSubjectView;
+ private TextView mCharacterLimitView;
+ private View mHistoryButton;
+ private View mSendAndCallButton;
+ private ListView mSubjectList;
+
+ private int mLimit = CALL_SUBJECT_LIMIT;
+ /** Handles changes to the text in the subject box. Ensures the character limit is updated. */
+ private final TextWatcher mTextWatcher =
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // no-op
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateCharacterLimit();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // no-op
+ }
+ };
+
+ private int mPhotoSize;
+ private SharedPreferences mPrefs;
+ private List<String> mSubjectHistory;
+ /** Handles displaying the list of past call subjects. */
+ private final View.OnClickListener mHistoryOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ hideSoftKeyboard(CallSubjectDialog.this, mCallSubjectView);
+ showCallHistory(mSubjectList.getVisibility() == View.GONE);
+ }
+ };
+ /**
+ * Handles auto-hiding the call history when user clicks in the call subject field to give it
+ * focus.
+ */
+ private final View.OnClickListener mCallSubjectClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mSubjectList.getVisibility() == View.VISIBLE) {
+ showCallHistory(false);
+ }
+ }
+ };
+
+ private long mPhotoID;
+ private Uri mPhotoUri;
+ private Uri mContactUri;
+ private String mNameOrNumber;
+ private boolean mIsBusiness;
+ private String mNumber;
+ private String mDisplayNumber;
+ private String mNumberLabel;
+ private PhoneAccountHandle mPhoneAccountHandle;
+ /** Handles starting a call with a call subject specified. */
+ private final View.OnClickListener mSendAndCallOnClickListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String subject = mCallSubjectView.getText().toString();
+ Intent intent =
+ new CallIntentBuilder(mNumber, CallInitiationType.Type.CALL_SUBJECT_DIALOG)
+ .setPhoneAccountHandle(mPhoneAccountHandle)
+ .setCallSubject(subject)
+ .build();
+
+ TelecomManagerCompat.placeCall(
+ CallSubjectDialog.this,
+ (TelecomManager) getSystemService(Context.TELECOM_SERVICE),
+ intent);
+
+ mSubjectHistory.add(subject);
+ saveSubjectHistory(mSubjectHistory);
+ finish();
+ }
+ };
+ /** Click listener which handles user clicks outside of the dialog. */
+ private View.OnClickListener mBackgroundListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ };
+ /**
+ * Item click listener which handles user clicks on the items in the list view. Dismisses the
+ * activity, returning the subject to the caller and closing the activity with the {@link
+ * Activity#RESULT_OK} result code.
+ */
+ private AdapterView.OnItemClickListener mItemClickListener =
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view, int position, long arg3) {
+ mCallSubjectView.setText(mSubjectHistory.get(position));
+ showCallHistory(false);
+ }
+ };
+
+ /**
+ * Show the call subject dialog given a phone number to dial (e.g. from the dialpad).
+ *
+ * @param activity The activity.
+ * @param number The number to dial.
+ */
+ public static void start(Activity activity, String number) {
+ start(
+ activity,
+ -1 /* photoId */,
+ null /* photoUri */,
+ null /* contactUri */,
+ number /* nameOrNumber */,
+ false /* isBusiness */,
+ number /* number */,
+ null /* displayNumber */,
+ null /* numberLabel */,
+ null /* phoneAccountHandle */);
+ }
+
+ /**
+ * Creates a call subject dialog.
+ *
+ * @param activity The current activity.
+ * @param photoId The photo ID (used to populate contact photo).
+ * @param photoUri The photo Uri (used to populate contact photo).
+ * @param contactUri The Contact URI (used so quick contact can be invoked from contact photo).
+ * @param nameOrNumber The name or number of the callee.
+ * @param isBusiness {@code true} if a business is being called (used for contact photo).
+ * @param number The raw number to dial.
+ * @param displayNumber The number to dial, formatted for display.
+ * @param numberLabel The label for the number (if from a contact).
+ * @param phoneAccountHandle The phone account handle.
+ */
+ public static void start(
+ Activity activity,
+ long photoId,
+ Uri photoUri,
+ Uri contactUri,
+ String nameOrNumber,
+ boolean isBusiness,
+ String number,
+ String displayNumber,
+ String numberLabel,
+ PhoneAccountHandle phoneAccountHandle) {
+ Bundle arguments = new Bundle();
+ arguments.putLong(ARG_PHOTO_ID, photoId);
+ arguments.putParcelable(ARG_PHOTO_URI, photoUri);
+ arguments.putParcelable(ARG_CONTACT_URI, contactUri);
+ arguments.putString(ARG_NAME_OR_NUMBER, nameOrNumber);
+ arguments.putBoolean(ARG_IS_BUSINESS, isBusiness);
+ arguments.putString(ARG_NUMBER, number);
+ arguments.putString(ARG_DISPLAY_NUMBER, displayNumber);
+ arguments.putString(ARG_NUMBER_LABEL, numberLabel);
+ arguments.putParcelable(ARG_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ start(activity, arguments);
+ }
+
+ /**
+ * Shows the call subject dialog given a Bundle containing all the arguments required to display
+ * the dialog (e.g. from Quick Contacts).
+ *
+ * @param activity The activity.
+ * @param arguments The arguments bundle.
+ */
+ public static void start(Activity activity, Bundle arguments) {
+ Intent intent = new Intent(activity, CallSubjectDialog.class);
+ intent.putExtras(arguments);
+ activity.startActivity(intent);
+ }
+
+ /**
+ * Loads the subject history from shared preferences.
+ *
+ * @param prefs Shared preferences.
+ * @return List of subject history strings.
+ */
+ public static List<String> loadSubjectHistory(SharedPreferences prefs) {
+ int historySize = prefs.getInt(PREF_KEY_SUBJECT_HISTORY_COUNT, 0);
+ List<String> subjects = new ArrayList(historySize);
+
+ for (int ix = 0; ix < historySize; ix++) {
+ String historyItem = prefs.getString(PREF_KEY_SUBJECT_HISTORY_ITEM + ix, null);
+ if (!TextUtils.isEmpty(historyItem)) {
+ subjects.add(historyItem);
+ }
+ }
+
+ return subjects;
+ }
+
+ /**
+ * Creates the dialog, inflating the layout and populating it with the name and phone number.
+ *
+ * @param savedInstanceState The last saved instance state of the Fragment, or null if this is a
+ * freshly created Fragment.
+ * @return Dialog instance.
+ */
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mAnimationDuration = getResources().getInteger(R.integer.call_subject_animation_duration);
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+ mPhotoSize =
+ getResources().getDimensionPixelSize(R.dimen.call_subject_dialog_contact_photo_size);
+ readArguments();
+ loadConfiguration();
+ mSubjectHistory = loadSubjectHistory(mPrefs);
+
+ setContentView(R.layout.dialog_call_subject);
+ getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ mBackgroundView = findViewById(R.id.call_subject_dialog);
+ mBackgroundView.setOnClickListener(mBackgroundListener);
+ mDialogView = findViewById(R.id.dialog_view);
+ mContactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
+ mNameView = (TextView) findViewById(R.id.name);
+ mNumberView = (TextView) findViewById(R.id.number);
+ mCallSubjectView = (EditText) findViewById(R.id.call_subject);
+ mCallSubjectView.addTextChangedListener(mTextWatcher);
+ mCallSubjectView.setOnClickListener(mCallSubjectClickListener);
+ InputFilter[] filters = new InputFilter[1];
+ filters[0] = new InputFilter.LengthFilter(mLimit);
+ mCallSubjectView.setFilters(filters);
+ mCharacterLimitView = (TextView) findViewById(R.id.character_limit);
+ mHistoryButton = findViewById(R.id.history_button);
+ mHistoryButton.setOnClickListener(mHistoryOnClickListener);
+ mHistoryButton.setVisibility(mSubjectHistory.isEmpty() ? View.GONE : View.VISIBLE);
+ mSendAndCallButton = findViewById(R.id.send_and_call_button);
+ mSendAndCallButton.setOnClickListener(mSendAndCallOnClickListener);
+ mSubjectList = (ListView) findViewById(R.id.subject_list);
+ mSubjectList.setOnItemClickListener(mItemClickListener);
+ mSubjectList.setVisibility(View.GONE);
+
+ updateContactInfo();
+ updateCharacterLimit();
+ }
+
+ /** Populates the contact info fields based on the current contact information. */
+ private void updateContactInfo() {
+ if (mContactUri != null) {
+ setPhoto(mPhotoID, mPhotoUri, mContactUri, mNameOrNumber, mIsBusiness);
+ } else {
+ mContactPhoto.setVisibility(View.GONE);
+ }
+ mNameView.setText(mNameOrNumber);
+ if (!TextUtils.isEmpty(mNumberLabel) && !TextUtils.isEmpty(mDisplayNumber)) {
+ mNumberView.setVisibility(View.VISIBLE);
+ mNumberView.setText(
+ getString(R.string.call_subject_type_and_number, mNumberLabel, mDisplayNumber));
+ } else {
+ mNumberView.setVisibility(View.GONE);
+ mNumberView.setText(null);
+ }
+ }
+
+ /** Reads arguments from the fragment arguments and populates the necessary instance variables. */
+ private void readArguments() {
+ Bundle arguments = getIntent().getExtras();
+ if (arguments == null) {
+ LogUtil.e("CallSubjectDialog.readArguments", "arguments cannot be null");
+ return;
+ }
+ mPhotoID = arguments.getLong(ARG_PHOTO_ID);
+ mPhotoUri = arguments.getParcelable(ARG_PHOTO_URI);
+ mContactUri = arguments.getParcelable(ARG_CONTACT_URI);
+ mNameOrNumber = arguments.getString(ARG_NAME_OR_NUMBER);
+ mIsBusiness = arguments.getBoolean(ARG_IS_BUSINESS);
+ mNumber = arguments.getString(ARG_NUMBER);
+ mDisplayNumber = arguments.getString(ARG_DISPLAY_NUMBER);
+ mNumberLabel = arguments.getString(ARG_NUMBER_LABEL);
+ mPhoneAccountHandle = arguments.getParcelable(ARG_PHONE_ACCOUNT_HANDLE);
+ }
+
+ /**
+ * Updates the character limit display, coloring the text RED when the limit is reached or
+ * exceeded.
+ */
+ private void updateCharacterLimit() {
+ String subjectText = mCallSubjectView.getText().toString();
+ final int length;
+
+ // If a message encoding is specified, use that to count bytes in the message.
+ if (mMessageEncoding != null) {
+ length = subjectText.getBytes(mMessageEncoding).length;
+ } else {
+ // No message encoding specified, so just count characters entered.
+ length = subjectText.length();
+ }
+
+ mCharacterLimitView.setText(getString(R.string.call_subject_limit, length, mLimit));
+ if (length >= mLimit) {
+ mCharacterLimitView.setTextColor(
+ getResources().getColor(R.color.call_subject_limit_exceeded));
+ } else {
+ mCharacterLimitView.setTextColor(
+ getResources().getColor(R.color.dialer_secondary_text_color));
+ }
+ }
+
+ /** Sets the photo on the quick contact photo. */
+ private void setPhoto(
+ long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) {
+ mContactPhoto.assignContactUri(contactUri);
+ if (CompatUtils.isLollipopCompatible()) {
+ mContactPhoto.setOverlay(null);
+ }
+
+ int contactType;
+ if (isBusiness) {
+ contactType = ContactPhotoManager.TYPE_BUSINESS;
+ } else {
+ contactType = ContactPhotoManager.TYPE_DEFAULT;
+ }
+
+ String lookupKey = null;
+ if (contactUri != null) {
+ lookupKey = UriUtils.getLookupKeyFromUri(contactUri);
+ }
+
+ ContactPhotoManager.DefaultImageRequest request =
+ new ContactPhotoManager.DefaultImageRequest(
+ displayName, lookupKey, contactType, true /* isCircular */);
+
+ if (photoId == 0 && photoUri != null) {
+ ContactPhotoManager.getInstance(this)
+ .loadPhoto(
+ mContactPhoto,
+ photoUri,
+ mPhotoSize,
+ false /* darkTheme */,
+ true /* isCircular */,
+ request);
+ } else {
+ ContactPhotoManager.getInstance(this)
+ .loadThumbnail(
+ mContactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request);
+ }
+ }
+
+ /**
+ * Saves the subject history list to shared prefs, removing older items so that there are only
+ * {@link #CALL_SUBJECT_HISTORY_SIZE} items at most.
+ *
+ * @param history The history.
+ */
+ private void saveSubjectHistory(List<String> history) {
+ // Remove oldest subject(s).
+ while (history.size() > CALL_SUBJECT_HISTORY_SIZE) {
+ history.remove(0);
+ }
+
+ SharedPreferences.Editor editor = mPrefs.edit();
+ int historyCount = 0;
+ for (String subject : history) {
+ if (!TextUtils.isEmpty(subject)) {
+ editor.putString(PREF_KEY_SUBJECT_HISTORY_ITEM + historyCount, subject);
+ historyCount++;
+ }
+ }
+ editor.putInt(PREF_KEY_SUBJECT_HISTORY_COUNT, historyCount);
+ editor.apply();
+ }
+
+ /** Hide software keyboard for the given {@link View}. */
+ public void hideSoftKeyboard(Context context, View view) {
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ /**
+ * Hides or shows the call history list.
+ *
+ * @param show {@code true} if the call history should be shown, {@code false} otherwise.
+ */
+ private void showCallHistory(final boolean show) {
+ // Bail early if the visibility has not changed.
+ if ((show && mSubjectList.getVisibility() == View.VISIBLE)
+ || (!show && mSubjectList.getVisibility() == View.GONE)) {
+ return;
+ }
+
+ final int dialogStartingBottom = mDialogView.getBottom();
+ if (show) {
+ // Showing the subject list; bind the list of history items to the list and show it.
+ ArrayAdapter<String> adapter =
+ new ArrayAdapter<String>(
+ CallSubjectDialog.this, R.layout.call_subject_history_list_item, mSubjectHistory);
+ mSubjectList.setAdapter(adapter);
+ mSubjectList.setVisibility(View.VISIBLE);
+ } else {
+ // Hiding the subject list.
+ mSubjectList.setVisibility(View.GONE);
+ }
+
+ // Use a ViewTreeObserver so that we can animate between the pre-layout and post-layout
+ // states.
+ ViewUtil.doOnPreDraw(
+ mBackgroundView,
+ true,
+ new Runnable() {
+ @Override
+ public void run() {
+ // Determine the amount the dialog has shifted due to the relayout.
+ int shiftAmount = dialogStartingBottom - mDialogView.getBottom();
+
+ // If the dialog needs to be shifted, do that now.
+ if (shiftAmount != 0) {
+ // Start animation in translated state and animate to translationY 0.
+ mDialogView.setTranslationY(shiftAmount);
+ mDialogView
+ .animate()
+ .translationY(0)
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setDuration(mAnimationDuration)
+ .start();
+ }
+
+ if (show) {
+ // Show the subject list.
+ mSubjectList.setTranslationY(mSubjectList.getHeight());
+
+ mSubjectList
+ .animate()
+ .translationY(0)
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setDuration(mAnimationDuration)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ mSubjectList.setVisibility(View.VISIBLE);
+ }
+ })
+ .start();
+ } else {
+ // Hide the subject list.
+ mSubjectList.setTranslationY(0);
+
+ mSubjectList
+ .animate()
+ .translationY(mSubjectList.getHeight())
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setDuration(mAnimationDuration)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ mSubjectList.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ }
+ })
+ .start();
+ }
+ }
+ });
+ }
+
+ /**
+ * Loads the message encoding and maximum message length from the phone account extras for the
+ * current phone account.
+ */
+ private void loadConfiguration() {
+ // Only attempt to load configuration from the phone account extras if the SDK is N or
+ // later. If we've got a prior SDK the default encoding and message length will suffice.
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ return;
+ }
+
+ if (mPhoneAccountHandle == null) {
+ return;
+ }
+
+ TelecomManager telecomManager = (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
+ final PhoneAccount account = telecomManager.getPhoneAccount(mPhoneAccountHandle);
+
+ Bundle phoneAccountExtras = account.getExtras();
+ if (phoneAccountExtras == null) {
+ return;
+ }
+
+ // Get limit, if provided; otherwise default to existing value.
+ mLimit = phoneAccountExtras.getInt(PhoneAccount.EXTRA_CALL_SUBJECT_MAX_LENGTH, mLimit);
+
+ // Get charset; default to none (e.g. count characters 1:1).
+ String charsetName =
+ phoneAccountExtras.getString(PhoneAccount.EXTRA_CALL_SUBJECT_CHARACTER_ENCODING);
+
+ if (!TextUtils.isEmpty(charsetName)) {
+ try {
+ mMessageEncoding = Charset.forName(charsetName);
+ } catch (java.nio.charset.UnsupportedCharsetException uce) {
+ // Character set was invalid; log warning and fallback to none.
+ LogUtil.e("CallSubjectDialog.loadConfiguration", "invalid charset: " + charsetName);
+ mMessageEncoding = null;
+ }
+ } else {
+ // No character set specified, so count characters 1:1.
+ mMessageEncoding = null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java
new file mode 100644
index 000000000..e96496cda
--- /dev/null
+++ b/java/com/android/contacts/common/dialog/ClearFrequentsDialog.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.dialog;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import com.android.contacts.common.R;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Dialog that clears the frequently contacted list after confirming with the user. */
+public class ClearFrequentsDialog extends DialogFragment {
+
+ /** Preferred way to show this dialog */
+ public static void show(FragmentManager fragmentManager) {
+ ClearFrequentsDialog dialog = new ClearFrequentsDialog();
+ dialog.show(fragmentManager, "clearFrequents");
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getActivity().getApplicationContext();
+ final ContentResolver resolver = getActivity().getContentResolver();
+ final OnClickListener okListener =
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (!PermissionsUtil.hasContactsPermissions(context)) {
+ return;
+ }
+
+ final ProgressDialog progressDialog =
+ ProgressDialog.show(
+ getContext(),
+ getString(R.string.clearFrequentsProgress_title),
+ null,
+ true,
+ true);
+
+ final AsyncTask<Void, Void, Void> task =
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ resolver.delete(
+ ContactsContract.DataUsageFeedback.DELETE_USAGE_URI, null, null);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ progressDialog.dismiss();
+ }
+ };
+ task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ };
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.clearFrequentsConfirmation_title)
+ .setMessage(R.string.clearFrequentsConfirmation)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, okListener)
+ .setCancelable(true)
+ .create();
+ }
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java
new file mode 100644
index 000000000..2607ad19a
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtender.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+import com.android.contacts.common.list.DirectoryPartition;
+import java.util.List;
+
+/** An interface for adding extended phone directories. */
+public interface PhoneDirectoryExtender {
+ /**
+ * Return a list of extended directories to add. May return null if no directories are to be
+ * added.
+ */
+ List<DirectoryPartition> getExtendedDirectories(Context context);
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java
new file mode 100644
index 000000000..84649f1ed
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderAccessor.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+
+/** Accessor for the phone directory extender singleton. */
+public final class PhoneDirectoryExtenderAccessor {
+
+ private static PhoneDirectoryExtender instance;
+
+ private PhoneDirectoryExtenderAccessor() {}
+
+ @NonNull
+ public static PhoneDirectoryExtender get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof PhoneDirectoryExtenderFactory) {
+ instance = ((PhoneDirectoryExtenderFactory) application).newPhoneDirectoryExtender();
+ }
+
+ if (instance == null) {
+ instance = new PhoneDirectoryExtenderStub();
+ }
+ return instance;
+ }
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java
new file mode 100644
index 000000000..9750ee300
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderFactory.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.support.annotation.NonNull;
+
+/**
+ * This interface should be implemented by the Application subclass. It allows the contacts module
+ * to get references to the PhoneDirectoryExtender.
+ */
+public interface PhoneDirectoryExtenderFactory {
+
+ @NonNull
+ PhoneDirectoryExtender newPhoneDirectoryExtender();
+}
diff --git a/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java
new file mode 100644
index 000000000..95f971533
--- /dev/null
+++ b/java/com/android/contacts/common/extensions/PhoneDirectoryExtenderStub.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.contacts.common.extensions;
+
+import android.content.Context;
+import com.android.contacts.common.list.DirectoryPartition;
+import java.util.Collections;
+import java.util.List;
+
+/** No-op implementation for phone directory extender. */
+class PhoneDirectoryExtenderStub implements PhoneDirectoryExtender {
+
+ @Override
+ public List<DirectoryPartition> getExtendedDirectories(Context context) {
+ return Collections.emptyList();
+ }
+}
diff --git a/java/com/android/contacts/common/format/FormatUtils.java b/java/com/android/contacts/common/format/FormatUtils.java
new file mode 100644
index 000000000..727c15b83
--- /dev/null
+++ b/java/com/android/contacts/common/format/FormatUtils.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.format;
+
+import android.database.CharArrayBuffer;
+import android.graphics.Typeface;
+import android.support.annotation.VisibleForTesting;
+import android.text.SpannableString;
+import android.text.style.StyleSpan;
+import java.util.Arrays;
+
+/** Assorted utility methods related to text formatting in Contacts. */
+public class FormatUtils {
+
+ /**
+ * Finds the earliest point in buffer1 at which the first part of buffer2 matches. For example,
+ * overlapPoint("abcd", "cdef") == 2.
+ */
+ public static int overlapPoint(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
+ if (buffer1 == null || buffer2 == null) {
+ return -1;
+ }
+ return overlapPoint(
+ Arrays.copyOfRange(buffer1.data, 0, buffer1.sizeCopied),
+ Arrays.copyOfRange(buffer2.data, 0, buffer2.sizeCopied));
+ }
+
+ /**
+ * Finds the earliest point in string1 at which the first part of string2 matches. For example,
+ * overlapPoint("abcd", "cdef") == 2.
+ */
+ @VisibleForTesting
+ public static int overlapPoint(String string1, String string2) {
+ if (string1 == null || string2 == null) {
+ return -1;
+ }
+ return overlapPoint(string1.toCharArray(), string2.toCharArray());
+ }
+
+ /**
+ * Finds the earliest point in array1 at which the first part of array2 matches. For example,
+ * overlapPoint("abcd", "cdef") == 2.
+ */
+ public static int overlapPoint(char[] array1, char[] array2) {
+ if (array1 == null || array2 == null) {
+ return -1;
+ }
+ int count1 = array1.length;
+ int count2 = array2.length;
+
+ // Ignore matching tails of the two arrays.
+ while (count1 > 0 && count2 > 0 && array1[count1 - 1] == array2[count2 - 1]) {
+ count1--;
+ count2--;
+ }
+
+ int size = count2;
+ for (int i = 0; i < count1; i++) {
+ if (i + size > count1) {
+ size = count1 - i;
+ }
+ int j;
+ for (j = 0; j < size; j++) {
+ if (array1[i + j] != array2[j]) {
+ break;
+ }
+ }
+ if (j == size) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ /**
+ * Applies the given style to a range of the input CharSequence.
+ *
+ * @param style The style to apply (see the style constants in {@link Typeface}).
+ * @param input The CharSequence to style.
+ * @param start Starting index of the range to style (will be clamped to be a minimum of 0).
+ * @param end Ending index of the range to style (will be clamped to a maximum of the input
+ * length).
+ * @param flags Bitmask for configuring behavior of the span. See {@link android.text.Spanned}.
+ * @return The styled CharSequence.
+ */
+ public static CharSequence applyStyleToSpan(
+ int style, CharSequence input, int start, int end, int flags) {
+ // Enforce bounds of the char sequence.
+ start = Math.max(0, start);
+ end = Math.min(input.length(), end);
+ SpannableString text = new SpannableString(input);
+ text.setSpan(new StyleSpan(style), start, end, flags);
+ return text;
+ }
+
+ @VisibleForTesting
+ public static void copyToCharArrayBuffer(String text, CharArrayBuffer buffer) {
+ if (text != null) {
+ char[] data = buffer.data;
+ if (data == null || data.length < text.length()) {
+ buffer.data = text.toCharArray();
+ } else {
+ text.getChars(0, text.length(), data, 0);
+ }
+ buffer.sizeCopied = text.length();
+ } else {
+ buffer.sizeCopied = 0;
+ }
+ }
+
+ /** Returns a String that represents the content of the given {@link CharArrayBuffer}. */
+ @VisibleForTesting
+ public static String charArrayBufferToString(CharArrayBuffer buffer) {
+ return new String(buffer.data, 0, buffer.sizeCopied);
+ }
+
+ /**
+ * Finds the index of the first word that starts with the given prefix.
+ *
+ * <p>If not found, returns -1.
+ *
+ * @param text the text in which to search for the prefix
+ * @param prefix the text to find, in upper case letters
+ */
+ public static int indexOfWordPrefix(CharSequence text, String prefix) {
+ if (prefix == null || text == null) {
+ return -1;
+ }
+
+ int textLength = text.length();
+ int prefixLength = prefix.length();
+
+ if (prefixLength == 0 || textLength < prefixLength) {
+ return -1;
+ }
+
+ int i = 0;
+ while (i < textLength) {
+ // Skip non-word characters
+ while (i < textLength && !Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+
+ if (i + prefixLength > textLength) {
+ return -1;
+ }
+
+ // Compare the prefixes
+ int j;
+ for (j = 0; j < prefixLength; j++) {
+ if (Character.toUpperCase(text.charAt(i + j)) != prefix.charAt(j)) {
+ break;
+ }
+ }
+ if (j == prefixLength) {
+ return i;
+ }
+
+ // Skip this word
+ while (i < textLength && Character.isLetterOrDigit(text.charAt(i))) {
+ i++;
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/java/com/android/contacts/common/format/TextHighlighter.java b/java/com/android/contacts/common/format/TextHighlighter.java
new file mode 100644
index 000000000..30c03fdf3
--- /dev/null
+++ b/java/com/android/contacts/common/format/TextHighlighter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.format;
+
+import android.text.SpannableString;
+import android.text.style.CharacterStyle;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+
+/** Highlights the text in a text field. */
+public class TextHighlighter {
+
+ private static final boolean DEBUG = false;
+ private final String TAG = TextHighlighter.class.getSimpleName();
+ private int mTextStyle;
+
+ private CharacterStyle mTextStyleSpan;
+
+ public TextHighlighter(int textStyle) {
+ mTextStyle = textStyle;
+ mTextStyleSpan = getStyleSpan();
+ }
+
+ /**
+ * Sets the text on the given text view, highlighting the word that matches the given prefix.
+ *
+ * @param view the view on which to set the text
+ * @param text the string to use as the text
+ * @param prefix the prefix to look for
+ */
+ public void setPrefixText(TextView view, String text, String prefix) {
+ view.setText(applyPrefixHighlight(text, prefix));
+ }
+
+ private CharacterStyle getStyleSpan() {
+ return new StyleSpan(mTextStyle);
+ }
+
+ /**
+ * Applies highlight span to the text.
+ *
+ * @param text Text sequence to be highlighted.
+ * @param start Start position of the highlight sequence.
+ * @param end End position of the highlight sequence.
+ */
+ public void applyMaskingHighlight(SpannableString text, int start, int end) {
+ /** Sets text color of the masked locations to be highlighted. */
+ text.setSpan(getStyleSpan(), start, end, 0);
+ }
+
+ /**
+ * Returns a CharSequence which highlights the given prefix if found in the given text.
+ *
+ * @param text the text to which to apply the highlight
+ * @param prefix the prefix to look for
+ */
+ public CharSequence applyPrefixHighlight(CharSequence text, String prefix) {
+ if (prefix == null) {
+ return text;
+ }
+
+ // Skip non-word characters at the beginning of prefix.
+ int prefixStart = 0;
+ while (prefixStart < prefix.length()
+ && !Character.isLetterOrDigit(prefix.charAt(prefixStart))) {
+ prefixStart++;
+ }
+ final String trimmedPrefix = prefix.substring(prefixStart);
+
+ int index = FormatUtils.indexOfWordPrefix(text, trimmedPrefix);
+ if (index != -1) {
+ final SpannableString result = new SpannableString(text);
+ result.setSpan(mTextStyleSpan, index, index + trimmedPrefix.length(), 0 /* flags */);
+ return result;
+ } else {
+ return text;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/format/testing/SpannedTestUtils.java b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java
new file mode 100644
index 000000000..293d9d5ad
--- /dev/null
+++ b/java/com/android/contacts/common/format/testing/SpannedTestUtils.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.format.testing;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.Html;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
+import android.widget.TextView;
+import junit.framework.Assert;
+
+/** Utility class to check the value of spanned text in text views. */
+@SmallTest
+public class SpannedTestUtils {
+
+ /**
+ * Checks that the text contained in the text view matches the given HTML text.
+ *
+ * @param expectedHtmlText the expected text to be in the text view
+ * @param textView the text view from which to get the text
+ */
+ public static void checkHtmlText(String expectedHtmlText, TextView textView) {
+ String actualHtmlText = Html.toHtml((Spanned) textView.getText());
+ if (TextUtils.isEmpty(expectedHtmlText)) {
+ // If the text is empty, it does not add the <p></p> bits to it.
+ Assert.assertEquals("", actualHtmlText);
+ } else {
+ Assert.assertEquals("<p dir=ltr>" + expectedHtmlText + "</p>\n", actualHtmlText);
+ }
+ }
+
+ /**
+ * Assert span exists in the correct location.
+ *
+ * @param seq The spannable string to check.
+ * @param start The starting index.
+ * @param end The ending index.
+ */
+ public static void assertPrefixSpan(CharSequence seq, int start, int end) {
+ Assert.assertTrue(seq instanceof Spanned);
+ Spanned spannable = (Spanned) seq;
+
+ if (start > 0) {
+ Assert.assertEquals(0, getNumForegroundColorSpansBetween(spannable, 0, start - 1));
+ }
+ Assert.assertEquals(1, getNumForegroundColorSpansBetween(spannable, start, end));
+ Assert.assertEquals(
+ 0, getNumForegroundColorSpansBetween(spannable, end + 1, spannable.length() - 1));
+ }
+
+ private static int getNumForegroundColorSpansBetween(Spanned value, int start, int end) {
+ return value.getSpans(start, end, StyleSpan.class).length;
+ }
+
+ /**
+ * Asserts that the given character sequence is not a Spanned object and text is correct.
+ *
+ * @param seq The sequence to check.
+ * @param expected The expected text.
+ */
+ public static void assertNotSpanned(CharSequence seq, String expected) {
+ Assert.assertFalse(seq instanceof Spanned);
+ Assert.assertEquals(expected, seq);
+ }
+
+ public static int getNextTransition(SpannableString seq, int start) {
+ return seq.nextSpanTransition(start, seq.length(), StyleSpan.class);
+ }
+}
diff --git a/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
new file mode 100644
index 000000000..7e1839c1e
--- /dev/null
+++ b/java/com/android/contacts/common/lettertiles/LetterTileDrawable.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.lettertiles;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.dialer.common.Assert;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A drawable that encapsulates all the functionality needed to display a letter tile to represent a
+ * contact image.
+ */
+public class LetterTileDrawable extends Drawable {
+
+ /**
+ * ContactType indicates the avatar type of the contact. For a person or for the default when no
+ * name is provided, it is {@link #TYPE_DEFAULT}, otherwise, for a business it is {@link
+ * #TYPE_BUSINESS}, and voicemail contacts should use {@link #TYPE_VOICEMAIL}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_PERSON, TYPE_BUSINESS, TYPE_VOICEMAIL})
+ public @interface ContactType {}
+
+ /** Contact type constants */
+ public static final int TYPE_PERSON = 1;
+ public static final int TYPE_BUSINESS = 2;
+ public static final int TYPE_VOICEMAIL = 3;
+ @ContactType public static final int TYPE_DEFAULT = TYPE_PERSON;
+
+ /**
+ * Shape indicates the letter tile shape. It can be either a {@link #SHAPE_CIRCLE}, otherwise, it
+ * is a {@link #SHAPE_RECTANGLE}.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SHAPE_CIRCLE, SHAPE_RECTANGLE})
+ public @interface Shape {}
+
+ /** Shape constants */
+ public static final int SHAPE_CIRCLE = 1;
+
+ public static final int SHAPE_RECTANGLE = 2;
+
+ /** 54% opacity */
+ private static final int ALPHA = 138;
+
+ /** Reusable components to avoid new allocations */
+ private static final Paint sPaint = new Paint();
+
+ private static final Rect sRect = new Rect();
+ private static final char[] sFirstChar = new char[1];
+ /** Letter tile */
+ private static TypedArray sColors;
+
+ private static int sDefaultColor;
+ private static int sTileFontColor;
+ private static float sLetterToTileRatio;
+ private static Bitmap sDefaultPersonAvatar;
+ private static Bitmap sDefaultBusinessAvatar;
+ private static Bitmap sDefaultVoicemailAvatar;
+ private static final String TAG = LetterTileDrawable.class.getSimpleName();
+ private final Paint mPaint;
+ private int mContactType = TYPE_DEFAULT;
+ private float mScale = 1.0f;
+ private float mOffset = 0.0f;
+ private boolean mIsCircle = false;
+
+ private int mColor;
+ private Character mLetter = null;
+
+ private boolean mAvatarWasVoicemailOrBusiness = false;
+ private String mDisplayName;
+
+ public LetterTileDrawable(final Resources res) {
+ if (sColors == null) {
+ sColors = res.obtainTypedArray(R.array.letter_tile_colors);
+ sDefaultColor = res.getColor(R.color.letter_tile_default_color);
+ sTileFontColor = res.getColor(R.color.letter_tile_font_color);
+ sLetterToTileRatio = res.getFraction(R.dimen.letter_to_tile_ratio, 1, 1);
+ sDefaultPersonAvatar =
+ BitmapFactory.decodeResource(
+ res, R.drawable.product_logo_avatar_anonymous_white_color_120);
+ sDefaultBusinessAvatar =
+ BitmapFactory.decodeResource(res, R.drawable.ic_business_white_120dp);
+ sDefaultVoicemailAvatar = BitmapFactory.decodeResource(res, R.drawable.ic_voicemail_avatar);
+ sPaint.setTypeface(
+ Typeface.create(res.getString(R.string.letter_tile_letter_font_family), Typeface.NORMAL));
+ sPaint.setTextAlign(Align.CENTER);
+ sPaint.setAntiAlias(true);
+ }
+ mPaint = new Paint();
+ mPaint.setFilterBitmap(true);
+ mPaint.setDither(true);
+ mColor = sDefaultColor;
+ }
+
+ private static Bitmap getBitmapForContactType(int contactType) {
+ switch (contactType) {
+ case TYPE_BUSINESS:
+ return sDefaultBusinessAvatar;
+ case TYPE_VOICEMAIL:
+ return sDefaultVoicemailAvatar;
+ case TYPE_PERSON:
+ default:
+ return sDefaultPersonAvatar;
+ }
+ }
+
+ private static boolean isEnglishLetter(final char c) {
+ return ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z');
+ }
+
+ @Override
+ public void draw(final Canvas canvas) {
+ final Rect bounds = getBounds();
+ if (!isVisible() || bounds.isEmpty()) {
+ return;
+ }
+ // Draw letter tile.
+ drawLetterTile(canvas);
+ }
+
+ /**
+ * Draw the bitmap onto the canvas at the current bounds taking into account the current scale.
+ */
+ private void drawBitmap(
+ final Bitmap bitmap, final int width, final int height, final Canvas canvas) {
+ // The bitmap should be drawn in the middle of the canvas without changing its width to
+ // height ratio.
+ final Rect destRect = copyBounds();
+
+ // Crop the destination bounds into a square, scaled and offset as appropriate
+ final int halfLength = (int) (mScale * Math.min(destRect.width(), destRect.height()) / 2);
+
+ destRect.set(
+ destRect.centerX() - halfLength,
+ (int) (destRect.centerY() - halfLength + mOffset * destRect.height()),
+ destRect.centerX() + halfLength,
+ (int) (destRect.centerY() + halfLength + mOffset * destRect.height()));
+
+ // Source rectangle remains the entire bounds of the source bitmap.
+ sRect.set(0, 0, width, height);
+
+ sPaint.setTextAlign(Align.CENTER);
+ sPaint.setAntiAlias(true);
+ sPaint.setAlpha(ALPHA);
+
+ canvas.drawBitmap(bitmap, sRect, destRect, sPaint);
+ }
+
+ private void drawLetterTile(final Canvas canvas) {
+ // Draw background color.
+ sPaint.setColor(mColor);
+
+ sPaint.setAlpha(mPaint.getAlpha());
+ final Rect bounds = getBounds();
+ final int minDimension = Math.min(bounds.width(), bounds.height());
+
+ if (mIsCircle) {
+ canvas.drawCircle(bounds.centerX(), bounds.centerY(), minDimension / 2, sPaint);
+ } else {
+ canvas.drawRect(bounds, sPaint);
+ }
+
+ // Draw letter/digit only if the first character is an english letter or there's a override
+
+ if (mLetter != null) {
+ // Draw letter or digit.
+ sFirstChar[0] = mLetter;
+
+ // Scale text by canvas bounds and user selected scaling factor
+ sPaint.setTextSize(mScale * sLetterToTileRatio * minDimension);
+ sPaint.getTextBounds(sFirstChar, 0, 1, sRect);
+ sPaint.setTypeface(Typeface.create("sans-serif", Typeface.NORMAL));
+ sPaint.setColor(sTileFontColor);
+ sPaint.setAlpha(ALPHA);
+
+ // Draw the letter in the canvas, vertically shifted up or down by the user-defined
+ // offset
+ canvas.drawText(
+ sFirstChar,
+ 0,
+ 1,
+ bounds.centerX(),
+ bounds.centerY() + mOffset * bounds.height() - sRect.exactCenterY(),
+ sPaint);
+ } else {
+ // Draw the default image if there is no letter/digit to be drawn
+ final Bitmap bitmap = getBitmapForContactType(mContactType);
+ drawBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), canvas);
+ }
+ }
+
+ public int getColor() {
+ return mColor;
+ }
+
+ public LetterTileDrawable setColor(int color) {
+ mColor = color;
+ return this;
+ }
+
+ /** Returns a deterministic color based on the provided contact identifier string. */
+ private int pickColor(final String identifier) {
+ if (TextUtils.isEmpty(identifier) || mContactType == TYPE_VOICEMAIL) {
+ return sDefaultColor;
+ }
+ // String.hashCode() implementation is not supposed to change across java versions, so
+ // this should guarantee the same email address always maps to the same color.
+ // The email should already have been normalized by the ContactRequest.
+ final int color = Math.abs(identifier.hashCode()) % sColors.length();
+ return sColors.getColor(color, sDefaultColor);
+ }
+
+ @Override
+ public void setAlpha(final int alpha) {
+ mPaint.setAlpha(alpha);
+ }
+
+ @Override
+ public void setColorFilter(final ColorFilter cf) {
+ mPaint.setColorFilter(cf);
+ }
+
+ @Override
+ public int getOpacity() {
+ return android.graphics.PixelFormat.OPAQUE;
+ }
+
+ @Override
+ public void getOutline(Outline outline) {
+ if (mIsCircle) {
+ outline.setOval(getBounds());
+ } else {
+ outline.setRect(getBounds());
+ }
+
+ outline.setAlpha(1);
+ }
+
+ /**
+ * Scale the drawn letter tile to a ratio of its default size
+ *
+ * @param scale The ratio the letter tile should be scaled to as a percentage of its default size,
+ * from a scale of 0 to 2.0f. The default is 1.0f.
+ */
+ public LetterTileDrawable setScale(float scale) {
+ mScale = scale;
+ return this;
+ }
+
+ /**
+ * Assigns the vertical offset of the position of the letter tile to the ContactDrawable
+ *
+ * @param offset The provided offset must be within the range of -0.5f to 0.5f. If set to -0.5f,
+ * the letter will be shifted upwards by 0.5 times the height of the canvas it is being drawn
+ * on, which means it will be drawn with the center of the letter starting at the top edge of
+ * the canvas. If set to 0.5f, the letter will be shifted downwards by 0.5 times the height of
+ * the canvas it is being drawn on, which means it will be drawn with the center of the letter
+ * starting at the bottom edge of the canvas. The default is 0.0f.
+ */
+ public LetterTileDrawable setOffset(float offset) {
+ Assert.checkArgument(offset >= -0.5f && offset <= 0.5f);
+ mOffset = offset;
+ return this;
+ }
+
+ public LetterTileDrawable setLetter(Character letter) {
+ mLetter = letter;
+ return this;
+ }
+
+ public Character getLetter() {
+ return this.mLetter;
+ }
+
+ private LetterTileDrawable setLetterAndColorFromContactDetails(
+ final String displayName, final String identifier) {
+ if (displayName != null && displayName.length() > 0 && isEnglishLetter(displayName.charAt(0))) {
+ mLetter = Character.toUpperCase(displayName.charAt(0));
+ } else {
+ mLetter = null;
+ }
+ mColor = pickColor(identifier);
+ return this;
+ }
+
+ public LetterTileDrawable setContactType(@ContactType int contactType) {
+ mContactType = contactType;
+ return this;
+ }
+
+ @ContactType
+ public int getContactType() {
+ return this.mContactType;
+ }
+
+ public LetterTileDrawable setIsCircular(boolean isCircle) {
+ mIsCircle = isCircle;
+ return this;
+ }
+
+ /**
+ * Creates a canonical letter tile for use across dialer fragments.
+ *
+ * @param displayName The display name to produce the letter in the tile. Null values or numbers
+ * yield no letter.
+ * @param identifierForTileColor The string used to produce the tile color.
+ * @param shape The shape of the tile.
+ * @param contactType The type of contact, e.g. TYPE_VOICEMAIL.
+ * @return this
+ */
+ public LetterTileDrawable setCanonicalDialerLetterTileDetails(
+ @Nullable final String displayName,
+ @Nullable final String identifierForTileColor,
+ @Shape final int shape,
+ final int contactType) {
+ setContactType(contactType);
+ /**
+ * During hangup, we lose the call state for special types of contacts, like voicemail. To help
+ * callers avoid extraneous LetterTileDrawable allocations, we keep track of the special case
+ * until we encounter a new display name.
+ */
+ if (contactType == TYPE_VOICEMAIL || contactType == TYPE_BUSINESS) {
+ this.mAvatarWasVoicemailOrBusiness = true;
+ } else if (displayName != null && !displayName.equals(mDisplayName)) {
+ this.mAvatarWasVoicemailOrBusiness = false;
+ }
+ this.mDisplayName = displayName;
+ if (shape == SHAPE_CIRCLE) {
+ this.setIsCircular(true);
+ } else {
+ this.setIsCircular(false);
+ }
+
+ /**
+ * To preserve style, we don't use contactType to set the tile icon. In the future, when all
+ * callers surface this detail, we can use this to better style the tile icon.
+ */
+ if (mAvatarWasVoicemailOrBusiness) {
+ this.setLetterAndColorFromContactDetails(null, displayName);
+ return this;
+ } else {
+ if (identifierForTileColor != null) {
+ this.setLetterAndColorFromContactDetails(displayName, identifierForTileColor);
+ return this;
+ } else {
+ this.setLetterAndColorFromContactDetails(displayName, displayName);
+ return this;
+ }
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java
new file mode 100644
index 000000000..601abf528
--- /dev/null
+++ b/java/com/android/contacts/common/list/AutoScrollListView.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.ListView;
+
+/**
+ * A ListView that can be asked to scroll (smoothly or otherwise) to a specific position. This class
+ * takes advantage of similar functionality that exists in {@link ListView} and enhances it.
+ */
+public class AutoScrollListView extends ListView {
+
+ /** Position the element at about 1/3 of the list height */
+ private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f;
+
+ private int mRequestedScrollPosition = -1;
+ private boolean mSmoothScrollRequested;
+
+ public AutoScrollListView(Context context) {
+ super(context);
+ }
+
+ public AutoScrollListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Brings the specified position to view by optionally performing a jump-scroll maneuver: first it
+ * jumps to some position near the one requested and then does a smooth scroll to the requested
+ * position. This creates an impression of full smooth scrolling without actually traversing the
+ * entire list. If smooth scrolling is not requested, instantly positions the requested item at a
+ * preferred offset.
+ */
+ public void requestPositionToScreen(int position, boolean smoothScroll) {
+ mRequestedScrollPosition = position;
+ mSmoothScrollRequested = smoothScroll;
+ requestLayout();
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+ if (mRequestedScrollPosition == -1) {
+ return;
+ }
+
+ final int position = mRequestedScrollPosition;
+ mRequestedScrollPosition = -1;
+
+ int firstPosition = getFirstVisiblePosition() + 1;
+ int lastPosition = getLastVisiblePosition();
+ if (position >= firstPosition && position <= lastPosition) {
+ return; // Already on screen
+ }
+
+ final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP);
+ if (!mSmoothScrollRequested) {
+ setSelectionFromTop(position, offset);
+
+ // Since we have changed the scrolling position, we need to redo child layout
+ // Calling "requestLayout" in the middle of a layout pass has no effect,
+ // so we call layoutChildren explicitly
+ super.layoutChildren();
+
+ } else {
+ // We will first position the list a couple of screens before or after
+ // the new selection and then scroll smoothly to it.
+ int twoScreens = (lastPosition - firstPosition) * 2;
+ int preliminaryPosition;
+ if (position < firstPosition) {
+ preliminaryPosition = position + twoScreens;
+ if (preliminaryPosition >= getCount()) {
+ preliminaryPosition = getCount() - 1;
+ }
+ if (preliminaryPosition < firstPosition) {
+ setSelection(preliminaryPosition);
+ super.layoutChildren();
+ }
+ } else {
+ preliminaryPosition = position - twoScreens;
+ if (preliminaryPosition < 0) {
+ preliminaryPosition = 0;
+ }
+ if (preliminaryPosition > lastPosition) {
+ setSelection(preliminaryPosition);
+ super.layoutChildren();
+ }
+ }
+
+ smoothScrollToPositionFromTop(position, offset);
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+
+ // Workaround for b/31160338 and b/32778636.
+ if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N
+ || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) {
+ layoutChildren();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactEntry.java b/java/com/android/contacts/common/list/ContactEntry.java
new file mode 100644
index 000000000..e33165e45
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactEntry.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.net.Uri;
+import android.provider.ContactsContract.PinnedPositions;
+import android.text.TextUtils;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/** Class to hold contact information */
+public class ContactEntry {
+
+ public static final ContactEntry BLANK_ENTRY = new ContactEntry();
+ private static final int UNSET_DISPLAY_ORDER_PREFERENCE = -1;
+ /** Primary name for a Contact */
+ public String namePrimary;
+ /** Alternative name for a Contact, e.g. last name first */
+ public String nameAlternative;
+ /**
+ * The user's preference on name display order, last name first or first time first. {@see
+ * ContactsPreferences}
+ */
+ public int nameDisplayOrder = UNSET_DISPLAY_ORDER_PREFERENCE;
+
+ public String phoneLabel;
+ public String phoneNumber;
+ public Uri photoUri;
+ public Uri lookupUri;
+ public String lookupKey;
+ public long id;
+ public int pinned = PinnedPositions.UNPINNED;
+ public boolean isFavorite = false;
+ public boolean isDefaultNumber = false;
+
+ public String getPreferredDisplayName() {
+ if (nameDisplayOrder == UNSET_DISPLAY_ORDER_PREFERENCE
+ || nameDisplayOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY
+ || TextUtils.isEmpty(nameAlternative)) {
+ return namePrimary;
+ }
+ return nameAlternative;
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactEntryListAdapter.java b/java/com/android/contacts/common/list/ContactEntryListAdapter.java
new file mode 100644
index 000000000..18bbae382
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactEntryListAdapter.java
@@ -0,0 +1,742 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.QuickContactBadge;
+import android.widget.SectionIndexer;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.util.SearchUtil;
+import com.android.dialer.compat.CompatUtils;
+import java.util.HashSet;
+
+/**
+ * Common base class for various contact-related lists, e.g. contact list, phone number list etc.
+ */
+public abstract class ContactEntryListAdapter extends IndexerListAdapter {
+
+ /**
+ * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be included in the
+ * search.
+ */
+ public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
+
+ private static final String TAG = "ContactEntryListAdapter";
+ private int mDisplayOrder;
+ private int mSortOrder;
+
+ private boolean mDisplayPhotos;
+ private boolean mCircularPhotos = true;
+ private boolean mQuickContactEnabled;
+ private boolean mAdjustSelectionBoundsEnabled;
+
+ /** The root view of the fragment that this adapter is associated with. */
+ private View mFragmentRootView;
+
+ private ContactPhotoManager mPhotoLoader;
+
+ private String mQueryString;
+ private String mUpperCaseQueryString;
+ private boolean mSearchMode;
+ private int mDirectorySearchMode;
+ private int mDirectoryResultLimit = Integer.MAX_VALUE;
+
+ private boolean mEmptyListEnabled = true;
+
+ private boolean mSelectionVisible;
+
+ private ContactListFilter mFilter;
+ private boolean mDarkTheme = false;
+
+ /** Resource used to provide header-text for default filter. */
+ private CharSequence mDefaultFilterHeaderText;
+
+ public ContactEntryListAdapter(Context context) {
+ super(context);
+ setDefaultFilterHeaderText(R.string.local_search_label);
+ addPartitions();
+ }
+
+ /**
+ * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of image
+ * loading requests that get cancelled on cursor changes.
+ */
+ protected void setFragmentRootView(View fragmentRootView) {
+ mFragmentRootView = fragmentRootView;
+ }
+
+ protected void setDefaultFilterHeaderText(int resourceId) {
+ mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ final ContactListItemView view = new ContactListItemView(context, null);
+ view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+ view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+ return view;
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ final ContactListItemView view = (ContactListItemView) itemView;
+ view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+ bindWorkProfileIcon(view, partition);
+ }
+
+ @Override
+ protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
+ return new ContactListPinnedHeaderView(context, null, parent);
+ }
+
+ @Override
+ protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
+ ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
+ }
+
+ protected void addPartitions() {
+ addPartition(createDefaultDirectoryPartition());
+ }
+
+ protected DirectoryPartition createDefaultDirectoryPartition() {
+ DirectoryPartition partition = new DirectoryPartition(true, true);
+ partition.setDirectoryId(Directory.DEFAULT);
+ partition.setDirectoryType(getContext().getString(R.string.contactsList));
+ partition.setPriorityDirectory(true);
+ partition.setPhotoSupported(true);
+ partition.setLabel(mDefaultFilterHeaderText.toString());
+ return partition;
+ }
+
+ /**
+ * Remove all directories after the default directory. This is typically used when contacts list
+ * screens are asked to exit the search mode and thus need to remove all remote directory results
+ * for the search.
+ *
+ * <p>This code assumes that the default directory and directories before that should not be
+ * deleted (e.g. Join screen has "suggested contacts" directory before the default director, and
+ * we should not remove the directory).
+ */
+ public void removeDirectoriesAfterDefault() {
+ final int partitionCount = getPartitionCount();
+ for (int i = partitionCount - 1; i >= 0; i--) {
+ final Partition partition = getPartition(i);
+ if ((partition instanceof DirectoryPartition)
+ && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
+ break;
+ } else {
+ removePartition(i);
+ }
+ }
+ }
+
+ protected int getPartitionByDirectoryId(long id) {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ if (((DirectoryPartition) partition).getDirectoryId() == id) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ }
+
+ protected DirectoryPartition getDirectoryById(long id) {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ if (directoryPartition.getDirectoryId() == id) {
+ return directoryPartition;
+ }
+ }
+ }
+ return null;
+ }
+
+ public abstract void configureLoader(CursorLoader loader, long directoryId);
+
+ /** Marks all partitions as "loading" */
+ public void onDataReload() {
+ boolean notify = false;
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ if (!directoryPartition.isLoading()) {
+ notify = true;
+ }
+ directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+ }
+ }
+ if (notify) {
+ notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void clearPartitions() {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
+ }
+ }
+ super.clearPartitions();
+ }
+
+ public boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ public void setSearchMode(boolean flag) {
+ mSearchMode = flag;
+ }
+
+ public String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString) {
+ mQueryString = queryString;
+ if (TextUtils.isEmpty(queryString)) {
+ mUpperCaseQueryString = null;
+ } else {
+ mUpperCaseQueryString = SearchUtil.cleanStartAndEndOfSearchQuery(queryString.toUpperCase());
+ }
+ }
+
+ public String getUpperCaseQueryString() {
+ return mUpperCaseQueryString;
+ }
+
+ public int getDirectorySearchMode() {
+ return mDirectorySearchMode;
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ public int getDirectoryResultLimit() {
+ return mDirectoryResultLimit;
+ }
+
+ public void setDirectoryResultLimit(int limit) {
+ this.mDirectoryResultLimit = limit;
+ }
+
+ public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
+ final int limit = directoryPartition.getResultLimit();
+ return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
+ }
+
+ public int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ public void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ }
+
+ protected ContactPhotoManager getPhotoLoader() {
+ return mPhotoLoader;
+ }
+
+ public void setPhotoLoader(ContactPhotoManager photoLoader) {
+ mPhotoLoader = photoLoader;
+ }
+
+ public boolean getDisplayPhotos() {
+ return mDisplayPhotos;
+ }
+
+ public void setDisplayPhotos(boolean displayPhotos) {
+ mDisplayPhotos = displayPhotos;
+ }
+
+ public boolean getCircularPhotos() {
+ return mCircularPhotos;
+ }
+
+ public boolean isSelectionVisible() {
+ return mSelectionVisible;
+ }
+
+ public void setSelectionVisible(boolean flag) {
+ this.mSelectionVisible = flag;
+ }
+
+ public boolean isQuickContactEnabled() {
+ return mQuickContactEnabled;
+ }
+
+ public void setQuickContactEnabled(boolean quickContactEnabled) {
+ mQuickContactEnabled = quickContactEnabled;
+ }
+
+ public boolean isAdjustSelectionBoundsEnabled() {
+ return mAdjustSelectionBoundsEnabled;
+ }
+
+ public void setAdjustSelectionBoundsEnabled(boolean enabled) {
+ mAdjustSelectionBoundsEnabled = enabled;
+ }
+
+ public void setProfileExists(boolean exists) {
+ // Stick the "ME" header for the profile
+ if (exists) {
+ setSectionHeader(R.string.user_profile_contacts_list_header, /* # of ME */ 1);
+ }
+ }
+
+ private void setSectionHeader(int resId, int numberOfItems) {
+ SectionIndexer indexer = getIndexer();
+ if (indexer != null) {
+ ((ContactsSectionIndexer) indexer)
+ .setProfileAndFavoritesHeader(getContext().getString(resId), numberOfItems);
+ }
+ }
+
+ public void setDarkTheme(boolean value) {
+ mDarkTheme = value;
+ }
+
+ /** Updates partitions according to the directory meta-data contained in the supplied cursor. */
+ public void changeDirectories(Cursor cursor) {
+ if (cursor.getCount() == 0) {
+ // Directory table must have at least local directory, without which this adapter will
+ // enter very weird state.
+ Log.e(
+ TAG,
+ "Directory search loader returned an empty cursor, which implies we have "
+ + "no directory entries.",
+ new RuntimeException());
+ return;
+ }
+ HashSet<Long> directoryIds = new HashSet<Long>();
+
+ int idColumnIndex = cursor.getColumnIndex(Directory._ID);
+ int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
+ int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
+ int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
+
+ // TODO preserve the order of partition to match those of the cursor
+ // Phase I: add new directories
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idColumnIndex);
+ directoryIds.add(id);
+ if (getPartitionByDirectoryId(id) == -1) {
+ DirectoryPartition partition = new DirectoryPartition(false, true);
+ partition.setDirectoryId(id);
+ if (DirectoryCompat.isRemoteDirectoryId(id)) {
+ if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
+ partition.setLabel(mContext.getString(R.string.directory_search_label_work));
+ } else {
+ partition.setLabel(mContext.getString(R.string.directory_search_label));
+ }
+ } else {
+ if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
+ partition.setLabel(mContext.getString(R.string.list_filter_phones_work));
+ } else {
+ partition.setLabel(mDefaultFilterHeaderText.toString());
+ }
+ }
+ partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
+ partition.setDisplayName(cursor.getString(displayNameColumnIndex));
+ int photoSupport = cursor.getInt(photoSupportColumnIndex);
+ partition.setPhotoSupported(
+ photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
+ || photoSupport == Directory.PHOTO_SUPPORT_FULL);
+ addPartition(partition);
+ }
+ }
+
+ // Phase II: remove deleted directories
+ int count = getPartitionCount();
+ for (int i = count; --i >= 0; ) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ long id = ((DirectoryPartition) partition).getDirectoryId();
+ if (!directoryIds.contains(id)) {
+ removePartition(i);
+ }
+ }
+ }
+
+ invalidate();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void changeCursor(int partitionIndex, Cursor cursor) {
+ if (partitionIndex >= getPartitionCount()) {
+ // There is no partition for this data
+ return;
+ }
+
+ Partition partition = getPartition(partitionIndex);
+ if (partition instanceof DirectoryPartition) {
+ ((DirectoryPartition) partition).setStatus(DirectoryPartition.STATUS_LOADED);
+ }
+
+ if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
+ mPhotoLoader.refreshCache();
+ }
+
+ super.changeCursor(partitionIndex, cursor);
+
+ if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
+ updateIndexer(cursor);
+ }
+
+ // When the cursor changes, cancel any pending asynchronous photo loads.
+ mPhotoLoader.cancelPendingRequests(mFragmentRootView);
+ }
+
+ public void changeCursor(Cursor cursor) {
+ changeCursor(0, cursor);
+ }
+
+ /** Updates the indexer, which is used to produce section headers. */
+ private void updateIndexer(Cursor cursor) {
+ if (cursor == null || cursor.isClosed()) {
+ setIndexer(null);
+ return;
+ }
+
+ Bundle bundle = cursor.getExtras();
+ if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)
+ && bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
+ String[] sections = bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+ int[] counts = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+
+ if (getExtraStartingSection()) {
+ // Insert an additional unnamed section at the top of the list.
+ String[] allSections = new String[sections.length + 1];
+ int[] allCounts = new int[counts.length + 1];
+ for (int i = 0; i < sections.length; i++) {
+ allSections[i + 1] = sections[i];
+ allCounts[i + 1] = counts[i];
+ }
+ allCounts[0] = 1;
+ allSections[0] = "";
+ setIndexer(new ContactsSectionIndexer(allSections, allCounts));
+ } else {
+ setIndexer(new ContactsSectionIndexer(sections, counts));
+ }
+ } else {
+ setIndexer(null);
+ }
+ }
+
+ protected boolean getExtraStartingSection() {
+ return false;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ // We need a separate view type for each item type, plus another one for
+ // each type with header, plus one for "other".
+ return getItemViewTypeCount() * 2 + 1;
+ }
+
+ @Override
+ public int getItemViewType(int partitionIndex, int position) {
+ int type = super.getItemViewType(partitionIndex, position);
+ if (!isUserProfile(position)
+ && isSectionHeaderDisplayEnabled()
+ && partitionIndex == getIndexedPartition()) {
+ Placement placement = getItemPlacementInSection(position);
+ return placement.firstInSection ? type : getItemViewTypeCount() + type;
+ } else {
+ return type;
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ // TODO
+ // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
+ // return true;
+ // }
+
+ if (!mEmptyListEnabled) {
+ return false;
+ } else if (isSearchMode()) {
+ return TextUtils.isEmpty(getQueryString());
+ } else {
+ return super.isEmpty();
+ }
+ }
+
+ public boolean isLoading() {
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition && ((DirectoryPartition) partition).isLoading()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Changes visibility parameters for the default directory partition. */
+ public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
+ int defaultPartitionIndex = -1;
+ int count = getPartitionCount();
+ for (int i = 0; i < count; i++) {
+ Partition partition = getPartition(i);
+ if (partition instanceof DirectoryPartition
+ && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
+ defaultPartitionIndex = i;
+ break;
+ }
+ }
+ if (defaultPartitionIndex != -1) {
+ setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
+ setHasHeader(defaultPartitionIndex, hasHeader);
+ }
+ }
+
+ @Override
+ protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ View view = inflater.inflate(R.layout.directory_header, parent, false);
+ if (!getPinnedPartitionHeadersEnabled()) {
+ // If the headers are unpinned, there is no need for their background
+ // color to be non-transparent. Setting this transparent reduces maintenance for
+ // non-pinned headers. We don't need to bother synchronizing the activity's
+ // background color with the header background color.
+ view.setBackground(null);
+ }
+ return view;
+ }
+
+ protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) {
+ final Partition partition = getPartition(partitionId);
+ if (partition instanceof DirectoryPartition) {
+ final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ final long directoryId = directoryPartition.getDirectoryId();
+ final long userType = ContactsUtils.determineUserType(directoryId, null);
+ view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK);
+ }
+ }
+
+ @Override
+ protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
+ Partition partition = getPartition(partitionIndex);
+ if (!(partition instanceof DirectoryPartition)) {
+ return;
+ }
+
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ long directoryId = directoryPartition.getDirectoryId();
+ TextView labelTextView = (TextView) view.findViewById(R.id.label);
+ TextView displayNameTextView = (TextView) view.findViewById(R.id.display_name);
+ labelTextView.setText(directoryPartition.getLabel());
+ if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ displayNameTextView.setText(null);
+ } else {
+ String directoryName = directoryPartition.getDisplayName();
+ String displayName =
+ !TextUtils.isEmpty(directoryName) ? directoryName : directoryPartition.getDirectoryType();
+ displayNameTextView.setText(displayName);
+ }
+
+ final Resources res = getContext().getResources();
+ final int headerPaddingTop =
+ partitionIndex == 1 && getPartition(0).isEmpty()
+ ? 0
+ : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
+ // There should be no extra padding at the top of the first directory header
+ view.setPaddingRelative(
+ view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), view.getPaddingBottom());
+ }
+
+ /** Checks whether the contact entry at the given position represents the user's profile. */
+ protected boolean isUserProfile(int position) {
+ // The profile only ever appears in the first position if it is present. So if the position
+ // is anything beyond 0, it can't be the profile.
+ boolean isUserProfile = false;
+ if (position == 0) {
+ int partition = getPartitionForPosition(position);
+ if (partition >= 0) {
+ // Save the old cursor position - the call to getItem() may modify the cursor
+ // position.
+ int offset = getCursor(partition).getPosition();
+ Cursor cursor = (Cursor) getItem(position);
+ if (cursor != null) {
+ int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
+ if (profileColumnIndex != -1) {
+ isUserProfile = cursor.getInt(profileColumnIndex) == 1;
+ }
+ // Restore the old cursor position.
+ cursor.moveToPosition(offset);
+ }
+ }
+ }
+ return isUserProfile;
+ }
+
+ public boolean isPhotoSupported(int partitionIndex) {
+ Partition partition = getPartition(partitionIndex);
+ if (partition instanceof DirectoryPartition) {
+ return ((DirectoryPartition) partition).isPhotoSupported();
+ }
+ return true;
+ }
+
+ /** Returns the currently selected filter. */
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ mFilter = filter;
+ }
+
+ // TODO: move sharable logic (bindXX() methods) to here with extra arguments
+
+ /**
+ * Loads the photo for the quick contact view and assigns the contact uri.
+ *
+ * @param photoIdColumn Index of the photo id column
+ * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
+ * @param contactIdColumn Index of the contact id column
+ * @param lookUpKeyColumn Index of the lookup key column
+ * @param displayNameColumn Index of the display name column
+ */
+ protected void bindQuickContact(
+ final ContactListItemView view,
+ int partitionIndex,
+ Cursor cursor,
+ int photoIdColumn,
+ int photoUriColumn,
+ int contactIdColumn,
+ int lookUpKeyColumn,
+ int displayNameColumn) {
+ long photoId = 0;
+ if (!cursor.isNull(photoIdColumn)) {
+ photoId = cursor.getLong(photoIdColumn);
+ }
+
+ QuickContactBadge quickContact = view.getQuickContact();
+ quickContact.assignContactUri(
+ getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
+ if (CompatUtils.hasPrioritizedMimeType()) {
+ // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume
+ // that only Dialer will use this QuickContact badge. This means prioritizing the phone
+ // mimetype here is reasonable.
+ quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ }
+
+ if (photoId != 0 || photoUriColumn == -1) {
+ getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, null);
+ } else {
+ final String photoUriString = cursor.getString(photoUriColumn);
+ final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+ DefaultImageRequest request = null;
+ if (photoUri == null) {
+ request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, lookUpKeyColumn);
+ }
+ getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, request);
+ }
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ // Whenever bindViewId() is called, the values passed into setId() are stable or
+ // stable-ish. For example, when one contact is modified we don't expect a second
+ // contact's Contact._ID values to change.
+ return true;
+ }
+
+ protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
+ // Set a semi-stable id, so that talkback won't get confused when the list gets
+ // refreshed. There is little harm in inserting the same ID twice.
+ long contactId = cursor.getLong(idColumn);
+ view.setId((int) (contactId % Integer.MAX_VALUE));
+ }
+
+ protected Uri getContactUri(
+ int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) {
+ long contactId = cursor.getLong(contactIdColumn);
+ String lookupKey = cursor.getString(lookUpKeyColumn);
+ long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId();
+ Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+ if (uri != null && directoryId != Directory.DEFAULT) {
+ uri =
+ uri.buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
+ .build();
+ }
+ return uri;
+ }
+
+ /**
+ * Retrieves the lookup key and display name from a cursor, and returns a {@link
+ * DefaultImageRequest} containing these contact details
+ *
+ * @param cursor Contacts cursor positioned at the current row to retrieve contact details for
+ * @param displayNameColumn Column index of the display name
+ * @param lookupKeyColumn Column index of the lookup key
+ * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
+ * display name and lookup key of the contact.
+ */
+ public DefaultImageRequest getDefaultImageRequestFromCursor(
+ Cursor cursor, int displayNameColumn, int lookupKeyColumn) {
+ final String displayName = cursor.getString(displayNameColumn);
+ final String lookupKey = cursor.getString(lookupKeyColumn);
+ return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java
new file mode 100644
index 000000000..a8d9b55ba
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactEntryListFragment.java
@@ -0,0 +1,862 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListView;
+import com.android.common.widget.CompositeCursorAdapter.Partition;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactListViewUtils;
+import java.util.Locale;
+
+/** Common base class for various contact-related list fragments. */
+public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter> extends Fragment
+ implements OnItemClickListener,
+ OnScrollListener,
+ OnFocusChangeListener,
+ OnTouchListener,
+ OnItemLongClickListener,
+ LoaderCallbacks<Cursor> {
+ private static final String TAG = "ContactEntryListFragment";
+ private static final String KEY_LIST_STATE = "liststate";
+ private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
+ private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
+ private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
+ private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled";
+ private static final String KEY_INCLUDE_PROFILE = "includeProfile";
+ private static final String KEY_SEARCH_MODE = "searchMode";
+ private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
+ private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition";
+ private static final String KEY_QUERY_STRING = "queryString";
+ private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode";
+ private static final String KEY_SELECTION_VISIBLE = "selectionVisible";
+ private static final String KEY_DARK_THEME = "darkTheme";
+ private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility";
+ private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit";
+
+ private static final String DIRECTORY_ID_ARG_KEY = "directoryId";
+
+ private static final int DIRECTORY_LOADER_ID = -1;
+
+ private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
+ private static final int DIRECTORY_SEARCH_MESSAGE = 1;
+
+ private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
+ private static final int STATUS_NOT_LOADED = 0;
+ private static final int STATUS_LOADING = 1;
+ private static final int STATUS_LOADED = 2;
+ protected boolean mUserProfileExists;
+ private boolean mSectionHeaderDisplayEnabled;
+ private boolean mPhotoLoaderEnabled;
+ private boolean mQuickContactEnabled = true;
+ private boolean mAdjustSelectionBoundsEnabled = true;
+ private boolean mIncludeProfile;
+ private boolean mSearchMode;
+ private boolean mVisibleScrollbarEnabled;
+ private boolean mShowEmptyListForEmptyQuery;
+ private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition();
+ private String mQueryString;
+ private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
+ private boolean mSelectionVisible;
+ private boolean mLegacyCompatibility;
+ private boolean mEnabled = true;
+ private T mAdapter;
+ private View mView;
+ private ListView mListView;
+ /** Used to save the scrolling state of the list when the fragment is not recreated. */
+ private int mListViewTopIndex;
+
+ private int mListViewTopOffset;
+ /** Used for keeping track of the scroll state of the list. */
+ private Parcelable mListState;
+
+ private int mDisplayOrder;
+ private int mSortOrder;
+ private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT;
+ private ContactPhotoManager mPhotoManager;
+ private ContactsPreferences mContactsPrefs;
+ private boolean mForceLoad;
+ private boolean mDarkTheme;
+ private int mDirectoryListStatus = STATUS_NOT_LOADED;
+
+ /**
+ * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by
+ * a change notification (true)
+ */
+ private boolean mLoadPriorityDirectoriesOnly;
+
+ private Context mContext;
+
+ private LoaderManager mLoaderManager;
+
+ private Handler mDelayedDirectorySearchHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
+ loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
+ }
+ }
+ };
+ private ContactsPreferences.ChangeListener mPreferencesChangeListener =
+ new ContactsPreferences.ChangeListener() {
+ @Override
+ public void onChange() {
+ loadPreferences();
+ reloadData();
+ }
+ };
+
+ protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
+
+ protected abstract T createListAdapter();
+
+ /**
+ * @param position Please note that the position is already adjusted for header views, so "0"
+ * means the first list item below header views.
+ */
+ protected abstract void onItemClick(int position, long id);
+
+ /**
+ * @param position Please note that the position is already adjusted for header views, so "0"
+ * means the first list item below header views.
+ */
+ protected boolean onItemLongClick(int position, long id) {
+ return false;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ setContext(activity);
+ setLoaderManager(super.getLoaderManager());
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ /** Sets a context for the fragment in the unit test environment. */
+ public void setContext(Context context) {
+ mContext = context;
+ configurePhotoLoader();
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (mEnabled != enabled) {
+ mEnabled = enabled;
+ if (mAdapter != null) {
+ if (mEnabled) {
+ reloadData();
+ } else {
+ mAdapter.clearPartitions();
+ }
+ }
+ }
+ }
+
+ @Override
+ public LoaderManager getLoaderManager() {
+ return mLoaderManager;
+ }
+
+ /** Overrides a loader manager for use in unit tests. */
+ public void setLoaderManager(LoaderManager loaderManager) {
+ mLoaderManager = loaderManager;
+ }
+
+ public T getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public View getView() {
+ return mView;
+ }
+
+ public ListView getListView() {
+ return mListView;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled);
+ outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
+ outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
+ outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
+ outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile);
+ outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
+ outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
+ outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition);
+ outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode);
+ outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible);
+ outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility);
+ outState.putString(KEY_QUERY_STRING, mQueryString);
+ outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit);
+ outState.putBoolean(KEY_DARK_THEME, mDarkTheme);
+
+ if (mListView != null) {
+ outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedState) {
+ super.onCreate(savedState);
+ restoreSavedState(savedState);
+ mAdapter = createListAdapter();
+ mContactsPrefs = new ContactsPreferences(mContext);
+ }
+
+ public void restoreSavedState(Bundle savedState) {
+ if (savedState == null) {
+ return;
+ }
+
+ mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED);
+ mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
+ mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
+ mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
+ mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE);
+ mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
+ mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
+ mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION);
+ mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE);
+ mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE);
+ mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY);
+ mQueryString = savedState.getString(KEY_QUERY_STRING);
+ mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT);
+ mDarkTheme = savedState.getBoolean(KEY_DARK_THEME);
+
+ // Retrieve list state. This will be applied in onLoadFinished
+ mListState = savedState.getParcelable(KEY_LIST_STATE);
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
+
+ mForceLoad = loadPreferences();
+
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ mLoadPriorityDirectoriesOnly = true;
+
+ startLoading();
+ }
+
+ protected void startLoading() {
+ if (mAdapter == null) {
+ // The method was called before the fragment was started
+ return;
+ }
+
+ configureAdapter();
+ int partitionCount = mAdapter.getPartitionCount();
+ for (int i = 0; i < partitionCount; i++) {
+ Partition partition = mAdapter.getPartition(i);
+ if (partition instanceof DirectoryPartition) {
+ DirectoryPartition directoryPartition = (DirectoryPartition) partition;
+ if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
+ if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {
+ startLoadingDirectoryPartition(i);
+ }
+ }
+ } else {
+ getLoaderManager().initLoader(i, null, this);
+ }
+ }
+
+ // Next time this method is called, we should start loading non-priority directories
+ mLoadPriorityDirectoriesOnly = false;
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ if (id == DIRECTORY_LOADER_ID) {
+ DirectoryListLoader loader = new DirectoryListLoader(mContext);
+ loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
+ loader.setLocalInvisibleDirectoryEnabled(
+ ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
+ return loader;
+ } else {
+ CursorLoader loader = createCursorLoader(mContext);
+ long directoryId =
+ args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
+ ? args.getLong(DIRECTORY_ID_ARG_KEY)
+ : Directory.DEFAULT;
+ mAdapter.configureLoader(loader, directoryId);
+ return loader;
+ }
+ }
+
+ public CursorLoader createCursorLoader(Context context) {
+ return new CursorLoader(context, null, null, null, null, null) {
+ @Override
+ protected Cursor onLoadInBackground() {
+ try {
+ return super.onLoadInBackground();
+ } catch (RuntimeException e) {
+ // We don't even know what the projection should be, so no point trying to
+ // return an empty MatrixCursor with the correct projection here.
+ Log.w(TAG, "RuntimeException while trying to query ContactsProvider.");
+ return null;
+ }
+ }
+ };
+ }
+
+ private void startLoadingDirectoryPartition(int partitionIndex) {
+ DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex);
+ partition.setStatus(DirectoryPartition.STATUS_LOADING);
+ long directoryId = partition.getDirectoryId();
+ if (mForceLoad) {
+ if (directoryId == Directory.DEFAULT) {
+ loadDirectoryPartition(partitionIndex, partition);
+ } else {
+ loadDirectoryPartitionDelayed(partitionIndex, partition);
+ }
+ } else {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
+ getLoaderManager().initLoader(partitionIndex, args, this);
+ }
+ }
+
+ /**
+ * Queues up a delayed request to search the specified directory. Since directory search will
+ * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing
+ * before sending a directory request.
+ */
+ private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
+ mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
+ Message msg =
+ mDelayedDirectorySearchHandler.obtainMessage(
+ DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
+ mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
+ }
+
+ /** Loads the directory partition. */
+ protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
+ Bundle args = new Bundle();
+ args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
+ getLoaderManager().restartLoader(partitionIndex, args, this);
+ }
+
+ /** Cancels all queued directory loading requests. */
+ private void removePendingDirectorySearchRequests() {
+ mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (!mEnabled) {
+ return;
+ }
+
+ int loaderId = loader.getId();
+ if (loaderId == DIRECTORY_LOADER_ID) {
+ mDirectoryListStatus = STATUS_LOADED;
+ mAdapter.changeDirectories(data);
+ startLoading();
+ } else {
+ onPartitionLoaded(loaderId, data);
+ if (isSearchMode()) {
+ int directorySearchMode = getDirectorySearchMode();
+ if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
+ if (mDirectoryListStatus == STATUS_NOT_LOADED) {
+ mDirectoryListStatus = STATUS_LOADING;
+ getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
+ } else {
+ startLoading();
+ }
+ }
+ } else {
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+ }
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {}
+
+ protected void onPartitionLoaded(int partitionIndex, Cursor data) {
+ if (partitionIndex >= mAdapter.getPartitionCount()) {
+ // When we get unsolicited data, ignore it. This could happen
+ // when we are switching from search mode to the default mode.
+ return;
+ }
+
+ mAdapter.changeCursor(partitionIndex, data);
+ setProfileHeader();
+
+ if (!isLoading()) {
+ completeRestoreInstanceState();
+ }
+ }
+
+ public boolean isLoading() {
+ if (mAdapter != null && mAdapter.isLoading()) {
+ return true;
+ }
+
+ return isLoadingDirectoryList();
+
+ }
+
+ public boolean isLoadingDirectoryList() {
+ return isSearchMode()
+ && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
+ && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mContactsPrefs.unregisterChangeListener();
+ mAdapter.clearPartitions();
+ }
+
+ protected void reloadData() {
+ removePendingDirectorySearchRequests();
+ mAdapter.onDataReload();
+ mLoadPriorityDirectoriesOnly = true;
+ mForceLoad = true;
+ startLoading();
+ }
+
+ /**
+ * Shows a view at the top of the list with a pseudo local profile prompting the user to add a
+ * local profile. Default implementation does nothing.
+ */
+ protected void setProfileHeader() {
+ mUserProfileExists = false;
+ }
+
+ /** Provides logic that dismisses this fragment. The default implementation does nothing. */
+ protected void finish() {}
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ if (mSectionHeaderDisplayEnabled != flag) {
+ mSectionHeaderDisplayEnabled = flag;
+ if (mAdapter != null) {
+ mAdapter.setSectionHeaderDisplayEnabled(flag);
+ }
+ configureVerticalScrollbar();
+ }
+ }
+
+ public boolean isVisibleScrollbarEnabled() {
+ return mVisibleScrollbarEnabled;
+ }
+
+ public void setVisibleScrollbarEnabled(boolean flag) {
+ if (mVisibleScrollbarEnabled != flag) {
+ mVisibleScrollbarEnabled = flag;
+ configureVerticalScrollbar();
+ }
+ }
+
+ private void configureVerticalScrollbar() {
+ boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled();
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(hasScrollbar);
+ mListView.setFastScrollAlwaysVisible(hasScrollbar);
+ mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
+ mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+ }
+ }
+
+ public boolean isPhotoLoaderEnabled() {
+ return mPhotoLoaderEnabled;
+ }
+
+ public void setPhotoLoaderEnabled(boolean flag) {
+ mPhotoLoaderEnabled = flag;
+ configurePhotoLoader();
+ }
+
+ public void setQuickContactEnabled(boolean flag) {
+ this.mQuickContactEnabled = flag;
+ }
+
+ public void setAdjustSelectionBoundsEnabled(boolean flag) {
+ mAdjustSelectionBoundsEnabled = flag;
+ }
+
+ public final boolean isSearchMode() {
+ return mSearchMode;
+ }
+
+ /**
+ * Enter/exit search mode. This is method is tightly related to the current query, and should only
+ * be called by {@link #setQueryString}.
+ *
+ * <p>Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it.
+ */
+ protected void setSearchMode(boolean flag) {
+ if (mSearchMode != flag) {
+ mSearchMode = flag;
+ setSectionHeaderDisplayEnabled(!mSearchMode);
+
+ if (!flag) {
+ mDirectoryListStatus = STATUS_NOT_LOADED;
+ getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
+ }
+
+ if (mAdapter != null) {
+ mAdapter.setSearchMode(flag);
+
+ mAdapter.clearPartitions();
+ if (!flag) {
+ // If we are switching from search to regular display, remove all directory
+ // partitions after default one, assuming they are remote directories which
+ // should be cleaned up on exiting the search mode.
+ mAdapter.removeDirectoriesAfterDefault();
+ }
+ mAdapter.configureDefaultPartition(false, flag);
+ }
+
+ if (mListView != null) {
+ mListView.setFastScrollEnabled(!flag);
+ }
+ }
+ }
+
+ public final String getQueryString() {
+ return mQueryString;
+ }
+
+ public void setQueryString(String queryString) {
+ if (!TextUtils.equals(mQueryString, queryString)) {
+ if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) {
+ if (TextUtils.isEmpty(mQueryString)) {
+ // Restore the adapter if the query used to be empty.
+ mListView.setAdapter(mAdapter);
+ } else if (TextUtils.isEmpty(queryString)) {
+ // Instantly clear the list view if the new query is empty.
+ mListView.setAdapter(null);
+ }
+ }
+
+ mQueryString = queryString;
+ setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery);
+
+ if (mAdapter != null) {
+ mAdapter.setQueryString(queryString);
+ reloadData();
+ }
+ }
+ }
+
+ public void setShowEmptyListForNullQuery(boolean show) {
+ mShowEmptyListForEmptyQuery = show;
+ }
+
+ public boolean getShowEmptyListForNullQuery() {
+ return mShowEmptyListForEmptyQuery;
+ }
+
+ public int getDirectoryLoaderId() {
+ return DIRECTORY_LOADER_ID;
+ }
+
+ public int getDirectorySearchMode() {
+ return mDirectorySearchMode;
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ protected int getContactNameDisplayOrder() {
+ return mDisplayOrder;
+ }
+
+ protected void setContactNameDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ if (mAdapter != null) {
+ mAdapter.setContactNameDisplayOrder(displayOrder);
+ }
+ }
+
+ public int getSortOrder() {
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ if (mAdapter != null) {
+ mAdapter.setSortOrder(sortOrder);
+ }
+ }
+
+ public void setDirectoryResultLimit(int limit) {
+ mDirectoryResultLimit = limit;
+ }
+
+ protected boolean loadPreferences() {
+ boolean changed = false;
+ if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
+ setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+ changed = true;
+ }
+
+ if (getSortOrder() != mContactsPrefs.getSortOrder()) {
+ setSortOrder(mContactsPrefs.getSortOrder());
+ changed = true;
+ }
+
+ return changed;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ onCreateView(inflater, container);
+
+ boolean searchMode = isSearchMode();
+ mAdapter.setSearchMode(searchMode);
+ mAdapter.configureDefaultPartition(false, searchMode);
+ mAdapter.setPhotoLoader(mPhotoManager);
+ mListView.setAdapter(mAdapter);
+
+ if (!isSearchMode()) {
+ mListView.setFocusableInTouchMode(true);
+ mListView.requestFocus();
+ }
+
+ return mView;
+ }
+
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ mView = inflateView(inflater, container);
+
+ mListView = (ListView) mView.findViewById(android.R.id.list);
+ if (mListView == null) {
+ throw new RuntimeException(
+ "Your content must have a ListView whose id attribute is " + "'android.R.id.list'");
+ }
+
+ View emptyView = mView.findViewById(android.R.id.empty);
+ if (emptyView != null) {
+ mListView.setEmptyView(emptyView);
+ }
+
+ mListView.setOnItemClickListener(this);
+ mListView.setOnItemLongClickListener(this);
+ mListView.setOnFocusChangeListener(this);
+ mListView.setOnTouchListener(this);
+ mListView.setFastScrollEnabled(!isSearchMode());
+
+ // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
+ // them when an A-Z headers is visible.
+ mListView.setDividerHeight(0);
+
+ // We manually save/restore the listview state
+ mListView.setSaveEnabled(false);
+
+ configureVerticalScrollbar();
+ configurePhotoLoader();
+
+ getAdapter().setFragmentRootView(getView());
+
+ ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView);
+ }
+
+ @Override
+ public void onHiddenChanged(boolean hidden) {
+ super.onHiddenChanged(hidden);
+ if (getActivity() != null && getView() != null && !hidden) {
+ // If the padding was last applied when in a hidden state, it may have been applied
+ // incorrectly. Therefore we need to reapply it.
+ ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView());
+ }
+ }
+
+ protected void configurePhotoLoader() {
+ if (isPhotoLoaderEnabled() && mContext != null) {
+ if (mPhotoManager == null) {
+ mPhotoManager = ContactPhotoManager.getInstance(mContext);
+ }
+ if (mListView != null) {
+ mListView.setOnScrollListener(this);
+ }
+ if (mAdapter != null) {
+ mAdapter.setPhotoLoader(mPhotoManager);
+ }
+ }
+ }
+
+ protected void configureAdapter() {
+ if (mAdapter == null) {
+ return;
+ }
+
+ mAdapter.setQuickContactEnabled(mQuickContactEnabled);
+ mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
+ mAdapter.setQueryString(mQueryString);
+ mAdapter.setDirectorySearchMode(mDirectorySearchMode);
+ mAdapter.setPinnedPartitionHeadersEnabled(false);
+ mAdapter.setContactNameDisplayOrder(mDisplayOrder);
+ mAdapter.setSortOrder(mSortOrder);
+ mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
+ mAdapter.setSelectionVisible(mSelectionVisible);
+ mAdapter.setDirectoryResultLimit(mDirectoryResultLimit);
+ mAdapter.setDarkTheme(mDarkTheme);
+ }
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
+ mPhotoManager.pause();
+ } else if (isPhotoLoaderEnabled()) {
+ mPhotoManager.resume();
+ }
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ hideSoftKeyboard();
+
+ int adjPosition = position - mListView.getHeaderViewsCount();
+ if (adjPosition >= 0) {
+ onItemClick(adjPosition, id);
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ int adjPosition = position - mListView.getHeaderViewsCount();
+
+ if (adjPosition >= 0) {
+ return onItemLongClick(adjPosition, id);
+ }
+ return false;
+ }
+
+ private void hideSoftKeyboard() {
+ // Hide soft keyboard, if visible
+ InputMethodManager inputMethodManager =
+ (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+ }
+
+ /** Dismisses the soft keyboard when the list takes focus. */
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (view == mListView && hasFocus) {
+ hideSoftKeyboard();
+ }
+ }
+
+ /** Dismisses the soft keyboard when the list is touched. */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (view == mListView) {
+ hideSoftKeyboard();
+ }
+ return false;
+ }
+
+ @Override
+ public void onPause() {
+ // Save the scrolling state of the list view
+ mListViewTopIndex = mListView.getFirstVisiblePosition();
+ View v = mListView.getChildAt(0);
+ mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
+
+ super.onPause();
+ removePendingDirectorySearchRequests();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ // Restore the selection of the list view. See b/19982820.
+ // This has to be done manually because if the list view has its emptyView set,
+ // the scrolling state will be reset when clearPartitions() is called on the adapter.
+ mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset);
+ }
+
+ /** Restore the list state after the adapter is populated. */
+ protected void completeRestoreInstanceState() {
+ if (mListState != null) {
+ mListView.onRestoreInstanceState(mListState);
+ mListState = null;
+ }
+ }
+
+ public void setDarkTheme(boolean value) {
+ mDarkTheme = value;
+ if (mAdapter != null) {
+ mAdapter.setDarkTheme(value);
+ }
+ }
+
+ private int getDefaultVerticalScrollbarPosition() {
+ final Locale locale = Locale.getDefault();
+ final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
+ switch (layoutDirection) {
+ case View.LAYOUT_DIRECTION_RTL:
+ return View.SCROLLBAR_POSITION_LEFT;
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ return View.SCROLLBAR_POSITION_RIGHT;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListAdapter.java b/java/com/android/contacts/common/list/ContactListAdapter.java
new file mode 100644
index 000000000..6cd311811
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListAdapter.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippets;
+import android.view.ViewGroup;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.R;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/**
+ * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. Also
+ * includes support for including the {@link ContactsContract.Profile} record in the list.
+ */
+public abstract class ContactListAdapter extends ContactEntryListAdapter {
+
+ private CharSequence mUnknownNameText;
+
+ public ContactListAdapter(Context context) {
+ super(context);
+
+ mUnknownNameText = context.getText(R.string.missing_name);
+ }
+
+ protected static Uri buildSectionIndexerUri(Uri uri) {
+ return uri.buildUpon().appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build();
+ }
+
+ public Uri getContactUri(int partitionIndex, Cursor cursor) {
+ long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+ String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY);
+ Uri uri = Contacts.getLookupUri(contactId, lookupKey);
+ long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId();
+ if (uri != null && directoryId != Directory.DEFAULT) {
+ uri =
+ uri.buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
+ .build();
+ }
+ return uri;
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setQuickContactEnabled(isQuickContactEnabled());
+ view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
+ view.setActivatedStateSupported(isSelectionVisible());
+ return view;
+ }
+
+ protected void bindSectionHeaderAndDivider(
+ ContactListItemView view, int position, Cursor cursor) {
+ view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
+ if (isSectionHeaderDisplayEnabled()) {
+ Placement placement = getItemPlacementInSection(position);
+ view.setSectionHeader(placement.sectionHeader);
+ } else {
+ view.setSectionHeader(null);
+ }
+ }
+
+ protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+ if (!isPhotoSupported(partitionIndex)) {
+ view.removePhotoView();
+ return;
+ }
+
+ // Set the photo, if available
+ long photoId = 0;
+ if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) {
+ photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID);
+ }
+
+ if (photoId != 0) {
+ getPhotoLoader()
+ .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null);
+ } else {
+ final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI);
+ final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+ DefaultImageRequest request = null;
+ if (photoUri == null) {
+ request =
+ getDefaultImageRequestFromCursor(
+ cursor, ContactQuery.CONTACT_DISPLAY_NAME, ContactQuery.CONTACT_LOOKUP_KEY);
+ }
+ getPhotoLoader()
+ .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request);
+ }
+ }
+
+ protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, ContactQuery.CONTACT_DISPLAY_NAME);
+ // Note: we don't show phonetic any more (See issue 5265330)
+
+ bindViewId(view, cursor, ContactQuery.CONTACT_ID);
+ }
+
+ protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) {
+ view.showPresenceAndStatusMessage(
+ cursor, ContactQuery.CONTACT_PRESENCE_STATUS, ContactQuery.CONTACT_CONTACT_STATUS);
+ }
+
+ protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) {
+ view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET);
+ }
+
+ @Override
+ public void changeCursor(int partitionIndex, Cursor cursor) {
+ super.changeCursor(partitionIndex, cursor);
+
+ if (cursor == null || !cursor.moveToFirst()) {
+ return;
+ }
+
+ // hasProfile tells whether the first row is a profile
+ final boolean hasProfile = cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1;
+
+ // Add ME profile on top of favorites
+ cursor.moveToFirst();
+ setProfileExists(hasProfile);
+ }
+
+ /** @return Projection useful for children. */
+ protected final String[] getProjection(boolean forSearch) {
+ final int sortOrder = getContactNameDisplayOrder();
+ if (forSearch) {
+ if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ return ContactQuery.FILTER_PROJECTION_PRIMARY;
+ } else {
+ return ContactQuery.FILTER_PROJECTION_ALTERNATIVE;
+ }
+ } else {
+ if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ return ContactQuery.CONTACT_PROJECTION_PRIMARY;
+ } else {
+ return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE;
+ }
+ }
+ }
+
+ protected static class ContactQuery {
+
+ public static final int CONTACT_ID = 0;
+ public static final int CONTACT_DISPLAY_NAME = 1;
+ public static final int CONTACT_PRESENCE_STATUS = 2;
+ public static final int CONTACT_CONTACT_STATUS = 3;
+ public static final int CONTACT_PHOTO_ID = 4;
+ public static final int CONTACT_PHOTO_URI = 5;
+ public static final int CONTACT_LOOKUP_KEY = 6;
+ public static final int CONTACT_IS_USER_PROFILE = 7;
+ public static final int CONTACT_PHONETIC_NAME = 8;
+ public static final int CONTACT_STARRED = 9;
+ public static final int CONTACT_SNIPPET = 10;
+ private static final String[] CONTACT_PROJECTION_PRIMARY =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_PRIMARY, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ };
+ private static final String[] CONTACT_PROJECTION_ALTERNATIVE =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ };
+ private static final String[] FILTER_PROJECTION_PRIMARY =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_PRIMARY, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ SearchSnippets.SNIPPET, // 10
+ };
+ private static final String[] FILTER_PROJECTION_ALTERNATIVE =
+ new String[] {
+ Contacts._ID, // 0
+ Contacts.DISPLAY_NAME_ALTERNATIVE, // 1
+ Contacts.CONTACT_PRESENCE, // 2
+ Contacts.CONTACT_STATUS, // 3
+ Contacts.PHOTO_ID, // 4
+ Contacts.PHOTO_THUMBNAIL_URI, // 5
+ Contacts.LOOKUP_KEY, // 6
+ Contacts.IS_USER_PROFILE, // 7
+ Contacts.PHONETIC_NAME, // 8
+ Contacts.STARRED, // 9
+ SearchSnippets.SNIPPET, // 10
+ };
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListFilter.java b/java/com/android/contacts/common/list/ContactListFilter.java
new file mode 100644
index 000000000..1a03bb64c
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListFilter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+
+/** Contact list filter parameters. */
+public final class ContactListFilter implements Comparable<ContactListFilter>, Parcelable {
+
+ public static final int FILTER_TYPE_DEFAULT = -1;
+ public static final int FILTER_TYPE_ALL_ACCOUNTS = -2;
+ public static final int FILTER_TYPE_CUSTOM = -3;
+ public static final int FILTER_TYPE_STARRED = -4;
+ public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5;
+ public static final int FILTER_TYPE_SINGLE_CONTACT = -6;
+
+ public static final int FILTER_TYPE_ACCOUNT = 0;
+ public static final Parcelable.Creator<ContactListFilter> CREATOR =
+ new Parcelable.Creator<ContactListFilter>() {
+ @Override
+ public ContactListFilter createFromParcel(Parcel source) {
+ int filterType = source.readInt();
+ String accountName = source.readString();
+ String accountType = source.readString();
+ String dataSet = source.readString();
+ return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+ }
+
+ @Override
+ public ContactListFilter[] newArray(int size) {
+ return new ContactListFilter[size];
+ }
+ };
+ /**
+ * Obsolete filter which had been used in Honeycomb. This may be stored in {@link
+ * SharedPreferences}, but should be replaced with ALL filter when it is found.
+ *
+ * <p>TODO: "group" filter and relevant variables are all obsolete. Remove them.
+ */
+ private static final int FILTER_TYPE_GROUP = 1;
+
+ private static final String KEY_FILTER_TYPE = "filter.type";
+ private static final String KEY_ACCOUNT_NAME = "filter.accountName";
+ private static final String KEY_ACCOUNT_TYPE = "filter.accountType";
+ private static final String KEY_DATA_SET = "filter.dataSet";
+ public final int filterType;
+ public final String accountType;
+ public final String accountName;
+ public final String dataSet;
+ public final Drawable icon;
+ private String mId;
+
+ public ContactListFilter(
+ int filterType, String accountType, String accountName, String dataSet, Drawable icon) {
+ this.filterType = filterType;
+ this.accountType = accountType;
+ this.accountName = accountName;
+ this.dataSet = dataSet;
+ this.icon = icon;
+ }
+
+ public static ContactListFilter createFilterWithType(int filterType) {
+ return new ContactListFilter(filterType, null, null, null, null);
+ }
+
+ public static ContactListFilter createAccountFilter(
+ String accountType, String accountName, String dataSet, Drawable icon) {
+ return new ContactListFilter(
+ ContactListFilter.FILTER_TYPE_ACCOUNT, accountType, accountName, dataSet, icon);
+ }
+
+ /**
+ * Store the given {@link ContactListFilter} to preferences. If the requested filter is of type
+ * {@link #FILTER_TYPE_SINGLE_CONTACT} then do not save it to preferences because it is a
+ * temporary state.
+ */
+ public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) {
+ if (filter != null && filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+ return;
+ }
+ prefs
+ .edit()
+ .putInt(KEY_FILTER_TYPE, filter == null ? FILTER_TYPE_DEFAULT : filter.filterType)
+ .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName)
+ .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType)
+ .putString(KEY_DATA_SET, filter == null ? null : filter.dataSet)
+ .apply();
+ }
+
+ /**
+ * Try to obtain ContactListFilter object saved in SharedPreference. If there's no info there,
+ * return ALL filter instead.
+ */
+ public static ContactListFilter restoreDefaultPreferences(SharedPreferences prefs) {
+ ContactListFilter filter = restoreFromPreferences(prefs);
+ if (filter == null) {
+ filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+ }
+ // "Group" filter is obsolete and thus is not exposed anymore. The "single contact mode"
+ // should also not be stored in preferences anymore since it is a temporary state.
+ if (filter.filterType == FILTER_TYPE_GROUP || filter.filterType == FILTER_TYPE_SINGLE_CONTACT) {
+ filter = ContactListFilter.createFilterWithType(FILTER_TYPE_ALL_ACCOUNTS);
+ }
+ return filter;
+ }
+
+ private static ContactListFilter restoreFromPreferences(SharedPreferences prefs) {
+ int filterType = prefs.getInt(KEY_FILTER_TYPE, FILTER_TYPE_DEFAULT);
+ if (filterType == FILTER_TYPE_DEFAULT) {
+ return null;
+ }
+
+ String accountName = prefs.getString(KEY_ACCOUNT_NAME, null);
+ String accountType = prefs.getString(KEY_ACCOUNT_TYPE, null);
+ String dataSet = prefs.getString(KEY_DATA_SET, null);
+ return new ContactListFilter(filterType, accountType, accountName, dataSet, null);
+ }
+
+ public static final String filterTypeToString(int filterType) {
+ switch (filterType) {
+ case FILTER_TYPE_DEFAULT:
+ return "FILTER_TYPE_DEFAULT";
+ case FILTER_TYPE_ALL_ACCOUNTS:
+ return "FILTER_TYPE_ALL_ACCOUNTS";
+ case FILTER_TYPE_CUSTOM:
+ return "FILTER_TYPE_CUSTOM";
+ case FILTER_TYPE_STARRED:
+ return "FILTER_TYPE_STARRED";
+ case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY";
+ case FILTER_TYPE_SINGLE_CONTACT:
+ return "FILTER_TYPE_SINGLE_CONTACT";
+ case FILTER_TYPE_ACCOUNT:
+ return "FILTER_TYPE_ACCOUNT";
+ default:
+ return "(unknown)";
+ }
+ }
+
+ /** Returns true if this filter is based on data and may become invalid over time. */
+ public boolean isValidationRequired() {
+ return filterType == FILTER_TYPE_ACCOUNT;
+ }
+
+ @Override
+ public String toString() {
+ switch (filterType) {
+ case FILTER_TYPE_DEFAULT:
+ return "default";
+ case FILTER_TYPE_ALL_ACCOUNTS:
+ return "all_accounts";
+ case FILTER_TYPE_CUSTOM:
+ return "custom";
+ case FILTER_TYPE_STARRED:
+ return "starred";
+ case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ return "with_phones";
+ case FILTER_TYPE_SINGLE_CONTACT:
+ return "single";
+ case FILTER_TYPE_ACCOUNT:
+ return "account: "
+ + accountType
+ + (dataSet != null ? "/" + dataSet : "")
+ + " "
+ + accountName;
+ }
+ return super.toString();
+ }
+
+ @Override
+ public int compareTo(ContactListFilter another) {
+ int res = accountName.compareTo(another.accountName);
+ if (res != 0) {
+ return res;
+ }
+
+ res = accountType.compareTo(another.accountType);
+ if (res != 0) {
+ return res;
+ }
+
+ return filterType - another.filterType;
+ }
+
+ @Override
+ public int hashCode() {
+ int code = filterType;
+ if (accountType != null) {
+ code = code * 31 + accountType.hashCode();
+ code = code * 31 + accountName.hashCode();
+ }
+ if (dataSet != null) {
+ code = code * 31 + dataSet.hashCode();
+ }
+ return code;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof ContactListFilter)) {
+ return false;
+ }
+
+ ContactListFilter otherFilter = (ContactListFilter) other;
+ return filterType == otherFilter.filterType
+ && TextUtils.equals(accountName, otherFilter.accountName)
+ && TextUtils.equals(accountType, otherFilter.accountType)
+ && TextUtils.equals(dataSet, otherFilter.dataSet);
+
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(filterType);
+ dest.writeString(accountName);
+ dest.writeString(accountType);
+ dest.writeString(dataSet);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Returns a string that can be used as a stable persistent identifier for this filter. */
+ public String getId() {
+ if (mId == null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(filterType);
+ if (accountType != null) {
+ sb.append('-').append(accountType);
+ }
+ if (dataSet != null) {
+ sb.append('/').append(dataSet);
+ }
+ if (accountName != null) {
+ sb.append('-').append(accountName.replace('-', '_'));
+ }
+ mId = sb.toString();
+ }
+ return mId;
+ }
+
+ /**
+ * Adds the account query parameters to the given {@code uriBuilder}.
+ *
+ * @throws IllegalStateException if the filter type is not {@link #FILTER_TYPE_ACCOUNT}.
+ */
+ public Uri.Builder addAccountQueryParameterToUrl(Uri.Builder uriBuilder) {
+ if (filterType != FILTER_TYPE_ACCOUNT) {
+ throw new IllegalStateException("filterType must be FILTER_TYPE_ACCOUNT");
+ }
+ uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_NAME, accountName);
+ uriBuilder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, accountType);
+ if (!TextUtils.isEmpty(dataSet)) {
+ uriBuilder.appendQueryParameter(RawContacts.DATA_SET, dataSet);
+ }
+ return uriBuilder;
+ }
+
+ public String toDebugString() {
+ final StringBuilder builder = new StringBuilder();
+ builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")");
+ if (filterType == FILTER_TYPE_ACCOUNT) {
+ builder
+ .append(", accountType: " + accountType)
+ .append(", accountName: " + accountName)
+ .append(", dataSet: " + dataSet);
+ }
+ builder.append(", icon: " + icon + "]");
+ return builder.toString();
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListFilterController.java b/java/com/android/contacts/common/list/ContactListFilterController.java
new file mode 100644
index 000000000..d2168f3f2
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListFilterController.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import com.android.contacts.common.model.AccountTypeManager;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Manages {@link ContactListFilter}. All methods must be called from UI thread. */
+public abstract class ContactListFilterController {
+
+ // singleton to cache the filter controller
+ private static ContactListFilterControllerImpl sFilterController = null;
+
+ public static ContactListFilterController getInstance(Context context) {
+ // We may need to synchronize this in the future if background task will call this.
+ if (sFilterController == null) {
+ sFilterController = new ContactListFilterControllerImpl(context);
+ }
+ return sFilterController;
+ }
+
+ public abstract void addListener(ContactListFilterListener listener);
+
+ public abstract void removeListener(ContactListFilterListener listener);
+
+ /** Return the currently-active filter. */
+ public abstract ContactListFilter getFilter();
+
+ /**
+ * @param filter the filter
+ * @param persistent True when the given filter should be saved soon. False when the filter should
+ * not be saved. The latter case may happen when some Intent requires a certain type of UI
+ * (e.g. single contact) temporarily.
+ */
+ public abstract void setContactListFilter(ContactListFilter filter, boolean persistent);
+
+ public abstract void selectCustomFilter();
+
+ /**
+ * Checks if the current filter is valid and reset the filter if not. It may happen when an
+ * account is removed while the filter points to the account with {@link
+ * ContactListFilter#FILTER_TYPE_ACCOUNT} type, for example. It may also happen if the current
+ * filter is {@link ContactListFilter#FILTER_TYPE_SINGLE_CONTACT}, in which case, we should switch
+ * to the last saved filter in {@link SharedPreferences}.
+ */
+ public abstract void checkFilterValidity(boolean notifyListeners);
+
+ public interface ContactListFilterListener {
+
+ void onContactListFilterChanged();
+ }
+}
+
+/**
+ * Stores the {@link ContactListFilter} selected by the user and saves it to {@link
+ * SharedPreferences} if necessary.
+ */
+class ContactListFilterControllerImpl extends ContactListFilterController {
+
+ private final Context mContext;
+ private final List<ContactListFilterListener> mListeners =
+ new ArrayList<ContactListFilterListener>();
+ private ContactListFilter mFilter;
+
+ public ContactListFilterControllerImpl(Context context) {
+ mContext = context;
+ mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences());
+ checkFilterValidity(true /* notify listeners */);
+ }
+
+ @Override
+ public void addListener(ContactListFilterListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void removeListener(ContactListFilterListener listener) {
+ mListeners.remove(listener);
+ }
+
+ @Override
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ private SharedPreferences getSharedPreferences() {
+ return PreferenceManager.getDefaultSharedPreferences(mContext);
+ }
+
+ @Override
+ public void setContactListFilter(ContactListFilter filter, boolean persistent) {
+ setContactListFilter(filter, persistent, true);
+ }
+
+ private void setContactListFilter(
+ ContactListFilter filter, boolean persistent, boolean notifyListeners) {
+ if (!filter.equals(mFilter)) {
+ mFilter = filter;
+ if (persistent) {
+ ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter);
+ }
+ if (notifyListeners && !mListeners.isEmpty()) {
+ notifyContactListFilterChanged();
+ }
+ }
+ }
+
+ @Override
+ public void selectCustomFilter() {
+ setContactListFilter(
+ ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_CUSTOM), true);
+ }
+
+ private void notifyContactListFilterChanged() {
+ for (ContactListFilterListener listener : mListeners) {
+ listener.onContactListFilterChanged();
+ }
+ }
+
+ @Override
+ public void checkFilterValidity(boolean notifyListeners) {
+ if (mFilter == null) {
+ return;
+ }
+
+ switch (mFilter.filterType) {
+ case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT:
+ setContactListFilter(
+ ContactListFilter.restoreDefaultPreferences(getSharedPreferences()),
+ false,
+ notifyListeners);
+ break;
+ case ContactListFilter.FILTER_TYPE_ACCOUNT:
+ if (!filterAccountExists()) {
+ // The current account filter points to invalid account. Use "all" filter
+ // instead.
+ setContactListFilter(
+ ContactListFilter.createFilterWithType(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS),
+ true,
+ notifyListeners);
+ }
+ }
+ }
+
+ /** @return true if the Account for the current filter exists. */
+ private boolean filterAccountExists() {
+ final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+ final AccountWithDataSet filterAccount =
+ new AccountWithDataSet(mFilter.accountName, mFilter.accountType, mFilter.dataSet);
+ return accountTypeManager.contains(filterAccount, false);
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListItemView.java b/java/com/android/contacts/common/list/ContactListItemView.java
new file mode 100644
index 000000000..76842483a
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListItemView.java
@@ -0,0 +1,1513 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.SearchSnippets;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView.SelectionBoundsAdjuster;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPresenceIconUtil;
+import com.android.contacts.common.ContactStatusUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.format.TextHighlighter;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.contacts.common.util.SearchUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.ViewUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A custom view for an item in the contact list. The view contains the contact's photo, a set of
+ * text views (for name, status, etc...) and icons for presence and call. The view uses no XML file
+ * for layout and all the measurements and layouts are done in the onMeasure and onLayout methods.
+ *
+ * <p>The layout puts the contact's photo on the right side of the view, the call icon (if present)
+ * to the left of the photo, the text lines are aligned to the left and the presence icon (if
+ * present) is set to the left of the status line.
+ *
+ * <p>The layout also supports a header (used as a header of a group of contacts) that is above the
+ * contact's data and a divider between contact view.
+ */
+public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster {
+ private static final Pattern SPLIT_PATTERN =
+ Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+");
+ static final char SNIPPET_START_MATCH = '[';
+ static final char SNIPPET_END_MATCH = ']';
+ /** A helper used to highlight a prefix in a text field. */
+ private final TextHighlighter mTextHighlighter;
+ // Style values for layout and appearance
+ // The initialized values are defaults if none is provided through xml.
+ private int mPreferredHeight = 0;
+ private int mGapBetweenImageAndText = 0;
+ private int mGapBetweenLabelAndData = 0;
+ private int mPresenceIconMargin = 4;
+ private int mPresenceIconSize = 16;
+ private int mTextIndent = 0;
+ private int mTextOffsetTop;
+ private int mNameTextViewTextSize;
+ private int mHeaderWidth;
+ private Drawable mActivatedBackgroundDrawable;
+ private int mVideoCallIconSize = 32;
+ private int mVideoCallIconMargin = 16;
+ // Set in onLayout. Represent left and right position of the View on the screen.
+ private int mLeftOffset;
+ private int mRightOffset;
+ /** Used with {@link #mLabelView}, specifying the width ratio between label and data. */
+ private int mLabelViewWidthWeight = 3;
+ /** Used with {@link #mDataView}, specifying the width ratio between label and data. */
+ private int mDataViewWidthWeight = 5;
+
+ private ArrayList<HighlightSequence> mNameHighlightSequence;
+ private ArrayList<HighlightSequence> mNumberHighlightSequence;
+ // Highlighting prefix for names.
+ private String mHighlightedPrefix;
+ /** Used to notify listeners when a video call icon is clicked. */
+ private PhoneNumberListAdapter.Listener mPhoneNumberListAdapterListener;
+ /** Indicates whether to show the "video call" icon, used to initiate a video call. */
+ private boolean mShowVideoCallIcon = false;
+ /** Indicates whether the view should leave room for the "video call" icon. */
+ private boolean mSupportVideoCallIcon = false;
+
+ private PhotoPosition mPhotoPosition = getDefaultPhotoPosition(false /* normal/non opposite */);
+ // Header layout data
+ private TextView mHeaderTextView;
+ private boolean mIsSectionHeaderEnabled;
+ // The views inside the contact view
+ private boolean mQuickContactEnabled = true;
+ private QuickContactBadge mQuickContact;
+ private ImageView mPhotoView;
+ private TextView mNameTextView;
+ private TextView mLabelView;
+ private TextView mDataView;
+ private TextView mSnippetView;
+ private TextView mStatusView;
+ private ImageView mPresenceIcon;
+ private ImageView mVideoCallIcon;
+ private ImageView mWorkProfileIcon;
+ private ColorStateList mSecondaryTextColor;
+ private int mDefaultPhotoViewSize = 0;
+ /**
+ * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding
+ * to align other data in this View.
+ */
+ private int mPhotoViewWidth;
+ /**
+ * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding.
+ */
+ private int mPhotoViewHeight;
+ /**
+ * Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of
+ * the photo should have horizontal padding on those left assuming there is a photo.
+ */
+ private boolean mKeepHorizontalPaddingForPhotoView;
+ /** Only effective when {@link #mPhotoView} is null. */
+ private boolean mKeepVerticalPaddingForPhotoView;
+ /**
+ * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used.
+ * False indicates those values should be updated before being used in position calculation.
+ */
+ private boolean mPhotoViewWidthAndHeightAreReady = false;
+
+ private int mNameTextViewHeight;
+ private int mNameTextViewTextColor = Color.BLACK;
+ private int mPhoneticNameTextViewHeight;
+ private int mLabelViewHeight;
+ private int mDataViewHeight;
+ private int mSnippetTextViewHeight;
+ private int mStatusTextViewHeight;
+ private int mCheckBoxWidth;
+ // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the
+ // same row.
+ private int mLabelAndDataViewMaxHeight;
+ private boolean mActivatedStateSupported;
+ private boolean mAdjustSelectionBoundsEnabled = true;
+ private Rect mBoundsWithoutHeader = new Rect();
+ private CharSequence mUnknownNameText;
+ private int mPosition;
+
+ public ContactListItemView(Context context) {
+ super(context);
+
+ mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+ mNameHighlightSequence = new ArrayList<HighlightSequence>();
+ mNumberHighlightSequence = new ArrayList<HighlightSequence>();
+ }
+
+ public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) {
+ this(context, attrs);
+
+ mSupportVideoCallIcon = supportVideoCallIcon;
+ }
+
+ public ContactListItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a;
+
+ if (R.styleable.ContactListItemView != null) {
+ // Read all style values
+ a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+ mPreferredHeight =
+ a.getDimensionPixelSize(
+ R.styleable.ContactListItemView_list_item_height, mPreferredHeight);
+ mActivatedBackgroundDrawable =
+ a.getDrawable(R.styleable.ContactListItemView_activated_background);
+
+ mGapBetweenImageAndText =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_gap_between_image_and_text,
+ mGapBetweenImageAndText);
+ mGapBetweenLabelAndData =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_gap_between_label_and_data,
+ mGapBetweenLabelAndData);
+ mPresenceIconMargin =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin);
+ mPresenceIconSize =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize);
+ mDefaultPhotoViewSize =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize);
+ mTextIndent =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_text_indent, mTextIndent);
+ mTextOffsetTop =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop);
+ mDataViewWidthWeight =
+ a.getInteger(
+ R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight);
+ mLabelViewWidthWeight =
+ a.getInteger(
+ R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight);
+ mNameTextViewTextColor =
+ a.getColor(
+ R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor);
+ mNameTextViewTextSize =
+ (int)
+ a.getDimension(
+ R.styleable.ContactListItemView_list_item_name_text_size,
+ (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size));
+ mVideoCallIconSize =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_video_call_icon_size, mVideoCallIconSize);
+ mVideoCallIconMargin =
+ a.getDimensionPixelOffset(
+ R.styleable.ContactListItemView_list_item_video_call_icon_margin,
+ mVideoCallIconMargin);
+
+ setPaddingRelative(
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0),
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0),
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0),
+ a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0));
+
+ a.recycle();
+ }
+
+ mTextHighlighter = new TextHighlighter(Typeface.BOLD);
+
+ if (R.styleable.Theme != null) {
+ a = getContext().obtainStyledAttributes(R.styleable.Theme);
+ mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary);
+ a.recycle();
+ }
+
+ mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
+
+ if (mActivatedBackgroundDrawable != null) {
+ mActivatedBackgroundDrawable.setCallback(this);
+ }
+
+ mNameHighlightSequence = new ArrayList<HighlightSequence>();
+ mNumberHighlightSequence = new ArrayList<HighlightSequence>();
+
+ setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
+ }
+
+ public static final PhotoPosition getDefaultPhotoPosition(boolean opposite) {
+ final Locale locale = Locale.getDefault();
+ final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
+ switch (layoutDirection) {
+ case View.LAYOUT_DIRECTION_RTL:
+ return (opposite ? PhotoPosition.LEFT : PhotoPosition.RIGHT);
+ case View.LAYOUT_DIRECTION_LTR:
+ default:
+ return (opposite ? PhotoPosition.RIGHT : PhotoPosition.LEFT);
+ }
+ }
+
+ /**
+ * Helper method for splitting a string into tokens. The lists passed in are populated with the
+ * tokens and offsets into the content of each token. The tokenization function parses e-mail
+ * addresses as a single token; otherwise it splits on any non-alphanumeric character.
+ *
+ * @param content Content to split.
+ * @return List of token strings.
+ */
+ private static List<String> split(String content) {
+ final Matcher matcher = SPLIT_PATTERN.matcher(content);
+ final ArrayList<String> tokens = new ArrayList<>();
+ while (matcher.find()) {
+ tokens.add(matcher.group());
+ }
+ return tokens;
+ }
+
+ public void setUnknownNameText(CharSequence unknownNameText) {
+ mUnknownNameText = unknownNameText;
+ }
+
+ public void setQuickContactEnabled(boolean flag) {
+ mQuickContactEnabled = flag;
+ }
+
+ /**
+ * Sets whether the video calling icon is shown. For the video calling icon to be shown, {@link
+ * #mSupportVideoCallIcon} must be {@code true}.
+ *
+ * @param showVideoCallIcon {@code true} if the video calling icon is shown, {@code false}
+ * otherwise.
+ * @param listener Listener to notify when the video calling icon is clicked.
+ * @param position The position in the adapater of the video calling icon.
+ */
+ public void setShowVideoCallIcon(
+ boolean showVideoCallIcon, PhoneNumberListAdapter.Listener listener, int position) {
+ mShowVideoCallIcon = showVideoCallIcon;
+ mPhoneNumberListAdapterListener = listener;
+ mPosition = position;
+
+ if (mShowVideoCallIcon) {
+ if (mVideoCallIcon == null) {
+ mVideoCallIcon = new ImageView(getContext());
+ addView(mVideoCallIcon);
+ }
+ mVideoCallIcon.setContentDescription(
+ getContext().getString(R.string.description_search_video_call));
+ mVideoCallIcon.setImageResource(R.drawable.ic_search_video_call);
+ mVideoCallIcon.setScaleType(ScaleType.CENTER);
+ mVideoCallIcon.setVisibility(View.VISIBLE);
+ mVideoCallIcon.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Inform the adapter that the video calling icon was clicked.
+ if (mPhoneNumberListAdapterListener != null) {
+ mPhoneNumberListAdapterListener.onVideoCallIconClicked(mPosition);
+ }
+ }
+ });
+ } else {
+ if (mVideoCallIcon != null) {
+ mVideoCallIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ /**
+ * Sets whether the view supports a video calling icon. This is independent of whether the view is
+ * actually showing an icon. Support for the video calling icon ensures that the layout leaves
+ * space for the video icon, should it be shown.
+ *
+ * @param supportVideoCallIcon {@code true} if the video call icon is supported, {@code false}
+ * otherwise.
+ */
+ public void setSupportVideoCallIcon(boolean supportVideoCallIcon) {
+ mSupportVideoCallIcon = supportVideoCallIcon;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // We will match parent's width and wrap content vertically, but make sure
+ // height is no less than listPreferredItemHeight.
+ final int specWidth = resolveSize(0, widthMeasureSpec);
+ final int preferredHeight = mPreferredHeight;
+
+ mNameTextViewHeight = 0;
+ mPhoneticNameTextViewHeight = 0;
+ mLabelViewHeight = 0;
+ mDataViewHeight = 0;
+ mLabelAndDataViewMaxHeight = 0;
+ mSnippetTextViewHeight = 0;
+ mStatusTextViewHeight = 0;
+ mCheckBoxWidth = 0;
+
+ ensurePhotoViewSize();
+
+ // Width each TextView is able to use.
+ int effectiveWidth;
+ // All the other Views will honor the photo, so available width for them may be shrunk.
+ if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) {
+ effectiveWidth =
+ specWidth
+ - getPaddingLeft()
+ - getPaddingRight()
+ - (mPhotoViewWidth + mGapBetweenImageAndText);
+ } else {
+ effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight();
+ }
+
+ if (mIsSectionHeaderEnabled) {
+ effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText;
+ }
+
+ if (mSupportVideoCallIcon) {
+ effectiveWidth -= (mVideoCallIconSize + mVideoCallIconMargin);
+ }
+
+ // Go over all visible text views and measure actual width of each of them.
+ // Also calculate their heights to get the total height for this entire view.
+
+ if (isVisible(mNameTextView)) {
+ // Calculate width for name text - this parallels similar measurement in onLayout.
+ int nameTextWidth = effectiveWidth;
+ if (mPhotoPosition != PhotoPosition.LEFT) {
+ nameTextWidth -= mTextIndent;
+ }
+ mNameTextView.measure(
+ MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mNameTextViewHeight = mNameTextView.getMeasuredHeight();
+ }
+
+ // If both data (phone number/email address) and label (type like "MOBILE") are quite long,
+ // we should ellipsize both using appropriate ratio.
+ final int dataWidth;
+ final int labelWidth;
+ if (isVisible(mDataView)) {
+ if (isVisible(mLabelView)) {
+ final int totalWidth = effectiveWidth - mGapBetweenLabelAndData;
+ dataWidth =
+ ((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
+ labelWidth =
+ ((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight));
+ } else {
+ dataWidth = effectiveWidth;
+ labelWidth = 0;
+ }
+ } else {
+ dataWidth = 0;
+ if (isVisible(mLabelView)) {
+ labelWidth = effectiveWidth;
+ } else {
+ labelWidth = 0;
+ }
+ }
+
+ if (isVisible(mDataView)) {
+ mDataView.measure(
+ MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mDataViewHeight = mDataView.getMeasuredHeight();
+ }
+
+ if (isVisible(mLabelView)) {
+ mLabelView.measure(
+ MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mLabelViewHeight = mLabelView.getMeasuredHeight();
+ }
+ mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight);
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.measure(
+ MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mSnippetTextViewHeight = mSnippetView.getMeasuredHeight();
+ }
+
+ // Status view height is the biggest of the text view and the presence icon
+ if (isVisible(mPresenceIcon)) {
+ mPresenceIcon.measure(
+ MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY));
+ mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight();
+ }
+
+ if (mSupportVideoCallIcon && isVisible(mVideoCallIcon)) {
+ mVideoCallIcon.measure(
+ MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(mVideoCallIconSize, MeasureSpec.EXACTLY));
+ }
+
+ if (isVisible(mWorkProfileIcon)) {
+ mWorkProfileIcon.measure(
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight());
+ }
+
+ if (isVisible(mStatusView)) {
+ // Presence and status are in a same row, so status will be affected by icon size.
+ final int statusWidth;
+ if (isVisible(mPresenceIcon)) {
+ statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin);
+ } else {
+ statusWidth = effectiveWidth;
+ }
+ mStatusView.measure(
+ MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight());
+ }
+
+ // Calculate height including padding.
+ int height =
+ (mNameTextViewHeight
+ + mPhoneticNameTextViewHeight
+ + mLabelAndDataViewMaxHeight
+ + mSnippetTextViewHeight
+ + mStatusTextViewHeight);
+
+ // Make sure the height is at least as high as the photo
+ height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop());
+
+ // Make sure height is at least the preferred height
+ height = Math.max(height, preferredHeight);
+
+ // Measure the header if it is visible.
+ if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) {
+ mHeaderTextView.measure(
+ MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY),
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+ }
+
+ setMeasuredDimension(specWidth, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int height = bottom - top;
+ final int width = right - left;
+
+ // Determine the vertical bounds by laying out the header first.
+ int topBound = 0;
+ int bottomBound = height;
+ int leftBound = getPaddingLeft();
+ int rightBound = width - getPaddingRight();
+
+ final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this);
+
+ // Put the section header on the left side of the contact view.
+ if (mIsSectionHeaderEnabled) {
+ // Align the text view all the way left, to be consistent with Contacts.
+ if (isLayoutRtl) {
+ rightBound = width;
+ } else {
+ leftBound = 0;
+ }
+ if (mHeaderTextView != null) {
+ int headerHeight = mHeaderTextView.getMeasuredHeight();
+ int headerTopBound = (bottomBound + topBound - headerHeight) / 2 + mTextOffsetTop;
+
+ mHeaderTextView.layout(
+ isLayoutRtl ? rightBound - mHeaderWidth : leftBound,
+ headerTopBound,
+ isLayoutRtl ? rightBound : leftBound + mHeaderWidth,
+ headerTopBound + headerHeight);
+ }
+ if (isLayoutRtl) {
+ rightBound -= mHeaderWidth;
+ } else {
+ leftBound += mHeaderWidth;
+ }
+ }
+
+ mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, bottomBound);
+ mLeftOffset = left + leftBound;
+ mRightOffset = left + rightBound;
+ if (mIsSectionHeaderEnabled) {
+ if (isLayoutRtl) {
+ rightBound -= mGapBetweenImageAndText;
+ } else {
+ leftBound += mGapBetweenImageAndText;
+ }
+ }
+
+ if (mActivatedStateSupported && isActivated()) {
+ mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader);
+ }
+
+ final View photoView = mQuickContact != null ? mQuickContact : mPhotoView;
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ // Photo is the left most view. All the other Views should on the right of the photo.
+ if (photoView != null) {
+ // Center the photo vertically
+ final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight);
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ } else if (mKeepHorizontalPaddingForPhotoView) {
+ // Draw nothing but keep the padding.
+ leftBound += mPhotoViewWidth + mGapBetweenImageAndText;
+ }
+ } else {
+ // Photo is the right most view. Right bound should be adjusted that way.
+ if (photoView != null) {
+ // Center the photo vertically
+ final int photoTop = topBound + (bottomBound - topBound - mPhotoViewHeight) / 2;
+ photoView.layout(
+ rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight);
+ rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+ } else if (mKeepHorizontalPaddingForPhotoView) {
+ // Draw nothing but keep the padding.
+ rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText);
+ }
+
+ // Add indent between left-most padding and texts.
+ leftBound += mTextIndent;
+ }
+
+ if (mSupportVideoCallIcon) {
+ // Place the video call button at the end of the list (e.g. take into account RTL mode).
+ if (isVisible(mVideoCallIcon)) {
+ // Center the video icon vertically
+ final int videoIconTop = topBound + (bottomBound - topBound - mVideoCallIconSize) / 2;
+
+ if (!isLayoutRtl) {
+ // When photo is on left, video icon is placed on the right edge.
+ mVideoCallIcon.layout(
+ rightBound - mVideoCallIconSize,
+ videoIconTop,
+ rightBound,
+ videoIconTop + mVideoCallIconSize);
+ } else {
+ // When photo is on right, video icon is placed on the left edge.
+ mVideoCallIcon.layout(
+ leftBound,
+ videoIconTop,
+ leftBound + mVideoCallIconSize,
+ videoIconTop + mVideoCallIconSize);
+ }
+ }
+
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ rightBound -= (mVideoCallIconSize + mVideoCallIconMargin);
+ } else {
+ leftBound += mVideoCallIconSize + mVideoCallIconMargin;
+ }
+ }
+
+ // Center text vertically, then apply the top offset.
+ final int totalTextHeight =
+ mNameTextViewHeight
+ + mPhoneticNameTextViewHeight
+ + mLabelAndDataViewMaxHeight
+ + mSnippetTextViewHeight
+ + mStatusTextViewHeight;
+ int textTopBound = (bottomBound + topBound - totalTextHeight) / 2 + mTextOffsetTop;
+
+ // Work Profile icon align top
+ int workProfileIconWidth = 0;
+ if (isVisible(mWorkProfileIcon)) {
+ workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth();
+ final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0;
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ // When photo is on left, label is placed on the right edge of the list item.
+ mWorkProfileIcon.layout(
+ rightBound - workProfileIconWidth - distanceFromEnd,
+ textTopBound,
+ rightBound - distanceFromEnd,
+ textTopBound + mNameTextViewHeight);
+ } else {
+ // When photo is on right, label is placed on the left of data view.
+ mWorkProfileIcon.layout(
+ leftBound + distanceFromEnd,
+ textTopBound,
+ leftBound + workProfileIconWidth + distanceFromEnd,
+ textTopBound + mNameTextViewHeight);
+ }
+ }
+
+ // Layout all text view and presence icon
+ // Put name TextView first
+ if (isVisible(mNameTextView)) {
+ final int distanceFromEnd =
+ workProfileIconWidth
+ + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0);
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ mNameTextView.layout(
+ leftBound,
+ textTopBound,
+ rightBound - distanceFromEnd,
+ textTopBound + mNameTextViewHeight);
+ } else {
+ mNameTextView.layout(
+ leftBound + distanceFromEnd,
+ textTopBound,
+ rightBound,
+ textTopBound + mNameTextViewHeight);
+ }
+ }
+
+ if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) {
+ textTopBound += mNameTextViewHeight;
+ }
+
+ // Presence and status
+ if (isLayoutRtl) {
+ int statusRightBound = rightBound;
+ if (isVisible(mPresenceIcon)) {
+ int iconWidth = mPresenceIcon.getMeasuredWidth();
+ mPresenceIcon.layout(
+ rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
+ statusRightBound -= (iconWidth + mPresenceIconMargin);
+ }
+
+ if (isVisible(mStatusView)) {
+ mStatusView.layout(
+ leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight);
+ }
+ } else {
+ int statusLeftBound = leftBound;
+ if (isVisible(mPresenceIcon)) {
+ int iconWidth = mPresenceIcon.getMeasuredWidth();
+ mPresenceIcon.layout(
+ leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight);
+ statusLeftBound += (iconWidth + mPresenceIconMargin);
+ }
+
+ if (isVisible(mStatusView)) {
+ mStatusView.layout(
+ statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight);
+ }
+ }
+
+ if (isVisible(mStatusView) || isVisible(mPresenceIcon)) {
+ textTopBound += mStatusTextViewHeight;
+ }
+
+ // Rest of text views
+ int dataLeftBound = leftBound;
+
+ // Label and Data align bottom.
+ if (isVisible(mLabelView)) {
+ if (!isLayoutRtl) {
+ mLabelView.layout(
+ dataLeftBound,
+ textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData;
+ } else {
+ dataLeftBound = leftBound + mLabelView.getMeasuredWidth();
+ mLabelView.layout(
+ rightBound - mLabelView.getMeasuredWidth(),
+ textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData);
+ }
+ }
+
+ if (isVisible(mDataView)) {
+ if (!isLayoutRtl) {
+ mDataView.layout(
+ dataLeftBound,
+ textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ } else {
+ mDataView.layout(
+ rightBound - mDataView.getMeasuredWidth(),
+ textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight,
+ rightBound,
+ textTopBound + mLabelAndDataViewMaxHeight);
+ }
+ }
+ if (isVisible(mLabelView) || isVisible(mDataView)) {
+ textTopBound += mLabelAndDataViewMaxHeight;
+ }
+
+ if (isVisible(mSnippetView)) {
+ mSnippetView.layout(
+ leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight);
+ }
+ }
+
+ @Override
+ public void adjustListItemSelectionBounds(Rect bounds) {
+ if (mAdjustSelectionBoundsEnabled) {
+ bounds.top += mBoundsWithoutHeader.top;
+ bounds.bottom = bounds.top + mBoundsWithoutHeader.height();
+ bounds.left = mBoundsWithoutHeader.left;
+ bounds.right = mBoundsWithoutHeader.right;
+ }
+ }
+
+ protected boolean isVisible(View view) {
+ return view != null && view.getVisibility() == View.VISIBLE;
+ }
+
+ /** Extracts width and height from the style */
+ private void ensurePhotoViewSize() {
+ if (!mPhotoViewWidthAndHeightAreReady) {
+ mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize();
+ if (!mQuickContactEnabled && mPhotoView == null) {
+ if (!mKeepHorizontalPaddingForPhotoView) {
+ mPhotoViewWidth = 0;
+ }
+ if (!mKeepVerticalPaddingForPhotoView) {
+ mPhotoViewHeight = 0;
+ }
+ }
+
+ mPhotoViewWidthAndHeightAreReady = true;
+ }
+ }
+
+ protected int getDefaultPhotoViewSize() {
+ return mDefaultPhotoViewSize;
+ }
+
+ /**
+ * Gets a LayoutParam that corresponds to the default photo size.
+ *
+ * @return A new LayoutParam.
+ */
+ private LayoutParams getDefaultPhotoLayoutParams() {
+ LayoutParams params = generateDefaultLayoutParams();
+ params.width = getDefaultPhotoViewSize();
+ params.height = params.width;
+ return params;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mActivatedStateSupported) {
+ mActivatedBackgroundDrawable.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mActivatedBackgroundDrawable || super.verifyDrawable(who);
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mActivatedStateSupported) {
+ mActivatedBackgroundDrawable.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ public void dispatchDraw(Canvas canvas) {
+ if (mActivatedStateSupported && isActivated()) {
+ mActivatedBackgroundDrawable.draw(canvas);
+ }
+
+ super.dispatchDraw(canvas);
+ }
+
+ /** Sets section header or makes it invisible if the title is null. */
+ public void setSectionHeader(String title) {
+ if (!TextUtils.isEmpty(title)) {
+ if (mHeaderTextView == null) {
+ mHeaderTextView = new TextView(getContext());
+ mHeaderTextView.setTextAppearance(getContext(), R.style.SectionHeaderStyle);
+ mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
+ addView(mHeaderTextView);
+ }
+ setMarqueeText(mHeaderTextView, title);
+ mHeaderTextView.setVisibility(View.VISIBLE);
+ mHeaderTextView.setAllCaps(true);
+ } else if (mHeaderTextView != null) {
+ mHeaderTextView.setVisibility(View.GONE);
+ }
+ }
+
+ public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) {
+ mIsSectionHeaderEnabled = isSectionHeaderEnabled;
+ }
+
+ /** Returns the quick contact badge, creating it if necessary. */
+ public QuickContactBadge getQuickContact() {
+ if (!mQuickContactEnabled) {
+ throw new IllegalStateException("QuickContact is disabled for this view");
+ }
+ if (mQuickContact == null) {
+ mQuickContact = new QuickContactBadge(getContext());
+ if (CompatUtils.isLollipopCompatible()) {
+ mQuickContact.setOverlay(null);
+ }
+ mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams());
+ if (mNameTextView != null) {
+ mQuickContact.setContentDescription(
+ getContext()
+ .getString(R.string.description_quick_contact_for, mNameTextView.getText()));
+ }
+
+ addView(mQuickContact);
+ mPhotoViewWidthAndHeightAreReady = false;
+ }
+ return mQuickContact;
+ }
+
+ /** Returns the photo view, creating it if necessary. */
+ public ImageView getPhotoView() {
+ if (mPhotoView == null) {
+ mPhotoView = new ImageView(getContext());
+ mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams());
+ // Quick contact style used above will set a background - remove it
+ mPhotoView.setBackground(null);
+ addView(mPhotoView);
+ mPhotoViewWidthAndHeightAreReady = false;
+ }
+ return mPhotoView;
+ }
+
+ /** Removes the photo view. */
+ public void removePhotoView() {
+ removePhotoView(false, true);
+ }
+
+ /**
+ * Removes the photo view.
+ *
+ * @param keepHorizontalPadding True means data on the right side will have padding on left,
+ * pretending there is still a photo view.
+ * @param keepVerticalPadding True means the View will have some height enough for accommodating a
+ * photo view.
+ */
+ public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) {
+ mPhotoViewWidthAndHeightAreReady = false;
+ mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding;
+ mKeepVerticalPaddingForPhotoView = keepVerticalPadding;
+ if (mPhotoView != null) {
+ removeView(mPhotoView);
+ mPhotoView = null;
+ }
+ if (mQuickContact != null) {
+ removeView(mQuickContact);
+ mQuickContact = null;
+ }
+ }
+
+ /**
+ * Sets a word prefix that will be highlighted if encountered in fields like name and search
+ * snippet. This will disable the mask highlighting for names.
+ *
+ * <p>NOTE: must be all upper-case
+ */
+ public void setHighlightedPrefix(String upperCasePrefix) {
+ mHighlightedPrefix = upperCasePrefix;
+ }
+
+ /** Clears previously set highlight sequences for the view. */
+ public void clearHighlightSequences() {
+ mNameHighlightSequence.clear();
+ mNumberHighlightSequence.clear();
+ mHighlightedPrefix = null;
+ }
+
+ /**
+ * Adds a highlight sequence to the name highlighter.
+ *
+ * @param start The start position of the highlight sequence.
+ * @param end The end position of the highlight sequence.
+ */
+ public void addNameHighlightSequence(int start, int end) {
+ mNameHighlightSequence.add(new HighlightSequence(start, end));
+ }
+
+ /**
+ * Adds a highlight sequence to the number highlighter.
+ *
+ * @param start The start position of the highlight sequence.
+ * @param end The end position of the highlight sequence.
+ */
+ public void addNumberHighlightSequence(int start, int end) {
+ mNumberHighlightSequence.add(new HighlightSequence(start, end));
+ }
+
+ /** Returns the text view for the contact name, creating it if necessary. */
+ public TextView getNameTextView() {
+ if (mNameTextView == null) {
+ mNameTextView = new TextView(getContext());
+ mNameTextView.setSingleLine(true);
+ mNameTextView.setEllipsize(getTextEllipsis());
+ mNameTextView.setTextColor(mNameTextViewTextColor);
+ mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize);
+ // Manually call setActivated() since this view may be added after the first
+ // setActivated() call toward this whole item view.
+ mNameTextView.setActivated(isActivated());
+ mNameTextView.setGravity(Gravity.CENTER_VERTICAL);
+ mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ mNameTextView.setId(R.id.cliv_name_textview);
+ if (CompatUtils.isLollipopCompatible()) {
+ mNameTextView.setElegantTextHeight(false);
+ }
+ addView(mNameTextView);
+ }
+ return mNameTextView;
+ }
+
+ /** Adds or updates a text view for the data label. */
+ public void setLabel(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mLabelView != null) {
+ mLabelView.setVisibility(View.GONE);
+ }
+ } else {
+ getLabelView();
+ setMarqueeText(mLabelView, text);
+ mLabelView.setVisibility(VISIBLE);
+ }
+ }
+
+ /** Returns the text view for the data label, creating it if necessary. */
+ public TextView getLabelView() {
+ if (mLabelView == null) {
+ mLabelView = new TextView(getContext());
+ mLabelView.setLayoutParams(
+ new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+
+ mLabelView.setSingleLine(true);
+ mLabelView.setEllipsize(getTextEllipsis());
+ mLabelView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
+ if (mPhotoPosition == PhotoPosition.LEFT) {
+ mLabelView.setAllCaps(true);
+ } else {
+ mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD);
+ }
+ mLabelView.setActivated(isActivated());
+ mLabelView.setId(R.id.cliv_label_textview);
+ addView(mLabelView);
+ }
+ return mLabelView;
+ }
+
+ /**
+ * Sets phone number for a list item. This takes care of number highlighting if the highlight mask
+ * exists.
+ */
+ public void setPhoneNumber(String text) {
+ if (text == null) {
+ if (mDataView != null) {
+ mDataView.setVisibility(View.GONE);
+ }
+ } else {
+ getDataView();
+
+ // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to
+ // mDataView. Make sure that determination of the highlight sequences are done only
+ // after number formatting.
+
+ // Sets phone number texts for display after highlighting it, if applicable.
+ // CharSequence textToSet = text;
+ final SpannableString textToSet = new SpannableString(text);
+
+ if (mNumberHighlightSequence.size() != 0) {
+ final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0);
+ mTextHighlighter.applyMaskingHighlight(
+ textToSet, highlightSequence.start, highlightSequence.end);
+ }
+
+ setMarqueeText(mDataView, textToSet);
+ mDataView.setVisibility(VISIBLE);
+
+ // We have a phone number as "mDataView" so make it always LTR and VIEW_START
+ mDataView.setTextDirection(View.TEXT_DIRECTION_LTR);
+ mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ }
+ }
+
+ private void setMarqueeText(TextView textView, CharSequence text) {
+ if (getTextEllipsis() == TruncateAt.MARQUEE) {
+ // To show MARQUEE correctly (with END effect during non-active state), we need
+ // to build Spanned with MARQUEE in addition to TextView's ellipsize setting.
+ final SpannableString spannable = new SpannableString(text);
+ spannable.setSpan(
+ TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ textView.setText(spannable);
+ } else {
+ textView.setText(text);
+ }
+ }
+
+ /** Returns the text view for the data text, creating it if necessary. */
+ public TextView getDataView() {
+ if (mDataView == null) {
+ mDataView = new TextView(getContext());
+ mDataView.setSingleLine(true);
+ mDataView.setEllipsize(getTextEllipsis());
+ mDataView.setTextAppearance(getContext(), R.style.TextAppearanceSmall);
+ mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ mDataView.setActivated(isActivated());
+ mDataView.setId(R.id.cliv_data_view);
+ if (CompatUtils.isLollipopCompatible()) {
+ mDataView.setElegantTextHeight(false);
+ }
+ addView(mDataView);
+ }
+ return mDataView;
+ }
+
+ /** Adds or updates a text view for the search snippet. */
+ public void setSnippet(String text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mSnippetView != null) {
+ mSnippetView.setVisibility(View.GONE);
+ }
+ } else {
+ mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix);
+ mSnippetView.setVisibility(VISIBLE);
+ if (ContactDisplayUtils.isPossiblePhoneNumber(text)) {
+ // Give the text-to-speech engine a hint that it's a phone number
+ mSnippetView.setContentDescription(PhoneNumberUtilsCompat.createTtsSpannable(text));
+ } else {
+ mSnippetView.setContentDescription(null);
+ }
+ }
+ }
+
+ /** Returns the text view for the search snippet, creating it if necessary. */
+ public TextView getSnippetView() {
+ if (mSnippetView == null) {
+ mSnippetView = new TextView(getContext());
+ mSnippetView.setSingleLine(true);
+ mSnippetView.setEllipsize(getTextEllipsis());
+ mSnippetView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+ mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ mSnippetView.setActivated(isActivated());
+ addView(mSnippetView);
+ }
+ return mSnippetView;
+ }
+
+ /** Returns the text view for the status, creating it if necessary. */
+ public TextView getStatusView() {
+ if (mStatusView == null) {
+ mStatusView = new TextView(getContext());
+ mStatusView.setSingleLine(true);
+ mStatusView.setEllipsize(getTextEllipsis());
+ mStatusView.setTextAppearance(getContext(), android.R.style.TextAppearance_Small);
+ mStatusView.setTextColor(mSecondaryTextColor);
+ mStatusView.setActivated(isActivated());
+ mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
+ addView(mStatusView);
+ }
+ return mStatusView;
+ }
+
+ /** Adds or updates a text view for the status. */
+ public void setStatus(CharSequence text) {
+ if (TextUtils.isEmpty(text)) {
+ if (mStatusView != null) {
+ mStatusView.setVisibility(View.GONE);
+ }
+ } else {
+ getStatusView();
+ setMarqueeText(mStatusView, text);
+ mStatusView.setVisibility(VISIBLE);
+ }
+ }
+
+ /** Adds or updates the presence icon view. */
+ public void setPresence(Drawable icon) {
+ if (icon != null) {
+ if (mPresenceIcon == null) {
+ mPresenceIcon = new ImageView(getContext());
+ addView(mPresenceIcon);
+ }
+ mPresenceIcon.setImageDrawable(icon);
+ mPresenceIcon.setScaleType(ScaleType.CENTER);
+ mPresenceIcon.setVisibility(View.VISIBLE);
+ } else {
+ if (mPresenceIcon != null) {
+ mPresenceIcon.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ /**
+ * Set to display work profile icon or not
+ *
+ * @param enabled set to display work profile icon or not
+ */
+ public void setWorkProfileIconEnabled(boolean enabled) {
+ if (mWorkProfileIcon != null) {
+ mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE);
+ } else if (enabled) {
+ mWorkProfileIcon = new ImageView(getContext());
+ addView(mWorkProfileIcon);
+ mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile);
+ mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE);
+ mWorkProfileIcon.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private TruncateAt getTextEllipsis() {
+ return TruncateAt.MARQUEE;
+ }
+
+ public void showDisplayName(Cursor cursor, int nameColumnIndex) {
+ CharSequence name = cursor.getString(nameColumnIndex);
+ setDisplayName(name);
+
+ // Since the quick contact content description is derived from the display name and there is
+ // no guarantee that when the quick contact is initialized the display name is already set,
+ // do it here too.
+ if (mQuickContact != null) {
+ mQuickContact.setContentDescription(
+ getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText()));
+ }
+ }
+
+ public void setDisplayName(CharSequence name) {
+ if (!TextUtils.isEmpty(name)) {
+ // Chooses the available highlighting method for highlighting.
+ if (mHighlightedPrefix != null) {
+ name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix);
+ } else if (mNameHighlightSequence.size() != 0) {
+ final SpannableString spannableName = new SpannableString(name);
+ for (HighlightSequence highlightSequence : mNameHighlightSequence) {
+ mTextHighlighter.applyMaskingHighlight(
+ spannableName, highlightSequence.start, highlightSequence.end);
+ }
+ name = spannableName;
+ }
+ } else {
+ name = mUnknownNameText;
+ }
+ setMarqueeText(getNameTextView(), name);
+
+ if (ContactDisplayUtils.isPossiblePhoneNumber(name)) {
+ // Give the text-to-speech engine a hint that it's a phone number
+ mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR);
+ mNameTextView.setContentDescription(
+ PhoneNumberUtilsCompat.createTtsSpannable(name.toString()));
+ } else {
+ // Remove span tags of highlighting for talkback to avoid reading highlighting and rest
+ // of the name into two separate parts.
+ mNameTextView.setContentDescription(name.toString());
+ }
+ }
+
+ public void hideDisplayName() {
+ if (mNameTextView != null) {
+ removeView(mNameTextView);
+ mNameTextView = null;
+ }
+ }
+
+ /** Sets the proper icon (star or presence or nothing) and/or status message. */
+ public void showPresenceAndStatusMessage(
+ Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) {
+ Drawable icon = null;
+ int presence = 0;
+ if (!cursor.isNull(presenceColumnIndex)) {
+ presence = cursor.getInt(presenceColumnIndex);
+ icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence);
+ }
+ setPresence(icon);
+
+ String statusMessage = null;
+ if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) {
+ statusMessage = cursor.getString(contactStatusColumnIndex);
+ }
+ // If there is no status message from the contact, but there was a presence value, then use
+ // the default status message string
+ if (statusMessage == null && presence != 0) {
+ statusMessage = ContactStatusUtil.getStatusString(getContext(), presence);
+ }
+ setStatus(statusMessage);
+ }
+
+ /** Shows search snippet. */
+ public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) {
+ if (cursor.getColumnCount() <= summarySnippetColumnIndex
+ || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) {
+ setSnippet(null);
+ return;
+ }
+
+ String snippet = cursor.getString(summarySnippetColumnIndex);
+
+ // Do client side snippeting if provider didn't do it
+ final Bundle extras = cursor.getExtras();
+ if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) {
+
+ final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY);
+
+ String displayName = null;
+ int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME);
+ if (displayNameIndex >= 0) {
+ displayName = cursor.getString(displayNameIndex);
+ }
+
+ snippet = updateSnippet(snippet, query, displayName);
+
+ } else {
+ if (snippet != null) {
+ int from = 0;
+ int to = snippet.length();
+ int start = snippet.indexOf(SNIPPET_START_MATCH);
+ if (start == -1) {
+ snippet = null;
+ } else {
+ int firstNl = snippet.lastIndexOf('\n', start);
+ if (firstNl != -1) {
+ from = firstNl + 1;
+ }
+ int end = snippet.lastIndexOf(SNIPPET_END_MATCH);
+ if (end != -1) {
+ int lastNl = snippet.indexOf('\n', end);
+ if (lastNl != -1) {
+ to = lastNl;
+ }
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = from; i < to; i++) {
+ char c = snippet.charAt(i);
+ if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) {
+ sb.append(c);
+ }
+ }
+ snippet = sb.toString();
+ }
+ }
+ }
+
+ setSnippet(snippet);
+ }
+
+ /**
+ * Used for deferred snippets from the database. The contents come back as large strings which
+ * need to be extracted for display.
+ *
+ * @param snippet The snippet from the database.
+ * @param query The search query substring.
+ * @param displayName The contact display name.
+ * @return The proper snippet to display.
+ */
+ private String updateSnippet(String snippet, String query, String displayName) {
+
+ if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) {
+ return null;
+ }
+ query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase());
+
+ // If the display name already contains the query term, return empty - snippets should
+ // not be needed in that case.
+ if (!TextUtils.isEmpty(displayName)) {
+ final String lowerDisplayName = displayName.toLowerCase();
+ final List<String> nameTokens = split(lowerDisplayName);
+ for (String nameToken : nameTokens) {
+ if (nameToken.startsWith(query)) {
+ return null;
+ }
+ }
+ }
+
+ // The snippet may contain multiple data lines.
+ // Show the first line that matches the query.
+ final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query);
+
+ if (matched != null && matched.line != null) {
+ // Tokenize for long strings since the match may be at the end of it.
+ // Skip this part for short strings since the whole string will be displayed.
+ // Most contact strings are short so the snippetize method will be called infrequently.
+ final int lengthThreshold =
+ getResources().getInteger(R.integer.snippet_length_before_tokenize);
+ if (matched.line.length() > lengthThreshold) {
+ return snippetize(matched.line, matched.startIndex, lengthThreshold);
+ } else {
+ return matched.line;
+ }
+ }
+
+ // No match found.
+ return null;
+ }
+
+ private String snippetize(String line, int matchIndex, int maxLength) {
+ // Show up to maxLength characters. But we only show full tokens so show the last full token
+ // up to maxLength characters. So as many starting tokens as possible before trying ending
+ // tokens.
+ int remainingLength = maxLength;
+ int tempRemainingLength = remainingLength;
+
+ // Start the end token after the matched query.
+ int index = matchIndex;
+ int endTokenIndex = index;
+
+ // Find the match token first.
+ while (index < line.length()) {
+ if (!Character.isLetterOrDigit(line.charAt(index))) {
+ endTokenIndex = index;
+ remainingLength = tempRemainingLength;
+ break;
+ }
+ tempRemainingLength--;
+ index++;
+ }
+
+ // Find as much content before the match.
+ index = matchIndex - 1;
+ tempRemainingLength = remainingLength;
+ int startTokenIndex = matchIndex;
+ while (index > -1 && tempRemainingLength > 0) {
+ if (!Character.isLetterOrDigit(line.charAt(index))) {
+ startTokenIndex = index;
+ remainingLength = tempRemainingLength;
+ }
+ tempRemainingLength--;
+ index--;
+ }
+
+ index = endTokenIndex;
+ tempRemainingLength = remainingLength;
+ // Find remaining content at after match.
+ while (index < line.length() && tempRemainingLength > 0) {
+ if (!Character.isLetterOrDigit(line.charAt(index))) {
+ endTokenIndex = index;
+ }
+ tempRemainingLength--;
+ index++;
+ }
+ // Append ellipse if there is content before or after.
+ final StringBuilder sb = new StringBuilder();
+ if (startTokenIndex > 0) {
+ sb.append("...");
+ }
+ sb.append(line.substring(startTokenIndex, endTokenIndex));
+ if (endTokenIndex < line.length()) {
+ sb.append("...");
+ }
+ return sb.toString();
+ }
+
+ public void setActivatedStateSupported(boolean flag) {
+ this.mActivatedStateSupported = flag;
+ }
+
+ public void setAdjustSelectionBoundsEnabled(boolean enabled) {
+ mAdjustSelectionBoundsEnabled = enabled;
+ }
+
+ @Override
+ public void requestLayout() {
+ // We will assume that once measured this will not need to resize
+ // itself, so there is no need to pass the layout request to the parent
+ // view (ListView).
+ forceLayout();
+ }
+
+ public void setPhotoPosition(PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+ }
+
+ /**
+ * Set drawable resources directly for the drawable resource of the photo view.
+ *
+ * @param drawableId Id of drawable resource.
+ */
+ public void setDrawableResource(int drawableId) {
+ ImageView photo = getPhotoView();
+ photo.setScaleType(ImageView.ScaleType.CENTER);
+ final Drawable drawable = ContextCompat.getDrawable(getContext(), drawableId);
+ final int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color);
+ if (CompatUtils.isLollipopCompatible()) {
+ photo.setImageDrawable(drawable);
+ photo.setImageTintList(ColorStateList.valueOf(iconColor));
+ } else {
+ final Drawable drawableWrapper = DrawableCompat.wrap(drawable).mutate();
+ DrawableCompat.setTint(drawableWrapper, iconColor);
+ photo.setImageDrawable(drawableWrapper);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final float x = event.getX();
+ final float y = event.getY();
+ // If the touch event's coordinates are not within the view's header, then delegate
+ // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume
+ // and ignore the touch event.
+ if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) {
+ return super.onTouchEvent(event);
+ } else {
+ return true;
+ }
+ }
+
+ private final boolean pointIsInView(float localX, float localY) {
+ return localX >= mLeftOffset
+ && localX < mRightOffset
+ && localY >= 0
+ && localY < (getBottom() - getTop());
+ }
+
+ /**
+ * Where to put contact photo. This affects the other Views' layout or look-and-feel.
+ *
+ * <p>TODO: replace enum with int constants
+ */
+ public enum PhotoPosition {
+ LEFT,
+ RIGHT
+ }
+
+ protected static class HighlightSequence {
+
+ private final int start;
+ private final int end;
+
+ HighlightSequence(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java
new file mode 100644
index 000000000..1f3e2bfe3
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout.LayoutParams;
+import android.widget.TextView;
+import com.android.contacts.common.R;
+
+/** A custom view for the pinned section header shown at the top of the contact list. */
+public class ContactListPinnedHeaderView extends TextView {
+
+ public ContactListPinnedHeaderView(Context context, AttributeSet attrs, View parent) {
+ super(context, attrs);
+
+ if (R.styleable.ContactListItemView == null) {
+ return;
+ }
+ TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView);
+ int backgroundColor =
+ a.getColor(R.styleable.ContactListItemView_list_item_background_color, Color.WHITE);
+ int textOffsetTop =
+ a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_text_offset_top, 0);
+ int paddingStartOffset =
+ a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_padding_left, 0);
+ int textWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width);
+ int widthIncludingPadding = paddingStartOffset + textWidth;
+ a.recycle();
+
+ setBackgroundColor(backgroundColor);
+ setTextAppearance(getContext(), R.style.SectionHeaderStyle);
+ setLayoutParams(new LayoutParams(textWidth, LayoutParams.WRAP_CONTENT));
+ setLayoutDirection(parent.getLayoutDirection());
+ setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL);
+
+ // Apply text top offset. Multiply by two, because we are implementing this by padding for a
+ // vertically centered view, rather than adjusting the position directly via a layout.
+ setPaddingRelative(
+ 0, getPaddingTop() + (textOffsetTop * 2), getPaddingEnd(), getPaddingBottom());
+ }
+
+ /** Sets section header or makes it invisible if the title is null. */
+ public void setSectionHeaderTitle(String title) {
+ if (!TextUtils.isEmpty(title)) {
+ setText(title);
+ } else {
+ setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactTileView.java b/java/com/android/contacts/common/list/ContactTileView.java
new file mode 100644
index 000000000..9273b0583
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactTileView.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.R;
+
+/** A ContactTile displays a contact's picture and name */
+public abstract class ContactTileView extends FrameLayout {
+
+ private static final String TAG = ContactTileView.class.getSimpleName();
+ protected Listener mListener;
+ private Uri mLookupUri;
+ private ImageView mPhoto;
+ private TextView mName;
+ private ContactPhotoManager mPhotoManager = null;
+
+ public ContactTileView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mName = (TextView) findViewById(R.id.contact_tile_name);
+ mPhoto = (ImageView) findViewById(R.id.contact_tile_image);
+
+ OnClickListener listener = createClickListener();
+ setOnClickListener(listener);
+ }
+
+ protected OnClickListener createClickListener() {
+ return new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mListener == null) {
+ return;
+ }
+ mListener.onContactSelected(
+ getLookupUri(), MoreContactUtils.getTargetRectFromView(ContactTileView.this));
+ }
+ };
+ }
+
+ public void setPhotoManager(ContactPhotoManager photoManager) {
+ mPhotoManager = photoManager;
+ }
+
+ /**
+ * Populates the data members to be displayed from the fields in {@link
+ * com.android.contacts.common.list.ContactEntry}
+ */
+ public void loadFromContact(ContactEntry entry) {
+
+ if (entry != null) {
+ mName.setText(getNameForView(entry));
+ mLookupUri = entry.lookupUri;
+
+ setVisibility(View.VISIBLE);
+
+ if (mPhotoManager != null) {
+ DefaultImageRequest request = getDefaultImageRequest(entry.namePrimary, entry.lookupKey);
+ configureViewForImage(entry.photoUri == null);
+ if (mPhoto != null) {
+ mPhotoManager.loadPhoto(
+ mPhoto,
+ entry.photoUri,
+ getApproximateImageSize(),
+ isDarkTheme(),
+ isContactPhotoCircular(),
+ request);
+
+
+ }
+ } else {
+ Log.w(TAG, "contactPhotoManager not set");
+ }
+ } else {
+ setVisibility(View.INVISIBLE);
+ }
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ /**
+ * Returns the string that should actually be displayed as the contact's name. Subclasses can
+ * override this to return formatted versions of the name - i.e. first name only.
+ */
+ protected String getNameForView(ContactEntry contactEntry) {
+ return contactEntry.namePrimary;
+ }
+
+ /**
+ * Implemented by subclasses to estimate the size of the picture. This can return -1 if only a
+ * thumbnail is shown anyway
+ */
+ protected abstract int getApproximateImageSize();
+
+ protected abstract boolean isDarkTheme();
+
+ /**
+ * Implemented by subclasses to reconfigure the view's layout and subviews, based on whether or
+ * not the contact has a user-defined photo.
+ *
+ * @param isDefaultImage True if the contact does not have a user-defined contact photo (which
+ * means a default contact image will be applied by the {@link ContactPhotoManager}
+ */
+ protected void configureViewForImage(boolean isDefaultImage) {
+ // No-op by default.
+ }
+
+ /**
+ * Implemented by subclasses to allow them to return a {@link DefaultImageRequest} with the
+ * various image parameters defined to match their own layouts.
+ *
+ * @param displayName The display name of the contact
+ * @param lookupKey The lookup key of the contact
+ * @return A {@link DefaultImageRequest} object with each field configured by the subclass as
+ * desired, or {@code null}.
+ */
+ protected DefaultImageRequest getDefaultImageRequest(String displayName, String lookupKey) {
+ return new DefaultImageRequest(displayName, lookupKey, isContactPhotoCircular());
+ }
+
+ /**
+ * Whether contact photo should be displayed as a circular image. Implemented by subclasses so
+ * they can change which drawables to fetch.
+ */
+ protected boolean isContactPhotoCircular() {
+ return true;
+ }
+
+ public interface Listener {
+
+ /** Notification that the contact was selected; no specific action is dictated. */
+ void onContactSelected(Uri contactLookupUri, Rect viewRect);
+
+ /** Notification that the specified number is to be called. */
+ void onCallNumberDirectly(String phoneNumber);
+ }
+}
diff --git a/java/com/android/contacts/common/list/ContactsSectionIndexer.java b/java/com/android/contacts/common/list/ContactsSectionIndexer.java
new file mode 100644
index 000000000..3f0f2b7ee
--- /dev/null
+++ b/java/com/android/contacts/common/list/ContactsSectionIndexer.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.text.TextUtils;
+import android.widget.SectionIndexer;
+import java.util.Arrays;
+
+/**
+ * A section indexer that is configured with precomputed section titles and their respective counts.
+ */
+public class ContactsSectionIndexer implements SectionIndexer {
+
+ private static final String BLANK_HEADER_STRING = " ";
+ private String[] mSections;
+ private int[] mPositions;
+ private int mCount;
+
+ /**
+ * Constructor.
+ *
+ * @param sections a non-null array
+ * @param counts a non-null array of the same size as <code>sections</code>
+ */
+ public ContactsSectionIndexer(String[] sections, int[] counts) {
+ if (sections == null || counts == null) {
+ throw new NullPointerException();
+ }
+
+ if (sections.length != counts.length) {
+ throw new IllegalArgumentException(
+ "The sections and counts arrays must have the same length");
+ }
+
+ // TODO process sections/counts based on current locale and/or specific section titles
+
+ this.mSections = sections;
+ mPositions = new int[counts.length];
+ int position = 0;
+ for (int i = 0; i < counts.length; i++) {
+ if (TextUtils.isEmpty(mSections[i])) {
+ mSections[i] = BLANK_HEADER_STRING;
+ } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
+ mSections[i] = mSections[i].trim();
+ }
+
+ mPositions[i] = position;
+ position += counts[i];
+ }
+ mCount = position;
+ }
+
+ public Object[] getSections() {
+ return mSections;
+ }
+
+ public int getPositionForSection(int section) {
+ if (section < 0 || section >= mSections.length) {
+ return -1;
+ }
+
+ return mPositions[section];
+ }
+
+ public int getSectionForPosition(int position) {
+ if (position < 0 || position >= mCount) {
+ return -1;
+ }
+
+ int index = Arrays.binarySearch(mPositions, position);
+
+ /*
+ * Consider this example: section positions are 0, 3, 5; the supplied
+ * position is 4. The section corresponding to position 4 starts at
+ * position 3, so the expected return value is 1. Binary search will not
+ * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+ * To get from that number to the expected value of 1 we need to negate
+ * and subtract 2.
+ */
+ return index >= 0 ? index : -index - 2;
+ }
+
+ public void setProfileAndFavoritesHeader(String header, int numberOfItemsToAdd) {
+ if (mSections != null) {
+ // Don't do anything if the header is already set properly.
+ if (mSections.length > 0 && header.equals(mSections[0])) {
+ return;
+ }
+
+ // Since the section indexer isn't aware of the profile at the top, we need to add a
+ // special section at the top for it and shift everything else down.
+ String[] tempSections = new String[mSections.length + 1];
+ int[] tempPositions = new int[mPositions.length + 1];
+ tempSections[0] = header;
+ tempPositions[0] = 0;
+ for (int i = 1; i <= mPositions.length; i++) {
+ tempSections[i] = mSections[i - 1];
+ tempPositions[i] = mPositions[i - 1] + numberOfItemsToAdd;
+ }
+ mSections = tempSections;
+ mPositions = tempPositions;
+ mCount = mCount + numberOfItemsToAdd;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/DefaultContactListAdapter.java b/java/com/android/contacts/common/list/DefaultContactListAdapter.java
new file mode 100644
index 000000000..7bcae0e0e
--- /dev/null
+++ b/java/com/android/contacts/common/list/DefaultContactListAdapter.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.SearchSnippets;
+import android.text.TextUtils;
+import android.view.View;
+import com.android.contacts.common.compat.ContactsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. */
+public class DefaultContactListAdapter extends ContactListAdapter {
+
+ public DefaultContactListAdapter(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ String sortOrder = null;
+ if (isSearchMode()) {
+ String query = getQueryString();
+ if (query == null) {
+ query = "";
+ }
+ query = query.trim();
+ if (TextUtils.isEmpty(query)) {
+ // Regardless of the directory, we don't want anything returned,
+ // so let's just send a "nothing" query to the local directory.
+ loader.setUri(Contacts.CONTENT_URI);
+ loader.setProjection(getProjection(false));
+ loader.setSelection("0");
+ } else {
+ final Builder builder = ContactsCompat.getContentUri().buildUpon();
+ appendSearchParameters(builder, query, directoryId);
+ loader.setUri(builder.build());
+ loader.setProjection(getProjection(true));
+ }
+ } else {
+ final ContactListFilter filter = getFilter();
+ configureUri(loader, directoryId, filter);
+ loader.setProjection(getProjection(false));
+ configureSelection(loader, directoryId, filter);
+ }
+
+ if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+ if (sortOrder == null) {
+ sortOrder = Contacts.SORT_KEY_PRIMARY;
+ } else {
+ sortOrder += ", " + Contacts.SORT_KEY_PRIMARY;
+ }
+ } else {
+ if (sortOrder == null) {
+ sortOrder = Contacts.SORT_KEY_ALTERNATIVE;
+ } else {
+ sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE;
+ }
+ }
+ loader.setSortOrder(sortOrder);
+ }
+
+ private void appendSearchParameters(Builder builder, String query, long directoryId) {
+ builder.appendPath(query); // Builder will encode the query
+ builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) {
+ builder.appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY,
+ String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+ }
+ builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1");
+ }
+
+ protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
+ Uri uri = Contacts.CONTENT_URI;
+
+ if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) {
+ uri = ContactListAdapter.buildSectionIndexerUri(uri);
+ }
+
+ // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT
+ if (filter != null
+ && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM
+ && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+ final Uri.Builder builder = uri.buildUpon();
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ filter.addAccountQueryParameterToUrl(builder);
+ }
+ uri = builder.build();
+ }
+
+ loader.setUri(uri);
+ }
+
+ private void configureSelection(CursorLoader loader, long directoryId, ContactListFilter filter) {
+ if (filter == null) {
+ return;
+ }
+
+ if (directoryId != Directory.DEFAULT) {
+ return;
+ }
+
+ StringBuilder selection = new StringBuilder();
+ List<String> selectionArgs = new ArrayList<String>();
+
+ switch (filter.filterType) {
+ case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
+ {
+ // We have already added directory=0 to the URI, which takes care of this
+ // filter
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT:
+ {
+ // We have already added the lookup key to the URI, which takes care of this
+ // filter
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_STARRED:
+ {
+ selection.append(Contacts.STARRED + "!=0");
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ {
+ selection.append(Contacts.HAS_PHONE_NUMBER + "=1");
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_CUSTOM:
+ {
+ selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+ if (isCustomFilterForPhoneNumbersOnly()) {
+ selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+ }
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ACCOUNT:
+ {
+ // We use query parameters for account filter, so no selection to add here.
+ break;
+ }
+ }
+ loader.setSelection(selection.toString());
+ loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+ final ContactListItemView view = (ContactListItemView) itemView;
+
+ view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+
+ bindSectionHeaderAndDivider(view, position, cursor);
+
+ if (isQuickContactEnabled()) {
+ bindQuickContact(
+ view,
+ partition,
+ cursor,
+ ContactQuery.CONTACT_PHOTO_ID,
+ ContactQuery.CONTACT_PHOTO_URI,
+ ContactQuery.CONTACT_ID,
+ ContactQuery.CONTACT_LOOKUP_KEY,
+ ContactQuery.CONTACT_DISPLAY_NAME);
+ } else {
+ if (getDisplayPhotos()) {
+ bindPhoto(view, partition, cursor);
+ }
+ }
+
+ bindNameAndViewId(view, cursor);
+ bindPresenceAndStatusMessage(view, cursor);
+
+ if (isSearchMode()) {
+ bindSearchSnippet(view, cursor);
+ } else {
+ view.setSnippet(null);
+ }
+ }
+
+ private boolean isCustomFilterForPhoneNumbersOnly() {
+ // TODO: this flag should not be stored in shared prefs. It needs to be in the db.
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+ return prefs.getBoolean(
+ ContactsPreferences.PREF_DISPLAY_ONLY_PHONES,
+ ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT);
+ }
+}
diff --git a/java/com/android/contacts/common/list/DirectoryListLoader.java b/java/com/android/contacts/common/list/DirectoryListLoader.java
new file mode 100644
index 000000000..48b098c07
--- /dev/null
+++ b/java/com/android/contacts/common/list/DirectoryListLoader.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.AsyncTaskLoader;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.DirectoryCompat;
+
+/** A specialized loader for the list of directories, see {@link Directory}. */
+public class DirectoryListLoader extends AsyncTaskLoader<Cursor> {
+
+ public static final int SEARCH_MODE_NONE = 0;
+ public static final int SEARCH_MODE_DEFAULT = 1;
+ public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2;
+ public static final int SEARCH_MODE_DATA_SHORTCUT = 3;
+ // This is a virtual column created for a MatrixCursor.
+ public static final String DIRECTORY_TYPE = "directoryType";
+ private static final String TAG = "ContactEntryListAdapter";
+ private static final String[] RESULT_PROJECTION = {
+ Directory._ID, DIRECTORY_TYPE, Directory.DISPLAY_NAME, Directory.PHOTO_SUPPORT,
+ };
+ private final ContentObserver mObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ forceLoad();
+ }
+ };
+ private int mDirectorySearchMode;
+ private boolean mLocalInvisibleDirectoryEnabled;
+ private MatrixCursor mDefaultDirectoryList;
+
+ public DirectoryListLoader(Context context) {
+ super(context);
+ }
+
+ public void setDirectorySearchMode(int mode) {
+ mDirectorySearchMode = mode;
+ }
+
+ /**
+ * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be
+ * included in the results.
+ */
+ public void setLocalInvisibleDirectoryEnabled(boolean flag) {
+ this.mLocalInvisibleDirectoryEnabled = flag;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ getContext().getContentResolver().registerContentObserver(DirectoryQuery.URI, false, mObserver);
+ forceLoad();
+ }
+
+ @Override
+ protected void onStopLoading() {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ if (mDirectorySearchMode == SEARCH_MODE_NONE) {
+ return getDefaultDirectories();
+ }
+
+ MatrixCursor result = new MatrixCursor(RESULT_PROJECTION);
+ Context context = getContext();
+ PackageManager pm = context.getPackageManager();
+ String selection;
+ switch (mDirectorySearchMode) {
+ case SEARCH_MODE_DEFAULT:
+ selection = null;
+ break;
+
+ case SEARCH_MODE_CONTACT_SHORTCUT:
+ selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL;
+ break;
+
+ case SEARCH_MODE_DATA_SHORTCUT:
+ selection =
+ Directory.SHORTCUT_SUPPORT
+ + " IN ("
+ + Directory.SHORTCUT_SUPPORT_FULL
+ + ", "
+ + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY
+ + ")";
+ break;
+
+ default:
+ throw new RuntimeException("Unsupported directory search mode: " + mDirectorySearchMode);
+ }
+ Cursor cursor = null;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(
+ DirectoryQuery.URI,
+ DirectoryQuery.PROJECTION,
+ selection,
+ null,
+ DirectoryQuery.ORDER_BY);
+
+ if (cursor == null) {
+ return result;
+ }
+
+ while (cursor.moveToNext()) {
+ long directoryId = cursor.getLong(DirectoryQuery.ID);
+ if (!mLocalInvisibleDirectoryEnabled && DirectoryCompat.isInvisibleDirectory(directoryId)) {
+ continue;
+ }
+ String directoryType = null;
+
+ String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) {
+ try {
+ directoryType = pm.getResourcesForApplication(packageName).getString(typeResourceId);
+ } catch (Exception e) {
+ Log.e(TAG, "Cannot obtain directory type from package: " + packageName);
+ }
+ }
+ String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+ int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
+ result.addRow(new Object[] {directoryId, directoryType, displayName, photoSupport});
+ }
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Runtime Exception when querying directory");
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return result;
+ }
+
+ private Cursor getDefaultDirectories() {
+ if (mDefaultDirectoryList == null) {
+ mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION);
+ mDefaultDirectoryList.addRow(
+ new Object[] {Directory.DEFAULT, getContext().getString(R.string.contactsList), null});
+ mDefaultDirectoryList.addRow(
+ new Object[] {
+ Directory.LOCAL_INVISIBLE,
+ getContext().getString(R.string.local_invisible_directory),
+ null
+ });
+ }
+ return mDefaultDirectoryList;
+ }
+
+ @Override
+ protected void onReset() {
+ stopLoading();
+ }
+
+ private static final class DirectoryQuery {
+
+ public static final Uri URI = DirectoryCompat.getContentUri();
+ public static final String ORDER_BY = Directory._ID;
+
+ public static final String[] PROJECTION = {
+ Directory._ID,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.DISPLAY_NAME,
+ Directory.PHOTO_SUPPORT,
+ };
+
+ public static final int ID = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int DISPLAY_NAME = 3;
+ public static final int PHOTO_SUPPORT = 4;
+ }
+}
diff --git a/java/com/android/contacts/common/list/DirectoryPartition.java b/java/com/android/contacts/common/list/DirectoryPartition.java
new file mode 100644
index 000000000..26b851041
--- /dev/null
+++ b/java/com/android/contacts/common/list/DirectoryPartition.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.provider.ContactsContract.Directory;
+import com.android.common.widget.CompositeCursorAdapter;
+
+/** Model object for a {@link Directory} row. */
+public final class DirectoryPartition extends CompositeCursorAdapter.Partition {
+
+ public static final int STATUS_NOT_LOADED = 0;
+ public static final int STATUS_LOADING = 1;
+ public static final int STATUS_LOADED = 2;
+
+ public static final int RESULT_LIMIT_DEFAULT = -1;
+
+ private long mDirectoryId;
+ private String mContentUri;
+ private String mDirectoryType;
+ private String mDisplayName;
+ private int mStatus;
+ private boolean mPriorityDirectory;
+ private boolean mPhotoSupported;
+ private int mResultLimit = RESULT_LIMIT_DEFAULT;
+ private boolean mDisplayNumber = true;
+
+ private String mLabel;
+
+ public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) {
+ super(showIfEmpty, hasHeader);
+ }
+
+ /** Directory ID, see {@link Directory}. */
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public void setDirectoryId(long directoryId) {
+ this.mDirectoryId = directoryId;
+ }
+
+ /**
+ * Directory type resolved from {@link Directory#PACKAGE_NAME} and {@link
+ * Directory#TYPE_RESOURCE_ID};
+ */
+ public String getDirectoryType() {
+ return mDirectoryType;
+ }
+
+ public void setDirectoryType(String directoryType) {
+ this.mDirectoryType = directoryType;
+ }
+
+ /** See {@link Directory#DISPLAY_NAME}. */
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public void setDisplayName(String displayName) {
+ this.mDisplayName = displayName;
+ }
+
+ public int getStatus() {
+ return mStatus;
+ }
+
+ public void setStatus(int status) {
+ mStatus = status;
+ }
+
+ public boolean isLoading() {
+ return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING;
+ }
+
+ /** Returns true if this directory should be loaded before non-priority directories. */
+ public boolean isPriorityDirectory() {
+ return mPriorityDirectory;
+ }
+
+ public void setPriorityDirectory(boolean priorityDirectory) {
+ mPriorityDirectory = priorityDirectory;
+ }
+
+ /** Returns true if this directory supports photos. */
+ public boolean isPhotoSupported() {
+ return mPhotoSupported;
+ }
+
+ public void setPhotoSupported(boolean flag) {
+ this.mPhotoSupported = flag;
+ }
+
+ /**
+ * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which
+ * implies using the adapter's {@link
+ * com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()}
+ */
+ public int getResultLimit() {
+ return mResultLimit;
+ }
+
+ public void setResultLimit(int resultLimit) {
+ mResultLimit = resultLimit;
+ }
+
+ /**
+ * Used by extended directories to specify a custom content URI. Extended directories MUST have a
+ * content URI
+ */
+ public String getContentUri() {
+ return mContentUri;
+ }
+
+ public void setContentUri(String contentUri) {
+ mContentUri = contentUri;
+ }
+
+ /** A label to display in the header next to the display name. */
+ public String getLabel() {
+ return mLabel;
+ }
+
+ public void setLabel(String label) {
+ mLabel = label;
+ }
+
+ @Override
+ public String toString() {
+ return "DirectoryPartition{"
+ + "mDirectoryId="
+ + mDirectoryId
+ + ", mContentUri='"
+ + mContentUri
+ + '\''
+ + ", mDirectoryType='"
+ + mDirectoryType
+ + '\''
+ + ", mDisplayName='"
+ + mDisplayName
+ + '\''
+ + ", mStatus="
+ + mStatus
+ + ", mPriorityDirectory="
+ + mPriorityDirectory
+ + ", mPhotoSupported="
+ + mPhotoSupported
+ + ", mResultLimit="
+ + mResultLimit
+ + ", mLabel='"
+ + mLabel
+ + '\''
+ + '}';
+ }
+
+ /**
+ * Whether or not to display the phone number in app that have that option - Dialer. If false,
+ * Phone Label should be used instead of Phone Number.
+ */
+ public boolean isDisplayNumber() {
+ return mDisplayNumber;
+ }
+
+ public void setDisplayNumber(boolean displayNumber) {
+ mDisplayNumber = displayNumber;
+ }
+}
diff --git a/java/com/android/contacts/common/list/IndexerListAdapter.java b/java/com/android/contacts/common/list/IndexerListAdapter.java
new file mode 100644
index 000000000..2289f6e59
--- /dev/null
+++ b/java/com/android/contacts/common/list/IndexerListAdapter.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.SectionIndexer;
+
+/** A list adapter that supports section indexer and a pinned header. */
+public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer {
+
+ protected Context mContext;
+ private SectionIndexer mIndexer;
+ private int mIndexedPartition = 0;
+ private boolean mSectionHeaderDisplayEnabled;
+ private View mHeader;
+ private Placement mPlacementCache = new Placement();
+
+ /** Constructor. */
+ public IndexerListAdapter(Context context) {
+ super(context);
+ mContext = context;
+ }
+
+ /**
+ * Creates a section header view that will be pinned at the top of the list as the user scrolls.
+ */
+ protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent);
+
+ /** Sets the title in the pinned header as the user scrolls. */
+ protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title);
+
+ public boolean isSectionHeaderDisplayEnabled() {
+ return mSectionHeaderDisplayEnabled;
+ }
+
+ public void setSectionHeaderDisplayEnabled(boolean flag) {
+ this.mSectionHeaderDisplayEnabled = flag;
+ }
+
+ public int getIndexedPartition() {
+ return mIndexedPartition;
+ }
+
+ public void setIndexedPartition(int partition) {
+ this.mIndexedPartition = partition;
+ }
+
+ public SectionIndexer getIndexer() {
+ return mIndexer;
+ }
+
+ public void setIndexer(SectionIndexer indexer) {
+ mIndexer = indexer;
+ mPlacementCache.invalidate();
+ }
+
+ public Object[] getSections() {
+ if (mIndexer == null) {
+ return new String[] {" "};
+ } else {
+ return mIndexer.getSections();
+ }
+ }
+
+ /** @return relative position of the section in the indexed partition */
+ public int getPositionForSection(int sectionIndex) {
+ if (mIndexer == null) {
+ return -1;
+ }
+
+ return mIndexer.getPositionForSection(sectionIndex);
+ }
+
+ /** @param position relative position in the indexed partition */
+ public int getSectionForPosition(int position) {
+ if (mIndexer == null) {
+ return -1;
+ }
+
+ return mIndexer.getSectionForPosition(position);
+ }
+
+ @Override
+ public int getPinnedHeaderCount() {
+ if (isSectionHeaderDisplayEnabled()) {
+ return super.getPinnedHeaderCount() + 1;
+ } else {
+ return super.getPinnedHeaderCount();
+ }
+ }
+
+ @Override
+ public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) {
+ if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) {
+ if (mHeader == null) {
+ mHeader = createPinnedSectionHeaderView(mContext, parent);
+ }
+ return mHeader;
+ } else {
+ return super.getPinnedHeaderView(viewIndex, convertView, parent);
+ }
+ }
+
+ @Override
+ public void configurePinnedHeaders(PinnedHeaderListView listView) {
+ super.configurePinnedHeaders(listView);
+
+ if (!isSectionHeaderDisplayEnabled()) {
+ return;
+ }
+
+ int index = getPinnedHeaderCount() - 1;
+ if (mIndexer == null || getCount() == 0) {
+ listView.setHeaderInvisible(index, false);
+ } else {
+ int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight());
+ int position = listPosition - listView.getHeaderViewsCount();
+
+ int section = -1;
+ int partition = getPartitionForPosition(position);
+ if (partition == mIndexedPartition) {
+ int offset = getOffsetInPartition(position);
+ if (offset != -1) {
+ section = getSectionForPosition(offset);
+ }
+ }
+
+ if (section == -1) {
+ listView.setHeaderInvisible(index, false);
+ } else {
+ View topChild = listView.getChildAt(listPosition);
+ if (topChild != null) {
+ // Match the pinned header's height to the height of the list item.
+ mHeader.setMinimumHeight(topChild.getMeasuredHeight());
+ }
+ setPinnedSectionTitle(mHeader, (String) mIndexer.getSections()[section]);
+
+ // Compute the item position where the current partition begins
+ int partitionStart = getPositionForPartition(mIndexedPartition);
+ if (hasHeader(mIndexedPartition)) {
+ partitionStart++;
+ }
+
+ // Compute the item position where the next section begins
+ int nextSectionPosition = partitionStart + getPositionForSection(section + 1);
+ boolean isLastInSection = position == nextSectionPosition - 1;
+ listView.setFadingHeader(index, listPosition, isLastInSection);
+ }
+ }
+ }
+
+ /**
+ * Computes the item's placement within its section and populates the {@code placement} object
+ * accordingly. Please note that the returned object is volatile and should be copied if the
+ * result needs to be used later.
+ */
+ public Placement getItemPlacementInSection(int position) {
+ if (mPlacementCache.position == position) {
+ return mPlacementCache;
+ }
+
+ mPlacementCache.position = position;
+ if (isSectionHeaderDisplayEnabled()) {
+ int section = getSectionForPosition(position);
+ if (section != -1 && getPositionForSection(section) == position) {
+ mPlacementCache.firstInSection = true;
+ mPlacementCache.sectionHeader = (String) getSections()[section];
+ } else {
+ mPlacementCache.firstInSection = false;
+ mPlacementCache.sectionHeader = null;
+ }
+
+ mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position);
+ } else {
+ mPlacementCache.firstInSection = false;
+ mPlacementCache.lastInSection = false;
+ mPlacementCache.sectionHeader = null;
+ }
+ return mPlacementCache;
+ }
+
+ /**
+ * An item view is displayed differently depending on whether it is placed at the beginning,
+ * middle or end of a section. It also needs to know the section header when it is at the
+ * beginning of a section. This object captures all this configuration.
+ */
+ public static final class Placement {
+
+ public boolean firstInSection;
+ public boolean lastInSection;
+ public String sectionHeader;
+ private int position = ListView.INVALID_POSITION;
+
+ public void invalidate() {
+ position = ListView.INVALID_POSITION;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
new file mode 100644
index 000000000..89bd889e6
--- /dev/null
+++ b/java/com/android/contacts/common/list/OnPhoneNumberPickerActionListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.app.ActionBar;
+import android.net.Uri;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+
+/** Action callbacks that can be sent by a phone number picker. */
+public interface OnPhoneNumberPickerActionListener {
+
+ int CALL_INITIATION_UNKNOWN = 0;
+
+ /** Returns the selected phone number uri to the requester. */
+ void onPickDataUri(Uri dataUri, boolean isVideoCall, CallSpecificAppData callSpecificAppData);
+
+ /**
+ * Returns the specified phone number to the requester. May call the specified phone number,
+ * either as an audio or video call.
+ */
+ void onPickPhoneNumber(
+ String phoneNumber, boolean isVideoCall, CallSpecificAppData callSpecificAppData);
+
+ /** Called when home menu in {@link ActionBar} is clicked by the user. */
+ void onHomeInActionBarSelected();
+}
diff --git a/java/com/android/contacts/common/list/PhoneNumberListAdapter.java b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java
new file mode 100644
index 000000000..c7b24229f
--- /dev/null
+++ b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.CursorLoader;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.CallableCompat;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.compat.PhoneCompat;
+import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.Constants;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.CallUtil;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and {@link
+ * SipAddress#CONTENT_ITEM_TYPE}.
+ *
+ * <p>By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)}
+ * is called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
+ * API instead of {@link Phone}.
+ */
+public class PhoneNumberListAdapter extends ContactEntryListAdapter {
+
+ private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
+ private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000";
+ // A list of extended directories to add to the directories from the database
+ private final List<DirectoryPartition> mExtendedDirectories;
+ private final CharSequence mUnknownNameText;
+ // Extended directories will have ID's that are higher than any of the id's from the database,
+ // so that we can identify them and set them up properly. If no extended directories
+ // exist, this will be Long.MAX_VALUE
+ private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
+ private ContactListItemView.PhotoPosition mPhotoPosition;
+ private boolean mUseCallableUri;
+ private Listener mListener;
+ private boolean mIsVideoEnabled;
+ private boolean mIsPresenceEnabled;
+
+ public PhoneNumberListAdapter(Context context) {
+ super(context);
+ setDefaultFilterHeaderText(R.string.list_filter_phones);
+ mUnknownNameText = context.getText(android.R.string.unknownName);
+
+ mExtendedDirectories =
+ PhoneDirectoryExtenderAccessor.get(mContext).getExtendedDirectories(mContext);
+
+ int videoCapabilities = CallUtil.getVideoCallingAvailability(context);
+ mIsVideoEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_ENABLED) != 0;
+ mIsPresenceEnabled = (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0;
+ }
+
+ @Override
+ public void configureLoader(CursorLoader loader, long directoryId) {
+ String query = getQueryString();
+ if (query == null) {
+ query = "";
+ }
+ if (isExtendedDirectory(directoryId)) {
+ final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
+ final String contentUri = directory.getContentUri();
+ if (contentUri == null) {
+ throw new IllegalStateException("Extended directory must have a content URL: " + directory);
+ }
+ final Builder builder = Uri.parse(contentUri).buildUpon();
+ builder.appendPath(query);
+ builder.appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY, String.valueOf(getDirectoryResultLimit(directory)));
+ loader.setUri(builder.build());
+ loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+ } else {
+ final boolean isRemoteDirectoryQuery = DirectoryCompat.isRemoteDirectoryId(directoryId);
+ final Builder builder;
+ if (isSearchMode()) {
+ final Uri baseUri;
+ if (isRemoteDirectoryQuery) {
+ baseUri = PhoneCompat.getContentFilterUri();
+ } else if (mUseCallableUri) {
+ baseUri = CallableCompat.getContentFilterUri();
+ } else {
+ baseUri = PhoneCompat.getContentFilterUri();
+ }
+ builder = baseUri.buildUpon();
+ builder.appendPath(query); // Builder will encode the query
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ if (isRemoteDirectoryQuery) {
+ builder.appendQueryParameter(
+ ContactsContract.LIMIT_PARAM_KEY,
+ String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
+ }
+ } else {
+ Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
+ builder =
+ baseUri
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
+ if (isSectionHeaderDisplayEnabled()) {
+ builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true");
+ }
+ applyFilter(loader, builder, directoryId, getFilter());
+ }
+
+ // Ignore invalid phone numbers that are too long. These can potentially cause freezes
+ // in the UI and there is no reason to display them.
+ final String prevSelection = loader.getSelection();
+ final String newSelection;
+ if (!TextUtils.isEmpty(prevSelection)) {
+ newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE;
+ } else {
+ newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE;
+ }
+ loader.setSelection(newSelection);
+
+ // Remove duplicates when it is possible.
+ builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
+ loader.setUri(builder.build());
+
+ // TODO a projection that includes the search snippet
+ if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
+ } else {
+ loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
+ }
+
+ if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+ loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
+ } else {
+ loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
+ }
+ }
+ }
+
+ protected boolean isExtendedDirectory(long directoryId) {
+ return directoryId >= mFirstExtendedDirectoryId;
+ }
+
+ private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
+ final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
+ return mExtendedDirectories.get(directoryIndex);
+ }
+
+ /**
+ * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
+ * filter}.
+ */
+ private void applyFilter(
+ CursorLoader loader, Uri.Builder uriBuilder, long directoryId, ContactListFilter filter) {
+ if (filter == null || directoryId != Directory.DEFAULT) {
+ return;
+ }
+
+ final StringBuilder selection = new StringBuilder();
+ final List<String> selectionArgs = new ArrayList<String>();
+
+ switch (filter.filterType) {
+ case ContactListFilter.FILTER_TYPE_CUSTOM:
+ {
+ selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
+ selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ACCOUNT:
+ {
+ filter.addAccountQueryParameterToUrl(uriBuilder);
+ break;
+ }
+ case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
+ case ContactListFilter.FILTER_TYPE_DEFAULT:
+ break; // No selection needed.
+ case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+ break; // This adapter is always "phone only", so no selection needed either.
+ default:
+ Log.w(
+ TAG,
+ "Unsupported filter type came "
+ + "(type: "
+ + filter.filterType
+ + ", toString: "
+ + filter
+ + ")"
+ + " showing all contacts.");
+ // No selection.
+ break;
+ }
+ loader.setSelection(selection.toString());
+ loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
+ }
+
+ public String getPhoneNumber(int position) {
+ final Cursor item = (Cursor) getItem(position);
+ return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
+ }
+
+ /**
+ * Retrieves the lookup key for the given cursor position.
+ *
+ * @param position The cursor position.
+ * @return The lookup key.
+ */
+ public String getLookupKey(int position) {
+ final Cursor item = (Cursor) getItem(position);
+ return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null;
+ }
+
+ @Override
+ protected ContactListItemView newView(
+ Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
+ ContactListItemView view = super.newView(context, partition, cursor, position, parent);
+ view.setUnknownNameText(mUnknownNameText);
+ view.setQuickContactEnabled(isQuickContactEnabled());
+ view.setPhotoPosition(mPhotoPosition);
+ return view;
+ }
+
+ protected void setHighlight(ContactListItemView view, Cursor cursor) {
+ view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
+ }
+
+ @Override
+ protected void bindView(View itemView, int partition, Cursor cursor, int position) {
+ super.bindView(itemView, partition, cursor, position);
+ ContactListItemView view = (ContactListItemView) itemView;
+
+ setHighlight(view, cursor);
+
+ // Look at elements before and after this position, checking if contact IDs are same.
+ // If they have one same contact ID, it means they can be grouped.
+ //
+ // In one group, only the first entry will show its photo and its name, and the other
+ // entries in the group show just their data (e.g. phone number, email address).
+ cursor.moveToPosition(position);
+ boolean isFirstEntry = true;
+ final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+ if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
+ final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
+ if (currentContactId == previousContactId) {
+ isFirstEntry = false;
+ }
+ }
+ cursor.moveToPosition(position);
+
+ bindViewId(view, cursor, PhoneQuery.PHONE_ID);
+
+ bindSectionHeaderAndDivider(view, position);
+ if (isFirstEntry) {
+ bindName(view, cursor);
+ if (isQuickContactEnabled()) {
+ bindQuickContact(
+ view,
+ partition,
+ cursor,
+ PhoneQuery.PHOTO_ID,
+ PhoneQuery.PHOTO_URI,
+ PhoneQuery.CONTACT_ID,
+ PhoneQuery.LOOKUP_KEY,
+ PhoneQuery.DISPLAY_NAME);
+ } else {
+ if (getDisplayPhotos()) {
+ bindPhoto(view, partition, cursor);
+ }
+ }
+ } else {
+ unbindName(view);
+
+ view.removePhotoView(true, false);
+ }
+
+ final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
+
+ // If the first partition does not have a header, then all subsequent partitions'
+ // getPositionForPartition returns an index off by 1.
+ int partitionOffset = 0;
+ if (partition > 0 && !getPartition(0).getHasHeader()) {
+ partitionOffset = 1;
+ }
+ position += getPositionForPartition(partition) + partitionOffset;
+
+ bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position);
+ }
+
+ protected void bindPhoneNumber(
+ ContactListItemView view, Cursor cursor, boolean displayNumber, int position) {
+ CharSequence label = null;
+ if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
+ final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
+ final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+
+ // TODO cache
+ label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
+ }
+ view.setLabel(label);
+ final String text;
+ if (displayNumber) {
+ text = cursor.getString(PhoneQuery.PHONE_NUMBER);
+ } else {
+ // Display phone label. If that's null, display geocoded location for the number
+ final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
+ if (phoneLabel != null) {
+ text = phoneLabel;
+ } else {
+ final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
+ text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
+ }
+ }
+ view.setPhoneNumber(text);
+
+ if (CompatUtils.isVideoCompatible()) {
+ // Determine if carrier presence indicates the number supports video calling.
+ int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE);
+ boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
+
+ boolean isVideoIconShown = mIsVideoEnabled && (!mIsPresenceEnabled || isPresent);
+ view.setShowVideoCallIcon(isVideoIconShown, mListener, position);
+ }
+ }
+
+ protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
+ if (isSectionHeaderDisplayEnabled()) {
+ Placement placement = getItemPlacementInSection(position);
+ view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
+ } else {
+ view.setSectionHeader(null);
+ }
+ }
+
+ protected void bindName(final ContactListItemView view, Cursor cursor) {
+ view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME);
+ // Note: we don't show phonetic names any more (see issue 5265330)
+ }
+
+ protected void unbindName(final ContactListItemView view) {
+ view.hideDisplayName();
+ }
+
+ @Override
+ protected void bindWorkProfileIcon(final ContactListItemView view, int partition) {
+ final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
+ final long directoryId = directory.getDirectoryId();
+ final long userType = ContactsUtils.determineUserType(directoryId, null);
+ // Work directory must not be a extended directory. An extended directory is custom
+ // directory in the app, but not a directory provided by framework. So it can't be
+ // USER_TYPE_WORK.
+ view.setWorkProfileIconEnabled(
+ !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK);
+ }
+
+ protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
+ if (!isPhotoSupported(partitionIndex)) {
+ view.removePhotoView();
+ return;
+ }
+
+ long photoId = 0;
+ if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
+ photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
+ }
+
+ if (photoId != 0) {
+ getPhotoLoader()
+ .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null);
+ } else {
+ final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
+ final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
+
+ DefaultImageRequest request = null;
+ if (photoUri == null) {
+ final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME);
+ final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY);
+ request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos());
+ }
+ getPhotoLoader()
+ .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request);
+ }
+ }
+
+ public ContactListItemView.PhotoPosition getPhotoPosition() {
+ return mPhotoPosition;
+ }
+
+ public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+ }
+
+ public void setUseCallableUri(boolean useCallableUri) {
+ mUseCallableUri = useCallableUri;
+ }
+
+ /**
+ * Override base implementation to inject extended directories between local & remote directories.
+ * This is done in the following steps: 1. Call base implementation to add directories from the
+ * cursor. 2. Iterate all base directories and establish the following information: a. The highest
+ * directory id so that we can assign unused id's to the extended directories. b. The index of the
+ * last non-remote directory. This is where we will insert extended directories. 3. Iterate the
+ * extended directories and for each one, assign an ID and insert it in the proper location.
+ */
+ @Override
+ public void changeDirectories(Cursor cursor) {
+ super.changeDirectories(cursor);
+ if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
+ return;
+ }
+ final int numExtendedDirectories = mExtendedDirectories.size();
+ if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
+ // already added all directories;
+ return;
+ }
+ //
+ mFirstExtendedDirectoryId = Long.MAX_VALUE;
+ if (numExtendedDirectories > 0) {
+ // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
+ // "special" ID.
+ long maxId = Directory.LOCAL_INVISIBLE;
+ int insertIndex = 0;
+ for (int i = 0, n = getPartitionCount(); i < n; i++) {
+ final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+ final long id = partition.getDirectoryId();
+ if (id > maxId) {
+ maxId = id;
+ }
+ if (!DirectoryCompat.isRemoteDirectoryId(id)) {
+ // assuming remote directories come after local, we will end up with the index
+ // where we should insert extended directories. This also works if there are no
+ // remote directories at all.
+ insertIndex = i + 1;
+ }
+ }
+ // Extended directories ID's cannot collide with base directories
+ mFirstExtendedDirectoryId = maxId + 1;
+ for (int i = 0; i < numExtendedDirectories; i++) {
+ final long id = mFirstExtendedDirectoryId + i;
+ final DirectoryPartition directory = mExtendedDirectories.get(i);
+ if (getPartitionByDirectoryId(id) == -1) {
+ addPartition(insertIndex, directory);
+ directory.setDirectoryId(id);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Uri getContactUri(
+ int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) {
+ final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
+ final long directoryId = directory.getDirectoryId();
+ if (!isExtendedDirectory(directoryId)) {
+ return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
+ }
+ return Contacts.CONTENT_LOOKUP_URI
+ .buildUpon()
+ .appendPath(Constants.LOOKUP_URI_ENCODED)
+ .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
+ .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
+ .encodedFragment(cursor.getString(lookUpKeyColumn))
+ .build();
+ }
+
+ public Listener getListener() {
+ return mListener;
+ }
+
+ public void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ public interface Listener {
+
+ void onVideoCallIconClicked(int position);
+ }
+
+ public static class PhoneQuery {
+
+ /**
+ * Optional key used as part of a JSON lookup key to specify an analytics category associated
+ * with the row.
+ */
+ public static final String ANALYTICS_CATEGORY = "analytics_category";
+
+ /**
+ * Optional key used as part of a JSON lookup key to specify an analytics action associated with
+ * the row.
+ */
+ public static final String ANALYTICS_ACTION = "analytics_action";
+
+ /**
+ * Optional key used as part of a JSON lookup key to specify an analytics value associated with
+ * the row.
+ */
+ public static final String ANALYTICS_VALUE = "analytics_value";
+
+ public static final String[] PROJECTION_PRIMARY_INTERNAL =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.PHOTO_ID, // 6
+ Phone.DISPLAY_NAME_PRIMARY, // 7
+ Phone.PHOTO_THUMBNAIL_URI, // 8
+ };
+
+ public static final String[] PROJECTION_PRIMARY;
+ public static final String[] PROJECTION_ALTERNATIVE_INTERNAL =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.PHOTO_ID, // 6
+ Phone.DISPLAY_NAME_ALTERNATIVE, // 7
+ Phone.PHOTO_THUMBNAIL_URI, // 8
+ };
+ public static final String[] PROJECTION_ALTERNATIVE;
+ public static final int PHONE_ID = 0;
+ public static final int PHONE_TYPE = 1;
+ public static final int PHONE_LABEL = 2;
+ public static final int PHONE_NUMBER = 3;
+ public static final int CONTACT_ID = 4;
+ public static final int LOOKUP_KEY = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int DISPLAY_NAME = 7;
+ public static final int PHOTO_URI = 8;
+ public static final int CARRIER_PRESENCE = 9;
+
+ static {
+ final List<String> projectionList =
+ new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL));
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Phone.CARRIER_PRESENCE); // 9
+ }
+ PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ static {
+ final List<String> projectionList =
+ new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL));
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Phone.CARRIER_PRESENCE); // 9
+ }
+ PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java
new file mode 100644
index 000000000..4ae81529b
--- /dev/null
+++ b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.AccountFilterUtil;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import java.util.Set;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Fragment containing a phone number list for picking. */
+public class PhoneNumberPickerFragment extends ContactEntryListFragment<ContactEntryListAdapter>
+ implements PhoneNumberListAdapter.Listener {
+
+ private static final String KEY_FILTER = "filter";
+ private OnPhoneNumberPickerActionListener mListener;
+ private ContactListFilter mFilter;
+ private View mAccountFilterHeader;
+ /**
+ * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE.
+ */
+ private View mPaddingView;
+ /** true if the loader has started at least once. */
+ private boolean mLoaderStarted;
+
+ private boolean mUseCallableUri;
+
+ private ContactListItemView.PhotoPosition mPhotoPosition =
+ ContactListItemView.getDefaultPhotoPosition(false /* normal/non opposite */);
+
+ private final Set<OnLoadFinishedListener> mLoadFinishedListeners =
+ new ArraySet<OnLoadFinishedListener>();
+
+ private CursorReranker mCursorReranker;
+
+ public PhoneNumberPickerFragment() {
+ setQuickContactEnabled(false);
+ setPhotoLoaderEnabled(true);
+ setSectionHeaderDisplayEnabled(true);
+ setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
+
+ // Show nothing instead of letting caller Activity show something.
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * Handles a click on the video call icon for a row in the list.
+ *
+ * @param position The position in the list where the click ocurred.
+ */
+ @Override
+ public void onVideoCallIconClicked(int position) {
+ callNumber(position, true /* isVideoCall */);
+ }
+
+ public void setDirectorySearchEnabled(boolean flag) {
+ setDirectorySearchMode(
+ flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE);
+ }
+
+ public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) {
+ this.mListener = listener;
+ }
+
+ public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() {
+ return mListener;
+ }
+
+ @Override
+ protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
+ super.onCreateView(inflater, container);
+
+ View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false);
+ mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding);
+ getListView().addHeaderView(paddingView);
+
+ mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container);
+ updateFilterHeaderView();
+
+ setVisibleScrollbarEnabled(getVisibleScrollbarEnabled());
+ }
+
+ protected boolean getVisibleScrollbarEnabled() {
+ return true;
+ }
+
+ @Override
+ protected void setSearchMode(boolean flag) {
+ super.setSearchMode(flag);
+ updateFilterHeaderView();
+ }
+
+ private void updateFilterHeaderView() {
+ final ContactListFilter filter = getFilter();
+ if (mAccountFilterHeader == null || filter == null) {
+ return;
+ }
+ final boolean shouldShowHeader =
+ !isSearchMode()
+ && AccountFilterUtil.updateAccountFilterTitleForPhone(
+ mAccountFilterHeader, filter, false);
+ if (shouldShowHeader) {
+ mPaddingView.setVisibility(View.GONE);
+ mAccountFilterHeader.setVisibility(View.VISIBLE);
+ } else {
+ mPaddingView.setVisibility(View.VISIBLE);
+ mAccountFilterHeader.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void restoreSavedState(Bundle savedState) {
+ super.restoreSavedState(savedState);
+
+ if (savedState == null) {
+ return;
+ }
+
+ mFilter = savedState.getParcelable(KEY_FILTER);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_FILTER, mFilter);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ if (mListener != null) {
+ mListener.onHomeInActionBarSelected();
+ }
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onItemClick(int position, long id) {
+ callNumber(position, false /* isVideoCall */);
+ }
+
+ /**
+ * Initiates a call to the number at the specified position.
+ *
+ * @param position The position.
+ * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false}
+ * otherwise.
+ */
+ private void callNumber(int position, boolean isVideoCall) {
+ final String number = getPhoneNumber(position);
+ if (!TextUtils.isEmpty(number)) {
+ cacheContactInfo(position);
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = getCallInitiationType(true /* isRemoteDirectory */);
+ callSpecificAppData.positionOfSelectedSearchResult = position;
+ callSpecificAppData.charactersInSearchString =
+ getQueryString() == null ? 0 : getQueryString().length();
+ mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData);
+ } else {
+ LogUtil.i(
+ "PhoneNumberPickerFragment.callNumber",
+ "item at %d was clicked before adapter is ready, ignoring",
+ position);
+ }
+
+ // Get the lookup key and track any analytics
+ final String lookupKey = getLookupKey(position);
+ if (!TextUtils.isEmpty(lookupKey)) {
+ maybeTrackAnalytics(lookupKey);
+ }
+ }
+
+ protected void cacheContactInfo(int position) {
+ // Not implemented. Hook for child classes
+ }
+
+ protected String getPhoneNumber(int position) {
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ return adapter.getPhoneNumber(position);
+ }
+
+ protected String getLookupKey(int position) {
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ return adapter.getLookupKey(position);
+ }
+
+ @Override
+ protected void startLoading() {
+ mLoaderStarted = true;
+ super.startLoading();
+ }
+
+ @Override
+ @MainThread
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ Assert.isMainThread();
+ // TODO: define and verify behavior for "Nearby places", corp directories,
+ // and dividers listed in UI between these categories
+ if (mCursorReranker != null
+ && data != null
+ && !data.isClosed()
+ && data.getCount() > 0
+ && loader.getId() != -1) { // skip invalid directory ID of -1
+ data = mCursorReranker.rerankCursor(data);
+ }
+ super.onLoadFinished(loader, data);
+
+ // disable scroll bar if there is no data
+ setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0);
+
+ if (data != null) {
+ notifyListeners();
+ }
+ }
+
+ /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */
+ public interface CursorReranker {
+ @MainThread
+ Cursor rerankCursor(Cursor data);
+ }
+
+ @MainThread
+ public void setReranker(@Nullable CursorReranker reranker) {
+ Assert.isMainThread();
+ mCursorReranker = reranker;
+ }
+
+ /** Listener that is notified when cursor has finished loading data. */
+ public interface OnLoadFinishedListener {
+ void onLoadFinished();
+ }
+
+ @MainThread
+ public void addOnLoadFinishedListener(OnLoadFinishedListener listener) {
+ Assert.isMainThread();
+ mLoadFinishedListeners.add(listener);
+ }
+
+ @MainThread
+ public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) {
+ Assert.isMainThread();
+ mLoadFinishedListeners.remove(listener);
+ }
+
+ @MainThread
+ protected void notifyListeners() {
+ Assert.isMainThread();
+ for (OnLoadFinishedListener listener : mLoadFinishedListeners) {
+ listener.onLoadFinished();
+ }
+ }
+
+ @MainThread
+ @Override
+ public void onDetach() {
+ Assert.isMainThread();
+ mLoadFinishedListeners.clear();
+ super.onDetach();
+ }
+
+ public void setUseCallableUri(boolean useCallableUri) {
+ mUseCallableUri = useCallableUri;
+ }
+
+ public boolean usesCallableUri() {
+ return mUseCallableUri;
+ }
+
+ @Override
+ protected ContactEntryListAdapter createListAdapter() {
+ PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
+ adapter.setDisplayPhotos(true);
+ adapter.setUseCallableUri(mUseCallableUri);
+ return adapter;
+ }
+
+ @Override
+ protected void configureAdapter() {
+ super.configureAdapter();
+
+ final ContactEntryListAdapter adapter = getAdapter();
+ if (adapter == null) {
+ return;
+ }
+
+ if (!isSearchMode() && mFilter != null) {
+ adapter.setFilter(mFilter);
+ }
+
+ setPhotoPosition(adapter);
+ }
+
+ protected void setPhotoPosition(ContactEntryListAdapter adapter) {
+ ((PhoneNumberListAdapter) adapter).setPhotoPosition(mPhotoPosition);
+ }
+
+ @Override
+ protected View inflateView(LayoutInflater inflater, ViewGroup container) {
+ return inflater.inflate(R.layout.contact_list_content, null);
+ }
+
+ public ContactListFilter getFilter() {
+ return mFilter;
+ }
+
+ public void setFilter(ContactListFilter filter) {
+ if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) {
+ return;
+ }
+
+ mFilter = filter;
+ if (mLoaderStarted) {
+ reloadData();
+ }
+ updateFilterHeaderView();
+ }
+
+ public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
+ mPhotoPosition = photoPosition;
+
+ final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter();
+ if (adapter != null) {
+ adapter.setPhotoPosition(photoPosition);
+ }
+ }
+
+ /**
+ * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number
+ * not in the local contacts database
+ */
+ protected int getCallInitiationType(boolean isRemoteDirectory) {
+ return OnPhoneNumberPickerActionListener.CALL_INITIATION_UNKNOWN;
+ }
+
+ /**
+ * Where a lookup key contains analytic event information, logs the associated analytics event.
+ *
+ * @param lookupKey The lookup key JSON object.
+ */
+ private void maybeTrackAnalytics(String lookupKey) {
+ try {
+ JSONObject json = new JSONObject(lookupKey);
+
+ String analyticsCategory =
+ json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY);
+ String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION);
+ String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE);
+
+ if (TextUtils.isEmpty(analyticsCategory)
+ || TextUtils.isEmpty(analyticsAction)
+ || TextUtils.isEmpty(analyticsValue)) {
+ return;
+ }
+
+ // Assume that the analytic value being tracked could be a float value, but just cast
+ // to a long so that the analytic server can handle it.
+ long value;
+ try {
+ float floatValue = Float.parseFloat(analyticsValue);
+ value = (long) floatValue;
+ } catch (NumberFormatException nfe) {
+ return;
+ }
+
+ Logger.get(getActivity())
+ .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value);
+ } catch (JSONException e) {
+ // Not an error; just a lookup key that doesn't have the right information.
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java
new file mode 100644
index 000000000..0bdcef084
--- /dev/null
+++ b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.common.widget.CompositeCursorAdapter;
+
+/** A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. */
+public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter
+ implements PinnedHeaderListView.PinnedHeaderAdapter {
+
+ public static final int PARTITION_HEADER_TYPE = 0;
+
+ private boolean mPinnedPartitionHeadersEnabled;
+ private boolean[] mHeaderVisibility;
+
+ public PinnedHeaderListAdapter(Context context) {
+ super(context);
+ }
+
+ public boolean getPinnedPartitionHeadersEnabled() {
+ return mPinnedPartitionHeadersEnabled;
+ }
+
+ public void setPinnedPartitionHeadersEnabled(boolean flag) {
+ this.mPinnedPartitionHeadersEnabled = flag;
+ }
+
+ @Override
+ public int getPinnedHeaderCount() {
+ if (mPinnedPartitionHeadersEnabled) {
+ return getPartitionCount();
+ } else {
+ return 0;
+ }
+ }
+
+ protected boolean isPinnedPartitionHeaderVisible(int partition) {
+ return getPinnedPartitionHeadersEnabled()
+ && hasHeader(partition)
+ && !isPartitionEmpty(partition);
+ }
+
+ /** The default implementation creates the same type of view as a normal partition header. */
+ @Override
+ public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) {
+ if (hasHeader(partition)) {
+ View view = null;
+ if (convertView != null) {
+ Integer headerType = (Integer) convertView.getTag();
+ if (headerType != null && headerType == PARTITION_HEADER_TYPE) {
+ view = convertView;
+ }
+ }
+ if (view == null) {
+ view = newHeaderView(getContext(), partition, null, parent);
+ view.setTag(PARTITION_HEADER_TYPE);
+ view.setFocusable(false);
+ view.setEnabled(false);
+ }
+ bindHeaderView(view, partition, getCursor(partition));
+ view.setLayoutDirection(parent.getLayoutDirection());
+ return view;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public void configurePinnedHeaders(PinnedHeaderListView listView) {
+ if (!getPinnedPartitionHeadersEnabled()) {
+ return;
+ }
+
+ int size = getPartitionCount();
+
+ // Cache visibility bits, because we will need them several times later on
+ if (mHeaderVisibility == null || mHeaderVisibility.length != size) {
+ mHeaderVisibility = new boolean[size];
+ }
+ for (int i = 0; i < size; i++) {
+ boolean visible = isPinnedPartitionHeaderVisible(i);
+ mHeaderVisibility[i] = visible;
+ if (!visible) {
+ listView.setHeaderInvisible(i, true);
+ }
+ }
+
+ int headerViewsCount = listView.getHeaderViewsCount();
+
+ // Starting at the top, find and pin headers for partitions preceding the visible one(s)
+ int maxTopHeader = -1;
+ int topHeaderHeight = 0;
+ for (int i = 0; i < size; i++) {
+ if (mHeaderVisibility[i]) {
+ int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount;
+ int partition = getPartitionForPosition(position);
+ if (i > partition) {
+ break;
+ }
+
+ listView.setHeaderPinnedAtTop(i, topHeaderHeight, false);
+ topHeaderHeight += listView.getPinnedHeaderHeight(i);
+ maxTopHeader = i;
+ }
+ }
+
+ // Starting at the bottom, find and pin headers for partitions following the visible one(s)
+ int maxBottomHeader = size;
+ int bottomHeaderHeight = 0;
+ int listHeight = listView.getHeight();
+ for (int i = size; --i > maxTopHeader; ) {
+ if (mHeaderVisibility[i]) {
+ int position = listView.getPositionAt(listHeight - bottomHeaderHeight) - headerViewsCount;
+ if (position < 0) {
+ break;
+ }
+
+ int partition = getPartitionForPosition(position - 1);
+ if (partition == -1 || i <= partition) {
+ break;
+ }
+
+ int height = listView.getPinnedHeaderHeight(i);
+ bottomHeaderHeight += height;
+
+ listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, false);
+ maxBottomHeader = i;
+ }
+ }
+
+ // Headers in between the top-pinned and bottom-pinned should be hidden
+ for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) {
+ if (mHeaderVisibility[i]) {
+ listView.setHeaderInvisible(i, isPartitionEmpty(i));
+ }
+ }
+ }
+
+ @Override
+ public int getScrollPositionForHeader(int viewIndex) {
+ return getPositionForPartition(viewIndex);
+ }
+}
diff --git a/java/com/android/contacts/common/list/PinnedHeaderListView.java b/java/com/android/contacts/common/list/PinnedHeaderListView.java
new file mode 100644
index 000000000..33c68b68c
--- /dev/null
+++ b/java/com/android/contacts/common/list/PinnedHeaderListView.java
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ListAdapter;
+import com.android.dialer.util.ViewUtil;
+
+/**
+ * A ListView that maintains a header pinned at the top of the list. The pinned header can be pushed
+ * up and dissolved as needed.
+ */
+public class PinnedHeaderListView extends AutoScrollListView
+ implements OnScrollListener, OnItemSelectedListener {
+
+ private static final int MAX_ALPHA = 255;
+ private static final int TOP = 0;
+ private static final int BOTTOM = 1;
+ private static final int FADING = 2;
+ private static final int DEFAULT_ANIMATION_DURATION = 20;
+ private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100;
+ private PinnedHeaderAdapter mAdapter;
+ private int mSize;
+ private PinnedHeader[] mHeaders;
+ private RectF mBounds = new RectF();
+ private OnScrollListener mOnScrollListener;
+ private OnItemSelectedListener mOnItemSelectedListener;
+ private int mScrollState;
+ private boolean mScrollToSectionOnHeaderTouch = false;
+ private boolean mHeaderTouched = false;
+ private int mAnimationDuration = DEFAULT_ANIMATION_DURATION;
+ private boolean mAnimating;
+ private long mAnimationTargetTime;
+ private int mHeaderPaddingStart;
+ private int mHeaderWidth;
+
+ public PinnedHeaderListView(Context context) {
+ this(context, null, android.R.attr.listViewStyle);
+ }
+
+ public PinnedHeaderListView(Context context, AttributeSet attrs) {
+ this(context, attrs, android.R.attr.listViewStyle);
+ }
+
+ public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ super.setOnScrollListener(this);
+ super.setOnItemSelectedListener(this);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ super.onLayout(changed, l, t, r, b);
+ mHeaderPaddingStart = getPaddingStart();
+ mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd();
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ mAdapter = (PinnedHeaderAdapter) adapter;
+ super.setAdapter(adapter);
+ }
+
+ @Override
+ public void setOnScrollListener(OnScrollListener onScrollListener) {
+ mOnScrollListener = onScrollListener;
+ super.setOnScrollListener(this);
+ }
+
+ @Override
+ public void setOnItemSelectedListener(OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ super.setOnItemSelectedListener(this);
+ }
+
+ public void setScrollToSectionOnHeaderTouch(boolean value) {
+ mScrollToSectionOnHeaderTouch = value;
+ }
+
+ @Override
+ public void onScroll(
+ AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (mAdapter != null) {
+ int count = mAdapter.getPinnedHeaderCount();
+ if (count != mSize) {
+ mSize = count;
+ if (mHeaders == null) {
+ mHeaders = new PinnedHeader[mSize];
+ } else if (mHeaders.length < mSize) {
+ PinnedHeader[] headers = mHeaders;
+ mHeaders = new PinnedHeader[mSize];
+ System.arraycopy(headers, 0, mHeaders, 0, headers.length);
+ }
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ if (mHeaders[i] == null) {
+ mHeaders[i] = new PinnedHeader();
+ }
+ mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this);
+ }
+
+ mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration;
+ mAdapter.configurePinnedHeaders(this);
+ invalidateIfAnimating();
+ }
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount);
+ }
+ }
+
+ @Override
+ protected float getTopFadingEdgeStrength() {
+ // Disable vertical fading at the top when the pinned header is present
+ return mSize > 0 ? 0 : super.getTopFadingEdgeStrength();
+ }
+
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ mScrollState = scrollState;
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(this, scrollState);
+ }
+ }
+
+ /**
+ * Ensures that the selected item is positioned below the top-pinned headers and above the
+ * bottom-pinned ones.
+ */
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int height = getHeight();
+
+ int windowTop = 0;
+ int windowBottom = height;
+
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ if (header.state == TOP) {
+ windowTop = header.y + header.height;
+ } else if (header.state == BOTTOM) {
+ windowBottom = header.y;
+ break;
+ }
+ }
+ }
+
+ View selectedView = getSelectedView();
+ if (selectedView != null) {
+ if (selectedView.getTop() < windowTop) {
+ setSelectionFromTop(position, windowTop);
+ } else if (selectedView.getBottom() > windowBottom) {
+ setSelectionFromTop(position, windowBottom - selectedView.getHeight());
+ }
+ }
+
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(parent, view, position, id);
+ }
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onNothingSelected(parent);
+ }
+ }
+
+ public int getPinnedHeaderHeight(int viewIndex) {
+ ensurePinnedHeaderLayout(viewIndex);
+ return mHeaders[viewIndex].view.getHeight();
+ }
+
+ /**
+ * Set header to be pinned at the top.
+ *
+ * @param viewIndex index of the header view
+ * @param y is position of the header in pixels.
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) {
+ ensurePinnedHeaderLayout(viewIndex);
+ PinnedHeader header = mHeaders[viewIndex];
+ header.visible = true;
+ header.y = y;
+ header.state = TOP;
+
+ // TODO perhaps we should animate at the top as well
+ header.animating = false;
+ }
+
+ /**
+ * Set header to be pinned at the bottom.
+ *
+ * @param viewIndex index of the header view
+ * @param y is position of the header in pixels.
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) {
+ ensurePinnedHeaderLayout(viewIndex);
+ PinnedHeader header = mHeaders[viewIndex];
+ header.state = BOTTOM;
+ if (header.animating) {
+ header.targetTime = mAnimationTargetTime;
+ header.sourceY = header.y;
+ header.targetY = y;
+ } else if (animate && (header.y != y || !header.visible)) {
+ if (header.visible) {
+ header.sourceY = header.y;
+ } else {
+ header.visible = true;
+ header.sourceY = y + header.height;
+ }
+ header.animating = true;
+ header.targetVisible = true;
+ header.targetTime = mAnimationTargetTime;
+ header.targetY = y;
+ } else {
+ header.visible = true;
+ header.y = y;
+ }
+ }
+
+ /**
+ * Set header to be pinned at the top of the first visible item.
+ *
+ * @param viewIndex index of the header view
+ * @param position is position of the header in pixels.
+ */
+ public void setFadingHeader(int viewIndex, int position, boolean fade) {
+ ensurePinnedHeaderLayout(viewIndex);
+
+ View child = getChildAt(position - getFirstVisiblePosition());
+ if (child == null) {
+ return;
+ }
+
+ PinnedHeader header = mHeaders[viewIndex];
+ header.visible = true;
+ header.state = FADING;
+ header.alpha = MAX_ALPHA;
+ header.animating = false;
+
+ int top = getTotalTopPinnedHeaderHeight();
+ header.y = top;
+ if (fade) {
+ int bottom = child.getBottom() - top;
+ int headerHeight = header.height;
+ if (bottom < headerHeight) {
+ int portion = bottom - headerHeight;
+ header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight;
+ header.y = top + portion;
+ }
+ }
+ }
+
+ /**
+ * Makes header invisible.
+ *
+ * @param viewIndex index of the header view
+ * @param animate true if the transition to the new coordinate should be animated
+ */
+ public void setHeaderInvisible(int viewIndex, boolean animate) {
+ PinnedHeader header = mHeaders[viewIndex];
+ if (header.visible && (animate || header.animating) && header.state == BOTTOM) {
+ header.sourceY = header.y;
+ if (!header.animating) {
+ header.visible = true;
+ header.targetY = getBottom() + header.height;
+ }
+ header.animating = true;
+ header.targetTime = mAnimationTargetTime;
+ header.targetVisible = false;
+ } else {
+ header.visible = false;
+ }
+ }
+
+ private void ensurePinnedHeaderLayout(int viewIndex) {
+ View view = mHeaders[viewIndex].view;
+ if (view.isLayoutRequested()) {
+ ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
+ int widthSpec;
+ int heightSpec;
+
+ if (layoutParams != null && layoutParams.width > 0) {
+ widthSpec = View.MeasureSpec.makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY);
+ } else {
+ widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY);
+ }
+
+ if (layoutParams != null && layoutParams.height > 0) {
+ heightSpec =
+ View.MeasureSpec.makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY);
+ } else {
+ heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ }
+ view.measure(widthSpec, heightSpec);
+ int height = view.getMeasuredHeight();
+ mHeaders[viewIndex].height = height;
+ view.layout(0, 0, view.getMeasuredWidth(), height);
+ }
+ }
+
+ /** Returns the sum of heights of headers pinned to the top. */
+ public int getTotalTopPinnedHeaderHeight() {
+ for (int i = mSize; --i >= 0; ) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.state == TOP) {
+ return header.y + header.height;
+ }
+ }
+ return 0;
+ }
+
+ /** Returns the list item position at the specified y coordinate. */
+ public int getPositionAt(int y) {
+ do {
+ int position = pointToPosition(getPaddingLeft() + 1, y);
+ if (position != -1) {
+ return position;
+ }
+ // If position == -1, we must have hit a separator. Let's examine
+ // a nearby pixel
+ y--;
+ } while (y > 0);
+ return 0;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mHeaderTouched = false;
+ if (super.onInterceptTouchEvent(ev)) {
+ return true;
+ }
+
+ if (mScrollState == SCROLL_STATE_IDLE) {
+ final int y = (int) ev.getY();
+ final int x = (int) ev.getX();
+ for (int i = mSize; --i >= 0; ) {
+ PinnedHeader header = mHeaders[i];
+ // For RTL layouts, this also takes into account that the scrollbar is on the left
+ // side.
+ final int padding = getPaddingLeft();
+ if (header.visible
+ && header.y <= y
+ && header.y + header.height > y
+ && x >= padding
+ && padding + header.view.getWidth() >= x) {
+ mHeaderTouched = true;
+ if (mScrollToSectionOnHeaderTouch && ev.getAction() == MotionEvent.ACTION_DOWN) {
+ return smoothScrollToPartition(i);
+ } else {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mHeaderTouched) {
+ if (ev.getAction() == MotionEvent.ACTION_UP) {
+ mHeaderTouched = false;
+ }
+ return true;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ private boolean smoothScrollToPartition(int partition) {
+ if (mAdapter == null) {
+ return false;
+ }
+ final int position = mAdapter.getScrollPositionForHeader(partition);
+ if (position == -1) {
+ return false;
+ }
+
+ int offset = 0;
+ for (int i = 0; i < partition; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ offset += header.height;
+ }
+ }
+ smoothScrollToPositionFromTop(
+ position + getHeaderViewsCount(), offset, DEFAULT_SMOOTH_SCROLL_DURATION);
+ return true;
+ }
+
+ private void invalidateIfAnimating() {
+ mAnimating = false;
+ for (int i = 0; i < mSize; i++) {
+ if (mHeaders[i].animating) {
+ mAnimating = true;
+ invalidate();
+ return;
+ }
+ }
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ long currentTime = mAnimating ? System.currentTimeMillis() : 0;
+
+ int top = 0;
+ int bottom = getBottom();
+ boolean hasVisibleHeaders = false;
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible) {
+ hasVisibleHeaders = true;
+ if (header.state == BOTTOM && header.y < bottom) {
+ bottom = header.y;
+ } else if (header.state == TOP || header.state == FADING) {
+ int newTop = header.y + header.height;
+ if (newTop > top) {
+ top = newTop;
+ }
+ }
+ }
+ }
+
+ if (hasVisibleHeaders) {
+ canvas.save();
+ }
+
+ super.dispatchDraw(canvas);
+
+ if (hasVisibleHeaders) {
+ canvas.restore();
+
+ // If the first item is visible and if it has a positive top that is greater than the
+ // first header's assigned y-value, use that for the first header's y value. This way,
+ // the header inherits any padding applied to the list view.
+ if (mSize > 0 && getFirstVisiblePosition() == 0) {
+ View firstChild = getChildAt(0);
+ PinnedHeader firstHeader = mHeaders[0];
+
+ if (firstHeader != null) {
+ int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0;
+ firstHeader.y = Math.max(firstHeader.y, firstHeaderTop);
+ }
+ }
+
+ // First draw top headers, then the bottom ones to handle the Z axis correctly
+ for (int i = mSize; --i >= 0; ) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && (header.state == TOP || header.state == FADING)) {
+ drawHeader(canvas, header, currentTime);
+ }
+ }
+
+ for (int i = 0; i < mSize; i++) {
+ PinnedHeader header = mHeaders[i];
+ if (header.visible && header.state == BOTTOM) {
+ drawHeader(canvas, header, currentTime);
+ }
+ }
+ }
+
+ invalidateIfAnimating();
+ }
+
+ private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) {
+ if (header.animating) {
+ int timeLeft = (int) (header.targetTime - currentTime);
+ if (timeLeft <= 0) {
+ header.y = header.targetY;
+ header.visible = header.targetVisible;
+ header.animating = false;
+ } else {
+ header.y =
+ header.targetY + (header.sourceY - header.targetY) * timeLeft / mAnimationDuration;
+ }
+ }
+ if (header.visible) {
+ View view = header.view;
+ int saveCount = canvas.save();
+ int translateX =
+ ViewUtil.isViewLayoutRtl(this)
+ ? getWidth() - mHeaderPaddingStart - view.getWidth()
+ : mHeaderPaddingStart;
+ canvas.translate(translateX, header.y);
+ if (header.state == FADING) {
+ mBounds.set(0, 0, view.getWidth(), view.getHeight());
+ canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG);
+ }
+ view.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ /** Adapter interface. The list adapter must implement this interface. */
+ public interface PinnedHeaderAdapter {
+
+ /** Returns the overall number of pinned headers, visible or not. */
+ int getPinnedHeaderCount();
+
+ /** Creates or updates the pinned header view. */
+ View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent);
+
+ /**
+ * Configures the pinned headers to match the visible list items. The adapter should call {@link
+ * PinnedHeaderListView#setHeaderPinnedAtTop}, {@link
+ * PinnedHeaderListView#setHeaderPinnedAtBottom}, {@link PinnedHeaderListView#setFadingHeader}
+ * or {@link PinnedHeaderListView#setHeaderInvisible}, for each header that needs to change its
+ * position or visibility.
+ */
+ void configurePinnedHeaders(PinnedHeaderListView listView);
+
+ /**
+ * Returns the list position to scroll to if the pinned header is touched. Return -1 if the list
+ * does not need to be scrolled.
+ */
+ int getScrollPositionForHeader(int viewIndex);
+ }
+
+ private static final class PinnedHeader {
+
+ View view;
+ boolean visible;
+ int y;
+ int height;
+ int alpha;
+ int state;
+
+ boolean animating;
+ boolean targetVisible;
+ int sourceY;
+ int targetY;
+ long targetTime;
+ }
+}
diff --git a/java/com/android/contacts/common/list/ViewPagerTabStrip.java b/java/com/android/contacts/common/list/ViewPagerTabStrip.java
new file mode 100644
index 000000000..969a6d342
--- /dev/null
+++ b/java/com/android/contacts/common/list/ViewPagerTabStrip.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.android.contacts.common.R;
+
+public class ViewPagerTabStrip extends LinearLayout {
+
+ private final Paint mSelectedUnderlinePaint;
+ private int mSelectedUnderlineThickness;
+ private int mIndexForSelection;
+ private float mSelectionOffset;
+
+ public ViewPagerTabStrip(Context context) {
+ this(context, null);
+ }
+
+ public ViewPagerTabStrip(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final Resources res = context.getResources();
+
+ mSelectedUnderlineThickness = res.getDimensionPixelSize(R.dimen.tab_selected_underline_height);
+ int underlineColor = res.getColor(R.color.tab_selected_underline_color);
+ int backgroundColor = res.getColor(R.color.contactscommon_actionbar_background_color);
+
+ mSelectedUnderlinePaint = new Paint();
+ mSelectedUnderlinePaint.setColor(underlineColor);
+
+ setBackgroundColor(backgroundColor);
+ setWillNotDraw(false);
+ }
+
+ /**
+ * Notifies this view that view pager has been scrolled. We save the tab index and selection
+ * offset for interpolating the position and width of selection underline.
+ */
+ void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ mIndexForSelection = position;
+ mSelectionOffset = positionOffset;
+ invalidate();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int childCount = getChildCount();
+
+ // Thick colored underline below the current selection
+ if (childCount > 0) {
+ View selectedTitle = getChildAt(mIndexForSelection);
+
+ if (selectedTitle == null) {
+ // The view pager's tab count changed but we weren't notified yet. Ignore this draw
+ // pass, when we get a new selection we will update and draw the selection strip in
+ // the correct place.
+ return;
+ }
+ int selectedLeft = selectedTitle.getLeft();
+ int selectedRight = selectedTitle.getRight();
+ final boolean isRtl = isRtl();
+ final boolean hasNextTab =
+ isRtl ? mIndexForSelection > 0 : (mIndexForSelection < (getChildCount() - 1));
+ if ((mSelectionOffset > 0.0f) && hasNextTab) {
+ // Draw the selection partway between the tabs
+ View nextTitle = getChildAt(mIndexForSelection + (isRtl ? -1 : 1));
+ int nextLeft = nextTitle.getLeft();
+ int nextRight = nextTitle.getRight();
+
+ selectedLeft =
+ (int) (mSelectionOffset * nextLeft + (1.0f - mSelectionOffset) * selectedLeft);
+ selectedRight =
+ (int) (mSelectionOffset * nextRight + (1.0f - mSelectionOffset) * selectedRight);
+ }
+
+ int height = getHeight();
+ canvas.drawRect(
+ selectedLeft,
+ height - mSelectedUnderlineThickness,
+ selectedRight,
+ height,
+ mSelectedUnderlinePaint);
+ }
+ }
+
+ private boolean isRtl() {
+ return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+}
diff --git a/java/com/android/contacts/common/list/ViewPagerTabs.java b/java/com/android/contacts/common/list/ViewPagerTabs.java
new file mode 100644
index 000000000..34f623ef4
--- /dev/null
+++ b/java/com/android/contacts/common/list/ViewPagerTabs.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.list;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Outline;
+import android.support.v4.view.PagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.FrameLayout;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.contacts.common.R;
+import com.android.dialer.compat.CompatUtils;
+
+/**
+ * Lightweight implementation of ViewPager tabs. This looks similar to traditional actionBar tabs,
+ * but allows for the view containing the tabs to be placed anywhere on screen. Text-related
+ * attributes can also be assigned in XML - these will get propogated to the child TextViews
+ * automatically.
+ */
+public class ViewPagerTabs extends HorizontalScrollView implements ViewPager.OnPageChangeListener {
+
+ private static final ViewOutlineProvider VIEW_BOUNDS_OUTLINE_PROVIDER;
+ private static final int TAB_SIDE_PADDING_IN_DPS = 10;
+ // TODO: This should use <declare-styleable> in the future
+ private static final int[] ATTRS =
+ new int[] {
+ android.R.attr.textSize,
+ android.R.attr.textStyle,
+ android.R.attr.textColor,
+ android.R.attr.textAllCaps
+ };
+
+ static {
+ if (CompatUtils.isLollipopCompatible()) {
+ VIEW_BOUNDS_OUTLINE_PROVIDER =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setRect(0, 0, view.getWidth(), view.getHeight());
+ }
+ };
+ } else {
+ VIEW_BOUNDS_OUTLINE_PROVIDER = null;
+ }
+ }
+
+ /**
+ * Linearlayout that will contain the TextViews serving as tabs. This is the only child of the
+ * parent HorizontalScrollView.
+ */
+ final int mTextStyle;
+
+ final ColorStateList mTextColor;
+ final int mTextSize;
+ final boolean mTextAllCaps;
+ ViewPager mPager;
+ int mPrevSelected = -1;
+ int mSidePadding;
+ private ViewPagerTabStrip mTabStrip;
+ private int[] mTabIcons;
+ // For displaying the unread count next to the tab icon.
+ private int[] mUnreadCounts;
+
+ public ViewPagerTabs(Context context) {
+ this(context, null);
+ }
+
+ public ViewPagerTabs(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ViewPagerTabs(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setFillViewport(true);
+
+ mSidePadding = (int) (getResources().getDisplayMetrics().density * TAB_SIDE_PADDING_IN_DPS);
+
+ final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS);
+ mTextSize = a.getDimensionPixelSize(0, 0);
+ mTextStyle = a.getInt(1, 0);
+ mTextColor = a.getColorStateList(2);
+ mTextAllCaps = a.getBoolean(3, false);
+
+ mTabStrip = new ViewPagerTabStrip(context);
+ addView(
+ mTabStrip,
+ new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+ a.recycle();
+
+ if (CompatUtils.isLollipopCompatible()) {
+ // enable shadow casting from view bounds
+ setOutlineProvider(VIEW_BOUNDS_OUTLINE_PROVIDER);
+ }
+ }
+
+ public void setViewPager(ViewPager viewPager) {
+ mPager = viewPager;
+ addTabs(mPager.getAdapter());
+ }
+
+ /**
+ * Set the tab icons and initialize an array for unread counts the same length as the icon array.
+ *
+ * @param tabIcons An array representing the tab icons in order.
+ */
+ public void configureTabIcons(int[] tabIcons) {
+ mTabIcons = tabIcons;
+ mUnreadCounts = new int[tabIcons.length];
+ }
+
+ public void setUnreadCount(int count, int position) {
+ if (mUnreadCounts == null || position >= mUnreadCounts.length) {
+ return;
+ }
+ mUnreadCounts[position] = count;
+ }
+
+ private void addTabs(PagerAdapter adapter) {
+ mTabStrip.removeAllViews();
+
+ final int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ addTab(adapter.getPageTitle(i), i);
+ }
+ }
+
+ private void addTab(CharSequence tabTitle, final int position) {
+ View tabView;
+ if (mTabIcons != null && position < mTabIcons.length) {
+ View layout = LayoutInflater.from(getContext()).inflate(R.layout.unread_count_tab, null);
+ View iconView = layout.findViewById(R.id.icon);
+ iconView.setBackgroundResource(mTabIcons[position]);
+ iconView.setContentDescription(tabTitle);
+ TextView textView = (TextView) layout.findViewById(R.id.count);
+ if (mUnreadCounts != null && mUnreadCounts[position] > 0) {
+ textView.setText(Integer.toString(mUnreadCounts[position]));
+ textView.setVisibility(View.VISIBLE);
+ iconView.setContentDescription(
+ getResources()
+ .getQuantityString(
+ R.plurals.tab_title_with_unread_items,
+ mUnreadCounts[position],
+ tabTitle.toString(),
+ mUnreadCounts[position]));
+ } else {
+ textView.setVisibility(View.INVISIBLE);
+ iconView.setContentDescription(getResources().getString(R.string.tab_title, tabTitle));
+ }
+ tabView = layout;
+ } else {
+ final TextView textView = new TextView(getContext());
+ textView.setText(tabTitle);
+ textView.setBackgroundResource(R.drawable.view_pager_tab_background);
+
+ // Assign various text appearance related attributes to child views.
+ if (mTextStyle > 0) {
+ textView.setTypeface(textView.getTypeface(), mTextStyle);
+ }
+ if (mTextSize > 0) {
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+ }
+ if (mTextColor != null) {
+ textView.setTextColor(mTextColor);
+ }
+ textView.setAllCaps(mTextAllCaps);
+ textView.setGravity(Gravity.CENTER);
+
+ tabView = textView;
+ }
+
+ tabView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mPager.setCurrentItem(getRtlPosition(position));
+ }
+ });
+
+ tabView.setOnLongClickListener(new OnTabLongClickListener(position));
+
+ tabView.setPadding(mSidePadding, 0, mSidePadding, 0);
+
+ mTabStrip.addView(
+ tabView,
+ position,
+ new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT, 1));
+
+ // Default to the first child being selected
+ if (position == 0) {
+ mPrevSelected = 0;
+ tabView.setSelected(true);
+ }
+ }
+
+ /**
+ * Remove a tab at a certain index.
+ *
+ * @param index The index of the tab view we wish to remove.
+ */
+ public void removeTab(int index) {
+ View view = mTabStrip.getChildAt(index);
+ if (view != null) {
+ mTabStrip.removeView(view);
+ }
+ }
+
+ /**
+ * Refresh a tab at a certain index by removing it and reconstructing it.
+ *
+ * @param index The index of the tab view we wish to update.
+ */
+ public void updateTab(int index) {
+ removeTab(index);
+
+ if (index < mPager.getAdapter().getCount()) {
+ addTab(mPager.getAdapter().getPageTitle(index), index);
+ }
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ position = getRtlPosition(position);
+ int tabStripChildCount = mTabStrip.getChildCount();
+ if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+ return;
+ }
+
+ mTabStrip.onPageScrolled(position, positionOffset, positionOffsetPixels);
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ position = getRtlPosition(position);
+ int tabStripChildCount = mTabStrip.getChildCount();
+ if ((tabStripChildCount == 0) || (position < 0) || (position >= tabStripChildCount)) {
+ return;
+ }
+
+ if (mPrevSelected >= 0 && mPrevSelected < tabStripChildCount) {
+ mTabStrip.getChildAt(mPrevSelected).setSelected(false);
+ }
+ final View selectedChild = mTabStrip.getChildAt(position);
+ selectedChild.setSelected(true);
+
+ // Update scroll position
+ final int scrollPos = selectedChild.getLeft() - (getWidth() - selectedChild.getWidth()) / 2;
+ smoothScrollTo(scrollPos, 0);
+ mPrevSelected = position;
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ private int getRtlPosition(int position) {
+ if (getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ return mTabStrip.getChildCount() - 1 - position;
+ }
+ return position;
+ }
+
+ /** Simulates actionbar tab behavior by showing a toast with the tab title when long clicked. */
+ private class OnTabLongClickListener implements OnLongClickListener {
+
+ final int mPosition;
+
+ public OnTabLongClickListener(int position) {
+ mPosition = position;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ final int[] screenPos = new int[2];
+ getLocationOnScreen(screenPos);
+
+ final Context context = getContext();
+ final int width = getWidth();
+ final int height = getHeight();
+ final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+
+ Toast toast =
+ Toast.makeText(context, mPager.getAdapter().getPageTitle(mPosition), Toast.LENGTH_SHORT);
+
+ // Show the toast under the tab
+ toast.setGravity(
+ Gravity.TOP | Gravity.CENTER_HORIZONTAL,
+ (screenPos[0] + width / 2) - screenWidth / 2,
+ screenPos[1] + height);
+
+ toast.show();
+ return true;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/location/CountryDetector.java b/java/com/android/contacts/common/location/CountryDetector.java
new file mode 100644
index 000000000..7d9e42b38
--- /dev/null
+++ b/java/com/android/contacts/common/location/CountryDetector.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.location;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.preference.PreferenceManager;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.Locale;
+
+/**
+ * This class is used to detect the country where the user is. It is a simplified version of the
+ * country detector service in the framework. The sources of country location are queried in the
+ * following order of reliability:
+ *
+ * <ul>
+ * <li>Mobile network
+ * <li>Location manager
+ * <li>SIM's country
+ * <li>User's default locale
+ * </ul>
+ *
+ * As far as possible this class tries to replicate the behavior of the system's country detector
+ * service: 1) Order in priority of sources of country location 2) Mobile network information
+ * provided by CDMA phones is ignored 3) Location information is updated every 12 hours (instead of
+ * 24 hours in the system) 4) Location updates only uses the {@link
+ * LocationManager#PASSIVE_PROVIDER} to avoid active use of the GPS 5) If a location is successfully
+ * obtained and geocoded, we never fall back to use of the SIM's country (for the system, the
+ * fallback never happens without a reboot) 6) Location is not used if the device does not implement
+ * a {@link android.location.Geocoder}
+ */
+public class CountryDetector {
+
+ public static final String KEY_PREFERENCE_TIME_UPDATED = "preference_time_updated";
+ public static final String KEY_PREFERENCE_CURRENT_COUNTRY = "preference_current_country";
+ private static final String TAG = "CountryDetector";
+ // Wait 12 hours between updates
+ private static final long TIME_BETWEEN_UPDATES_MS = 1000L * 60 * 60 * 12;
+ // Minimum distance before an update is triggered, in meters. We don't need this to be too
+ // exact because all we care about is what country the user is in.
+ private static final long DISTANCE_BETWEEN_UPDATES_METERS = 5000;
+ private static CountryDetector sInstance;
+ private final TelephonyManager mTelephonyManager;
+ private final LocationManager mLocationManager;
+ private final LocaleProvider mLocaleProvider;
+ // Used as a default country code when all the sources of country data have failed in the
+ // exceedingly rare event that the device does not have a default locale set for some reason.
+ private static final String DEFAULT_COUNTRY_ISO = "US";
+ private final Context mContext;
+
+ private CountryDetector(Context context) {
+ this(
+ context,
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE),
+ (LocationManager) context.getSystemService(Context.LOCATION_SERVICE),
+ new LocaleProvider());
+ }
+
+ private CountryDetector(
+ Context context,
+ TelephonyManager telephonyManager,
+ LocationManager locationManager,
+ LocaleProvider localeProvider) {
+ mTelephonyManager = telephonyManager;
+ mLocationManager = locationManager;
+ mLocaleProvider = localeProvider;
+ mContext = context;
+
+ registerForLocationUpdates(context, mLocationManager);
+ }
+
+ public static void registerForLocationUpdates(Context context, LocationManager locationManager) {
+ if (!PermissionsUtil.hasLocationPermissions(context)) {
+ Log.w(TAG, "No location permissions, not registering for location updates.");
+ return;
+ }
+
+ if (!Geocoder.isPresent()) {
+ // Certain devices do not have an implementation of a geocoder - in that case there is
+ // no point trying to get location updates because we cannot retrieve the country based
+ // on the location anyway.
+ return;
+ }
+ final Intent activeIntent = new Intent(context, LocationChangedReceiver.class);
+ final PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(context, 0, activeIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ locationManager.requestLocationUpdates(
+ LocationManager.PASSIVE_PROVIDER,
+ TIME_BETWEEN_UPDATES_MS,
+ DISTANCE_BETWEEN_UPDATES_METERS,
+ pendingIntent);
+ }
+
+ /**
+ * Returns the instance of the country detector. {@link #initialize(Context)} must have been
+ * called previously.
+ *
+ * @return the initialized country detector.
+ */
+ public static synchronized CountryDetector getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new CountryDetector(context.getApplicationContext());
+ }
+ return sInstance;
+ }
+
+ /** Factory method for {@link CountryDetector} that allows the caller to provide mock objects. */
+ public CountryDetector getInstanceForTest(
+ Context context,
+ TelephonyManager telephonyManager,
+ LocationManager locationManager,
+ LocaleProvider localeProvider,
+ Geocoder geocoder) {
+ return new CountryDetector(context, telephonyManager, locationManager, localeProvider);
+ }
+
+ public String getCurrentCountryIso() {
+ String result = null;
+ if (isNetworkCountryCodeAvailable()) {
+ result = getNetworkBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = getLocationBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = getSimBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = getLocaleBasedCountryIso();
+ }
+ if (TextUtils.isEmpty(result)) {
+ result = DEFAULT_COUNTRY_ISO;
+ }
+ return result.toUpperCase(Locale.US);
+ }
+
+ /** @return the country code of the current telephony network the user is connected to. */
+ private String getNetworkBasedCountryIso() {
+ return mTelephonyManager.getNetworkCountryIso();
+ }
+
+ /** @return the geocoded country code detected by the {@link LocationManager}. */
+ private String getLocationBasedCountryIso() {
+ if (!Geocoder.isPresent() || !PermissionsUtil.hasLocationPermissions(mContext)) {
+ return null;
+ }
+ final SharedPreferences sharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(mContext);
+ return sharedPreferences.getString(KEY_PREFERENCE_CURRENT_COUNTRY, null);
+ }
+
+ /** @return the country code of the SIM card currently inserted in the device. */
+ private String getSimBasedCountryIso() {
+ return mTelephonyManager.getSimCountryIso();
+ }
+
+ /** @return the country code of the user's currently selected locale. */
+ private String getLocaleBasedCountryIso() {
+ Locale defaultLocale = mLocaleProvider.getDefaultLocale();
+ if (defaultLocale != null) {
+ return defaultLocale.getCountry();
+ }
+ return null;
+ }
+
+ private boolean isNetworkCountryCodeAvailable() {
+ // On CDMA TelephonyManager.getNetworkCountryIso() just returns the SIM's country code.
+ // In this case, we want to ignore the value returned and fallback to location instead.
+ return mTelephonyManager.getPhoneType() == TelephonyManager.PHONE_TYPE_GSM;
+ }
+
+ /**
+ * Class that can be used to return the user's default locale. This is in its own class so that it
+ * can be mocked out.
+ */
+ public static class LocaleProvider {
+
+ public Locale getDefaultLocale() {
+ return Locale.getDefault();
+ }
+ }
+
+ public static class LocationChangedReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (!intent.hasExtra(LocationManager.KEY_LOCATION_CHANGED)) {
+ return;
+ }
+
+ final Location location =
+ (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED);
+
+ UpdateCountryService.updateCountry(context, location);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/location/UpdateCountryService.java b/java/com/android/contacts/common/location/UpdateCountryService.java
new file mode 100644
index 000000000..f23e09e20
--- /dev/null
+++ b/java/com/android/contacts/common/location/UpdateCountryService.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.location;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Service used to perform asynchronous geocoding from within a broadcast receiver. Given a {@link
+ * Location}, convert it into a country code, and save it in shared preferences.
+ */
+public class UpdateCountryService extends IntentService {
+
+ private static final String TAG = UpdateCountryService.class.getSimpleName();
+
+ private static final String ACTION_UPDATE_COUNTRY = "saveCountry";
+
+ private static final String KEY_INTENT_LOCATION = "location";
+
+ public UpdateCountryService() {
+ super(TAG);
+ }
+
+ public static void updateCountry(Context context, Location location) {
+ final Intent serviceIntent = new Intent(context, UpdateCountryService.class);
+ serviceIntent.setAction(ACTION_UPDATE_COUNTRY);
+ serviceIntent.putExtra(UpdateCountryService.KEY_INTENT_LOCATION, location);
+ context.startService(serviceIntent);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent == null) {
+ Log.d(TAG, "onHandleIntent: could not handle null intent");
+ return;
+ }
+ if (ACTION_UPDATE_COUNTRY.equals(intent.getAction())) {
+ final Location location = intent.getParcelableExtra(KEY_INTENT_LOCATION);
+ final String country = getCountryFromLocation(getApplicationContext(), location);
+
+ if (country == null) {
+ return;
+ }
+
+ final SharedPreferences prefs =
+ PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+
+ final Editor editor = prefs.edit();
+ editor.putLong(CountryDetector.KEY_PREFERENCE_TIME_UPDATED, System.currentTimeMillis());
+ editor.putString(CountryDetector.KEY_PREFERENCE_CURRENT_COUNTRY, country);
+ editor.commit();
+ }
+ }
+
+ /**
+ * Given a {@link Location}, return a country code.
+ *
+ * @return the ISO 3166-1 two letter country code
+ */
+ private String getCountryFromLocation(Context context, Location location) {
+ final Geocoder geocoder = new Geocoder(context);
+ String country = null;
+ try {
+ double latitude = location.getLatitude();
+ // Latitude has to be between 90 and -90 (latitude of north and south poles wrt equator)
+ if (latitude <= 90 && latitude >= -90) {
+ final List<Address> addresses =
+ geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1);
+ if (addresses != null && addresses.size() > 0) {
+ country = addresses.get(0).getCountryCode();
+ }
+ } else {
+ Log.w(TAG, "Invalid latitude");
+ }
+ } catch (IOException e) {
+ Log.w(TAG, "Exception occurred when getting geocoded country from location");
+ }
+ return country;
+ }
+}
diff --git a/java/com/android/contacts/common/model/AccountTypeManager.java b/java/com/android/contacts/common/model/AccountTypeManager.java
new file mode 100644
index 000000000..f225ff6ac
--- /dev/null
+++ b/java/com/android/contacts/common/model/AccountTypeManager.java
@@ -0,0 +1,813 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AuthenticatorDescription;
+import android.accounts.OnAccountsUpdateListener;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SyncAdapterType;
+import android.content.SyncStatusObserver;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.provider.ContactsContract;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.TimingLogger;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.list.ContactListFilterController;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.account.ExchangeAccountType;
+import com.android.contacts.common.model.account.ExternalAccountType;
+import com.android.contacts.common.model.account.FallbackAccountType;
+import com.android.contacts.common.model.account.GoogleAccountType;
+import com.android.contacts.common.model.account.SamsungAccountType;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.Constants;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Singleton holder for all parsed {@link AccountType} available on the system, typically filled
+ * through {@link PackageManager} queries.
+ */
+public abstract class AccountTypeManager {
+
+ static final String TAG = "AccountTypeManager";
+
+ private static final Object mInitializationLock = new Object();
+ private static AccountTypeManager mAccountTypeManager;
+
+ /**
+ * Requests the singleton instance of {@link AccountTypeManager} with data bound from the
+ * available authenticators. This method can safely be called from the UI thread.
+ */
+ public static AccountTypeManager getInstance(Context context) {
+ synchronized (mInitializationLock) {
+ if (mAccountTypeManager == null) {
+ context = context.getApplicationContext();
+ mAccountTypeManager = new AccountTypeManagerImpl(context);
+ }
+ }
+ return mAccountTypeManager;
+ }
+
+ /**
+ * Set the instance of account type manager. This is only for and should only be used by unit
+ * tests. While having this method is not ideal, it's simpler than the alternative of holding this
+ * as a service in the ContactsApplication context class.
+ *
+ * @param mockManager The mock AccountTypeManager.
+ */
+ public static void setInstanceForTest(AccountTypeManager mockManager) {
+ synchronized (mInitializationLock) {
+ mAccountTypeManager = mockManager;
+ }
+ }
+
+ /**
+ * Returns the list of all accounts (if contactWritableOnly is false) or just the list of contact
+ * writable accounts (if contactWritableOnly is true).
+ */
+ // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
+ public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
+
+ /** Returns the list of accounts that are group writable. */
+ public abstract List<AccountWithDataSet> getGroupWritableAccounts();
+
+ public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
+
+ public final AccountType getAccountType(String accountType, String dataSet) {
+ return getAccountType(AccountTypeWithDataSet.get(accountType, dataSet));
+ }
+
+ public final AccountType getAccountTypeForAccount(AccountWithDataSet account) {
+ if (account != null) {
+ return getAccountType(account.getAccountTypeWithDataSet());
+ }
+ return getAccountType(null, null);
+ }
+
+ /**
+ * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
+ * support the "invite" feature and have one or more account.
+ * <p>This is a filtered down and more "usable" list compared to {@link
+ * #getAllInvitableAccountTypes}, where usable is defined as: (1) making sure that the app
+ * that contributed the account type is not disabled (in order to avoid presenting the user
+ * with an option that does nothing), and (2) that there is at least one raw contact with that
+ * account type in the database (assuming that the user probably doesn't use that account
+ * type).
+ * <p>Warning: Don't use on the UI thread because this can scan the database.
+ */
+ public abstract Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes();
+
+ /**
+ * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
+ * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
+ * {@link FallbackAccountType}.
+ */
+ public DataKind getKindOrFallback(AccountType type, String mimeType) {
+ return type == null ? null : type.getKindForMimetype(mimeType);
+ }
+
+ /**
+ * Returns all registered {@link AccountType}s, including extension ones.
+ *
+ * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+ */
+ public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
+
+ /**
+ * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+ * @return true when this instance contains the given account.
+ */
+ public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
+ for (AccountWithDataSet account_2 : getAccounts(false)) {
+ if (account.equals(account_2)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
+
+class AccountTypeManagerImpl extends AccountTypeManager
+ implements OnAccountsUpdateListener, SyncStatusObserver {
+
+ private static final Map<AccountTypeWithDataSet, AccountType>
+ EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP =
+ Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
+
+ /**
+ * A sample contact URI used to test whether any activities will respond to an invitable intent
+ * with the given URI as the intent data. This doesn't need to be specific to a real contact
+ * because an app that intercepts the intent should probably do so for all types of contact URIs.
+ */
+ private static final Uri SAMPLE_CONTACT_URI = ContactsContract.Contacts.getLookupUri(1, "xxx");
+
+ private static final int MESSAGE_LOAD_DATA = 0;
+ private static final int MESSAGE_PROCESS_BROADCAST_INTENT = 1;
+ private static final Comparator<AccountWithDataSet> ACCOUNT_COMPARATOR =
+ new Comparator<AccountWithDataSet>() {
+ @Override
+ public int compare(AccountWithDataSet a, AccountWithDataSet b) {
+ if (Objects.equals(a.name, b.name)
+ && Objects.equals(a.type, b.type)
+ && Objects.equals(a.dataSet, b.dataSet)) {
+ return 0;
+ } else if (b.name == null || b.type == null) {
+ return -1;
+ } else if (a.name == null || a.type == null) {
+ return 1;
+ } else {
+ int diff = a.name.compareTo(b.name);
+ if (diff != 0) {
+ return diff;
+ }
+ diff = a.type.compareTo(b.type);
+ if (diff != 0) {
+ return diff;
+ }
+
+ // Accounts without data sets get sorted before those that have them.
+ if (a.dataSet != null) {
+ return b.dataSet == null ? 1 : a.dataSet.compareTo(b.dataSet);
+ } else {
+ return -1;
+ }
+ }
+ }
+ };
+ private final InvitableAccountTypeCache mInvitableAccountTypeCache;
+ /**
+ * The boolean value is equal to true if the {@link InvitableAccountTypeCache} has been
+ * initialized. False otherwise.
+ */
+ private final AtomicBoolean mInvitablesCacheIsInitialized = new AtomicBoolean(false);
+ /**
+ * The boolean value is equal to true if the {@link FindInvitablesTask} is still executing. False
+ * otherwise.
+ */
+ private final AtomicBoolean mInvitablesTaskIsRunning = new AtomicBoolean(false);
+
+ private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+ private Context mContext;
+ private final Runnable mCheckFilterValidityRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ ContactListFilterController.getInstance(mContext).checkFilterValidity(true);
+ }
+ };
+ private AccountManager mAccountManager;
+ private AccountType mFallbackAccountType;
+ private List<AccountWithDataSet> mAccounts = new ArrayList<>();
+ private List<AccountWithDataSet> mContactWritableAccounts = new ArrayList<>();
+ private List<AccountWithDataSet> mGroupWritableAccounts = new ArrayList<>();
+ private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = new ArrayMap<>();
+ private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
+ EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+ private HandlerThread mListenerThread;
+ private Handler mListenerHandler;
+ private BroadcastReceiver mBroadcastReceiver =
+ new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Message msg = mListenerHandler.obtainMessage(MESSAGE_PROCESS_BROADCAST_INTENT, intent);
+ mListenerHandler.sendMessage(msg);
+ }
+ };
+ /* A latch that ensures that asynchronous initialization completes before data is used */
+ private volatile CountDownLatch mInitializationLatch = new CountDownLatch(1);
+
+ /** Internal constructor that only performs initial parsing. */
+ public AccountTypeManagerImpl(Context context) {
+ mContext = context;
+ mFallbackAccountType = new FallbackAccountType(context);
+
+ mAccountManager = AccountManager.get(mContext);
+
+ mListenerThread = new HandlerThread("AccountChangeListener");
+ mListenerThread.start();
+ mListenerHandler =
+ new Handler(mListenerThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_LOAD_DATA:
+ loadAccountsInBackground();
+ break;
+ case MESSAGE_PROCESS_BROADCAST_INTENT:
+ processBroadcastIntent((Intent) msg.obj);
+ break;
+ }
+ }
+ };
+
+ mInvitableAccountTypeCache = new InvitableAccountTypeCache();
+
+ // Request updates when packages or accounts change
+ IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+ IntentFilter sdFilter = new IntentFilter();
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+ sdFilter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+ mContext.registerReceiver(mBroadcastReceiver, sdFilter);
+
+ // Request updates when locale is changed so that the order of each field will
+ // be able to be changed on the locale change.
+ filter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(mBroadcastReceiver, filter);
+
+ mAccountManager.addOnAccountsUpdatedListener(this, mListenerHandler, false);
+
+ ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this);
+
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ /**
+ * Find a specific {@link AuthenticatorDescription} in the provided list that matches the given
+ * account type.
+ */
+ protected static AuthenticatorDescription findAuthenticator(
+ AuthenticatorDescription[] auths, String accountType) {
+ for (AuthenticatorDescription auth : auths) {
+ if (accountType.equals(auth.type)) {
+ return auth;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return all {@link AccountType}s with at least one account which supports "invite", i.e. its
+ * {@link AccountType#getInviteContactActivityClassName()} is not empty.
+ */
+ @VisibleForTesting
+ static Map<AccountTypeWithDataSet, AccountType> findAllInvitableAccountTypes(
+ Context context,
+ Collection<AccountWithDataSet> accounts,
+ Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet) {
+ Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
+ for (AccountWithDataSet account : accounts) {
+ AccountTypeWithDataSet accountTypeWithDataSet = account.getAccountTypeWithDataSet();
+ AccountType type = accountTypesByTypeAndDataSet.get(accountTypeWithDataSet);
+ if (type == null) {
+ continue; // just in case
+ }
+ if (result.containsKey(accountTypeWithDataSet)) {
+ continue;
+ }
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(
+ TAG,
+ "Type "
+ + accountTypeWithDataSet
+ + " inviteClass="
+ + type.getInviteContactActivityClassName());
+ }
+ if (!TextUtils.isEmpty(type.getInviteContactActivityClassName())) {
+ result.put(accountTypeWithDataSet, type);
+ }
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public void onStatusChanged(int which) {
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ public void processBroadcastIntent(Intent intent) {
+ mListenerHandler.sendEmptyMessage(MESSAGE_LOAD_DATA);
+ }
+
+ /* This notification will arrive on the background thread */
+ public void onAccountsUpdated(Account[] accounts) {
+ // Refresh to catch any changed accounts
+ loadAccountsInBackground();
+ }
+
+ /**
+ * Returns instantly if accounts and account types have already been loaded. Otherwise waits for
+ * the background thread to complete the loading.
+ */
+ void ensureAccountsLoaded() {
+ CountDownLatch latch = mInitializationLatch;
+ if (latch == null) {
+ return;
+ }
+ while (true) {
+ try {
+ latch.await();
+ return;
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * Loads account list and corresponding account types (potentially with data sets). Always called
+ * on a background thread.
+ */
+ protected void loadAccountsInBackground() {
+ if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+ Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground start");
+ }
+ TimingLogger timings = new TimingLogger(TAG, "loadAccountsInBackground");
+ final long startTime = SystemClock.currentThreadTimeMillis();
+ final long startTimeWall = SystemClock.elapsedRealtime();
+
+ // Account types, keyed off the account type and data set concatenation.
+ final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = new ArrayMap<>();
+
+ // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}. Since there can
+ // be multiple account types (with different data sets) for the same type of account, each
+ // type string may have multiple AccountType entries.
+ final Map<String, List<AccountType>> accountTypesByType = new ArrayMap<>();
+
+ final List<AccountWithDataSet> allAccounts = new ArrayList<>();
+ final List<AccountWithDataSet> contactWritableAccounts = new ArrayList<>();
+ final List<AccountWithDataSet> groupWritableAccounts = new ArrayList<>();
+ final Set<String> extensionPackages = new HashSet<>();
+
+ final AccountManager am = mAccountManager;
+
+ final SyncAdapterType[] syncs = ContentResolver.getSyncAdapterTypes();
+ final AuthenticatorDescription[] auths = am.getAuthenticatorTypes();
+
+ // First process sync adapters to find any that provide contact data.
+ for (SyncAdapterType sync : syncs) {
+ if (!ContactsContract.AUTHORITY.equals(sync.authority)) {
+ // Skip sync adapters that don't provide contact data.
+ continue;
+ }
+
+ // Look for the formatting details provided by each sync
+ // adapter, using the authenticator to find general resources.
+ final String type = sync.accountType;
+ final AuthenticatorDescription auth = findAuthenticator(auths, type);
+ if (auth == null) {
+ Log.w(TAG, "No authenticator found for type=" + type + ", ignoring it.");
+ continue;
+ }
+
+ AccountType accountType;
+ if (GoogleAccountType.ACCOUNT_TYPE.equals(type)) {
+ accountType = new GoogleAccountType(mContext, auth.packageName);
+ } else if (ExchangeAccountType.isExchangeType(type)) {
+ accountType = new ExchangeAccountType(mContext, auth.packageName, type);
+ } else if (SamsungAccountType.isSamsungAccountType(mContext, type, auth.packageName)) {
+ accountType = new SamsungAccountType(mContext, auth.packageName, type);
+ } else {
+ Log.d(
+ TAG, "Registering external account type=" + type + ", packageName=" + auth.packageName);
+ accountType = new ExternalAccountType(mContext, auth.packageName, false);
+ }
+ if (!accountType.isInitialized()) {
+ if (accountType.isEmbedded()) {
+ throw new IllegalStateException(
+ "Problem initializing embedded type " + accountType.getClass().getCanonicalName());
+ } else {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ }
+
+ accountType.accountType = auth.type;
+ accountType.titleRes = auth.labelId;
+ accountType.iconRes = auth.iconId;
+
+ addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+
+ // Check to see if the account type knows of any other non-sync-adapter packages
+ // that may provide other data sets of contact data.
+ extensionPackages.addAll(accountType.getExtensionPackageNames());
+ }
+
+ // If any extension packages were specified, process them as well.
+ if (!extensionPackages.isEmpty()) {
+ Log.d(TAG, "Registering " + extensionPackages.size() + " extension packages");
+ for (String extensionPackage : extensionPackages) {
+ ExternalAccountType accountType = new ExternalAccountType(mContext, extensionPackage, true);
+ if (!accountType.isInitialized()) {
+ // Skip external account types that couldn't be initialized.
+ continue;
+ }
+ if (!accountType.hasContactsMetadata()) {
+ Log.w(
+ TAG,
+ "Skipping extension package "
+ + extensionPackage
+ + " because"
+ + " it doesn't have the CONTACTS_STRUCTURE metadata");
+ continue;
+ }
+ if (TextUtils.isEmpty(accountType.accountType)) {
+ Log.w(
+ TAG,
+ "Skipping extension package "
+ + extensionPackage
+ + " because"
+ + " the CONTACTS_STRUCTURE metadata doesn't have the accountType"
+ + " attribute");
+ continue;
+ }
+ Log.d(
+ TAG,
+ "Registering extension package account type="
+ + accountType.accountType
+ + ", dataSet="
+ + accountType.dataSet
+ + ", packageName="
+ + extensionPackage);
+
+ addAccountType(accountType, accountTypesByTypeAndDataSet, accountTypesByType);
+ }
+ }
+ timings.addSplit("Loaded account types");
+
+ // Map in accounts to associate the account names with each account type entry.
+ Account[] accounts = mAccountManager.getAccounts();
+ for (Account account : accounts) {
+ boolean syncable = ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
+
+ if (syncable) {
+ List<AccountType> accountTypes = accountTypesByType.get(account.type);
+ if (accountTypes != null) {
+ // Add an account-with-data-set entry for each account type that is
+ // authenticated by this account.
+ for (AccountType accountType : accountTypes) {
+ AccountWithDataSet accountWithDataSet =
+ new AccountWithDataSet(account.name, account.type, accountType.dataSet);
+ allAccounts.add(accountWithDataSet);
+ if (accountType.areContactsWritable()) {
+ contactWritableAccounts.add(accountWithDataSet);
+ }
+ if (accountType.isGroupMembershipEditable()) {
+ groupWritableAccounts.add(accountWithDataSet);
+ }
+ }
+ }
+ }
+ }
+
+ Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
+ Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR);
+ Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR);
+
+ timings.addSplit("Loaded accounts");
+
+ synchronized (this) {
+ mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
+ mAccounts = allAccounts;
+ mContactWritableAccounts = contactWritableAccounts;
+ mGroupWritableAccounts = groupWritableAccounts;
+ mInvitableAccountTypes =
+ findAllInvitableAccountTypes(mContext, allAccounts, accountTypesByTypeAndDataSet);
+ }
+
+ timings.dumpToLog();
+ final long endTimeWall = SystemClock.elapsedRealtime();
+ final long endTime = SystemClock.currentThreadTimeMillis();
+
+ Log.i(
+ TAG,
+ "Loaded meta-data for "
+ + mAccountTypesWithDataSets.size()
+ + " account types, "
+ + mAccounts.size()
+ + " accounts in "
+ + (endTimeWall - startTimeWall)
+ + "ms(wall) "
+ + (endTime - startTime)
+ + "ms(cpu)");
+
+ if (mInitializationLatch != null) {
+ mInitializationLatch.countDown();
+ mInitializationLatch = null;
+ }
+ if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
+ Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
+ }
+
+ // Check filter validity since filter may become obsolete after account update. It must be
+ // done from UI thread.
+ mMainThreadHandler.post(mCheckFilterValidityRunnable);
+ }
+
+ // Bookkeeping method for tracking the known account types in the given maps.
+ private void addAccountType(
+ AccountType accountType,
+ Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet,
+ Map<String, List<AccountType>> accountTypesByType) {
+ accountTypesByTypeAndDataSet.put(accountType.getAccountTypeAndDataSet(), accountType);
+ List<AccountType> accountsForType = accountTypesByType.get(accountType.accountType);
+ if (accountsForType == null) {
+ accountsForType = new ArrayList<>();
+ }
+ accountsForType.add(accountType);
+ accountTypesByType.put(accountType.accountType, accountsForType);
+ }
+
+ /** Return list of all known, contact writable {@link AccountWithDataSet}'s. */
+ @Override
+ public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
+ ensureAccountsLoaded();
+ return contactWritableOnly ? mContactWritableAccounts : mAccounts;
+ }
+
+ /** Return the list of all known, group writable {@link AccountWithDataSet}'s. */
+ public List<AccountWithDataSet> getGroupWritableAccounts() {
+ ensureAccountsLoaded();
+ return mGroupWritableAccounts;
+ }
+
+ /**
+ * Find the best {@link DataKind} matching the requested {@link AccountType#accountType}, {@link
+ * AccountType#dataSet}, and {@link DataKind#mimeType}. If no direct match found, we try searching
+ * {@link FallbackAccountType}.
+ */
+ @Override
+ public DataKind getKindOrFallback(AccountType type, String mimeType) {
+ ensureAccountsLoaded();
+ DataKind kind = null;
+
+ // Try finding account type and kind matching request
+ if (type != null) {
+ kind = type.getKindForMimetype(mimeType);
+ }
+
+ if (kind == null) {
+ // Nothing found, so try fallback as last resort
+ kind = mFallbackAccountType.getKindForMimetype(mimeType);
+ }
+
+ if (kind == null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unknown type=" + type + ", mime=" + mimeType);
+ }
+ }
+
+ return kind;
+ }
+
+ /** Return {@link AccountType} for the given account type and data set. */
+ @Override
+ public AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet) {
+ ensureAccountsLoaded();
+ synchronized (this) {
+ AccountType type = mAccountTypesWithDataSets.get(accountTypeWithDataSet);
+ return type != null ? type : mFallbackAccountType;
+ }
+ }
+
+ /**
+ * @return Unmodifiable map from {@link AccountTypeWithDataSet}s to {@link AccountType}s which
+ * support the "invite" feature and have one or more account. This is an unfiltered list. See
+ * {@link #getUsableInvitableAccountTypes()}.
+ */
+ private Map<AccountTypeWithDataSet, AccountType> getAllInvitableAccountTypes() {
+ ensureAccountsLoaded();
+ return mInvitableAccountTypes;
+ }
+
+ @Override
+ public Map<AccountTypeWithDataSet, AccountType> getUsableInvitableAccountTypes() {
+ ensureAccountsLoaded();
+ // Since this method is not thread-safe, it's possible for multiple threads to encounter
+ // the situation where (1) the cache has not been initialized yet or
+ // (2) an async task to refresh the account type list in the cache has already been
+ // started. Hence we use {@link AtomicBoolean}s and return cached values immediately
+ // while we compute the actual result in the background. We use this approach instead of
+ // using "synchronized" because computing the account type list involves a DB read, and
+ // can potentially cause a deadlock situation if this method is called from code which
+ // holds the DB lock. The trade-off of potentially having an incorrect list of invitable
+ // account types for a short period of time seems more manageable than enforcing the
+ // context in which this method is called.
+
+ // Computing the list of usable invitable account types is done on the fly as requested.
+ // If this method has never been called before, then block until the list has been computed.
+ if (!mInvitablesCacheIsInitialized.get()) {
+ mInvitableAccountTypeCache.setCachedValue(findUsableInvitableAccountTypes(mContext));
+ mInvitablesCacheIsInitialized.set(true);
+ } else {
+ // Otherwise, there is a value in the cache. If the value has expired and
+ // an async task has not already been started by another thread, then kick off a new
+ // async task to compute the list.
+ if (mInvitableAccountTypeCache.isExpired()
+ && mInvitablesTaskIsRunning.compareAndSet(false, true)) {
+ new FindInvitablesTask().execute();
+ }
+ }
+
+ return mInvitableAccountTypeCache.getCachedValue();
+ }
+
+ /**
+ * Return all usable {@link AccountType}s that support the "invite" feature from the list of all
+ * potential invitable account types (retrieved from {@link #getAllInvitableAccountTypes}). A
+ * usable invitable account type means: (1) there is at least 1 raw contact in the database with
+ * that account type, and (2) the app contributing the account type is not disabled.
+ *
+ * <p>Warning: Don't use on the UI thread because this can scan the database.
+ */
+ private Map<AccountTypeWithDataSet, AccountType> findUsableInvitableAccountTypes(
+ Context context) {
+ Map<AccountTypeWithDataSet, AccountType> allInvitables = getAllInvitableAccountTypes();
+ if (allInvitables.isEmpty()) {
+ return EMPTY_UNMODIFIABLE_ACCOUNT_TYPE_MAP;
+ }
+
+ final Map<AccountTypeWithDataSet, AccountType> result = new ArrayMap<>();
+ result.putAll(allInvitables);
+
+ final PackageManager packageManager = context.getPackageManager();
+ for (AccountTypeWithDataSet accountTypeWithDataSet : allInvitables.keySet()) {
+ AccountType accountType = allInvitables.get(accountTypeWithDataSet);
+
+ // Make sure that account types don't come from apps that are disabled.
+ Intent invitableIntent = MoreContactUtils.getInvitableIntent(accountType, SAMPLE_CONTACT_URI);
+ if (invitableIntent == null) {
+ result.remove(accountTypeWithDataSet);
+ continue;
+ }
+ ResolveInfo resolveInfo =
+ packageManager.resolveActivity(invitableIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo == null) {
+ // If we can't find an activity to start for this intent, then there's no point in
+ // showing this option to the user.
+ result.remove(accountTypeWithDataSet);
+ continue;
+ }
+
+ // Make sure that there is at least 1 raw contact with this account type. This check
+ // is non-trivial and should not be done on the UI thread.
+ if (!accountTypeWithDataSet.hasData(context)) {
+ result.remove(accountTypeWithDataSet);
+ }
+ }
+
+ return Collections.unmodifiableMap(result);
+ }
+
+ @Override
+ public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+ ensureAccountsLoaded();
+ final List<AccountType> accountTypes = new ArrayList<>();
+ synchronized (this) {
+ for (AccountType type : mAccountTypesWithDataSets.values()) {
+ if (!contactWritableOnly || type.areContactsWritable()) {
+ accountTypes.add(type);
+ }
+ }
+ }
+ return accountTypes;
+ }
+
+ /**
+ * This cache holds a list of invitable {@link AccountTypeWithDataSet}s, in the form of a {@link
+ * Map<AccountTypeWithDataSet, AccountType>}. Note that the cached value is valid only for {@link
+ * #TIME_TO_LIVE} milliseconds.
+ */
+ private static final class InvitableAccountTypeCache {
+
+ /**
+ * The cached {@link #mInvitableAccountTypes} list expires after this number of milliseconds has
+ * elapsed.
+ */
+ private static final long TIME_TO_LIVE = 60000;
+
+ private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes;
+
+ private long mTimeLastSet;
+
+ /**
+ * Returns true if the data in this cache is stale and needs to be refreshed. Returns false
+ * otherwise.
+ */
+ public boolean isExpired() {
+ return SystemClock.elapsedRealtime() - mTimeLastSet > TIME_TO_LIVE;
+ }
+
+ /**
+ * Returns the cached value. Note that the caller is responsible for checking {@link
+ * #isExpired()} to ensure that the value is not stale.
+ */
+ public Map<AccountTypeWithDataSet, AccountType> getCachedValue() {
+ return mInvitableAccountTypes;
+ }
+
+ public void setCachedValue(Map<AccountTypeWithDataSet, AccountType> map) {
+ mInvitableAccountTypes = map;
+ mTimeLastSet = SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Background task to find all usable {@link AccountType}s that support the "invite" feature from
+ * the list of all potential invitable account types. Once the work is completed, the list of
+ * account types is stored in the {@link AccountTypeManager}'s {@link InvitableAccountTypeCache}.
+ */
+ private class FindInvitablesTask
+ extends AsyncTask<Void, Void, Map<AccountTypeWithDataSet, AccountType>> {
+
+ @Override
+ protected Map<AccountTypeWithDataSet, AccountType> doInBackground(Void... params) {
+ return findUsableInvitableAccountTypes(mContext);
+ }
+
+ @Override
+ protected void onPostExecute(Map<AccountTypeWithDataSet, AccountType> accountTypes) {
+ mInvitableAccountTypeCache.setCachedValue(accountTypes);
+ mInvitablesTaskIsRunning.set(false);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/BuilderWrapper.java b/java/com/android/contacts/common/model/BuilderWrapper.java
new file mode 100644
index 000000000..9c666e59c
--- /dev/null
+++ b/java/com/android/contacts/common/model/BuilderWrapper.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation.Builder;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs. Since ContentProviderOperation is usually
+ * created by Builder and we don’t have access to the type via Builder, so we need to create a
+ * wrapper class for Builder first and include type. Then we could use the builder and the type in
+ * this class to create a wrapper of ContentProviderOperation.
+ */
+public class BuilderWrapper {
+
+ private Builder mBuilder;
+ private int mType;
+
+ public BuilderWrapper(Builder builder, int type) {
+ mBuilder = builder;
+ mType = type;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int mType) {
+ this.mType = mType;
+ }
+
+ public Builder getBuilder() {
+ return mBuilder;
+ }
+
+ public void setBuilder(Builder mBuilder) {
+ this.mBuilder = mBuilder;
+ }
+}
diff --git a/java/com/android/contacts/common/model/CPOWrapper.java b/java/com/android/contacts/common/model/CPOWrapper.java
new file mode 100644
index 000000000..4a67e6700
--- /dev/null
+++ b/java/com/android/contacts/common/model/CPOWrapper.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentProviderOperation;
+
+/**
+ * This class is created for the purpose of compatibility and make the type of
+ * ContentProviderOperation available on pre-M SDKs.
+ */
+public class CPOWrapper {
+
+ private ContentProviderOperation mOperation;
+ private int mType;
+
+ public CPOWrapper(ContentProviderOperation builder, int type) {
+ mOperation = builder;
+ mType = type;
+ }
+
+ public int getType() {
+ return mType;
+ }
+
+ public void setType(int type) {
+ this.mType = type;
+ }
+
+ public ContentProviderOperation getOperation() {
+ return mOperation;
+ }
+
+ public void setOperation(ContentProviderOperation operation) {
+ this.mOperation = operation;
+ }
+}
diff --git a/java/com/android/contacts/common/model/Contact.java b/java/com/android/contacts/common/model/Contact.java
new file mode 100644
index 000000000..ad0b66efe
--- /dev/null
+++ b/java/com/android/contacts/common/model/Contact.java
@@ -0,0 +1,384 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.support.annotation.VisibleForTesting;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+
+/**
+ * A Contact represents a single person or logical entity as perceived by the user. The information
+ * about a contact can come from multiple data sources, which are each represented by a RawContact
+ * object. Thus, a Contact is associated with a collection of RawContact objects.
+ *
+ * <p>The aggregation of raw contacts into a single contact is performed automatically, and it is
+ * also possible for users to manually split and join raw contacts into various contacts.
+ *
+ * <p>Only the {@link ContactLoader} class can create a Contact object with various flags to allow
+ * partial loading of contact data. Thus, an instance of this class should be treated as a read-only
+ * object.
+ */
+public class Contact {
+
+ private final Uri mRequestedUri;
+ private final Uri mLookupUri;
+ private final Uri mUri;
+ private final long mDirectoryId;
+ private final String mLookupKey;
+ private final long mId;
+ private final long mNameRawContactId;
+ private final int mDisplayNameSource;
+ private final long mPhotoId;
+ private final String mPhotoUri;
+ private final String mDisplayName;
+ private final String mAltDisplayName;
+ private final String mPhoneticName;
+ private final boolean mStarred;
+ private final Integer mPresence;
+ private final boolean mSendToVoicemail;
+ private final String mCustomRingtone;
+ private final boolean mIsUserProfile;
+ private final Contact.Status mStatus;
+ private final Exception mException;
+ private ImmutableList<RawContact> mRawContacts;
+ private ImmutableList<AccountType> mInvitableAccountTypes;
+ private String mDirectoryDisplayName;
+ private String mDirectoryType;
+ private String mDirectoryAccountType;
+ private String mDirectoryAccountName;
+ private int mDirectoryExportSupport;
+ private ImmutableList<GroupMetaData> mGroups;
+ private byte[] mPhotoBinaryData;
+ /**
+ * Small version of the contact photo loaded from a blob instead of from a file. If a large
+ * contact photo is not available yet, then this has the same value as mPhotoBinaryData.
+ */
+ private byte[] mThumbnailPhotoBinaryData;
+
+ /** Constructor for special results, namely "no contact found" and "error". */
+ private Contact(Uri requestedUri, Contact.Status status, Exception exception) {
+ if (status == Status.ERROR && exception == null) {
+ throw new IllegalArgumentException("ERROR result must have exception");
+ }
+ mStatus = status;
+ mException = exception;
+ mRequestedUri = requestedUri;
+ mLookupUri = null;
+ mUri = null;
+ mDirectoryId = -1;
+ mLookupKey = null;
+ mId = -1;
+ mRawContacts = null;
+ mNameRawContactId = -1;
+ mDisplayNameSource = DisplayNameSources.UNDEFINED;
+ mPhotoId = -1;
+ mPhotoUri = null;
+ mDisplayName = null;
+ mAltDisplayName = null;
+ mPhoneticName = null;
+ mStarred = false;
+ mPresence = null;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = false;
+ mCustomRingtone = null;
+ mIsUserProfile = false;
+ }
+
+ /** Constructor to call when contact was found */
+ public Contact(
+ Uri requestedUri,
+ Uri uri,
+ Uri lookupUri,
+ long directoryId,
+ String lookupKey,
+ long id,
+ long nameRawContactId,
+ int displayNameSource,
+ long photoId,
+ String photoUri,
+ String displayName,
+ String altDisplayName,
+ String phoneticName,
+ boolean starred,
+ Integer presence,
+ boolean sendToVoicemail,
+ String customRingtone,
+ boolean isUserProfile) {
+ mStatus = Status.LOADED;
+ mException = null;
+ mRequestedUri = requestedUri;
+ mLookupUri = lookupUri;
+ mUri = uri;
+ mDirectoryId = directoryId;
+ mLookupKey = lookupKey;
+ mId = id;
+ mRawContacts = null;
+ mNameRawContactId = nameRawContactId;
+ mDisplayNameSource = displayNameSource;
+ mPhotoId = photoId;
+ mPhotoUri = photoUri;
+ mDisplayName = displayName;
+ mAltDisplayName = altDisplayName;
+ mPhoneticName = phoneticName;
+ mStarred = starred;
+ mPresence = presence;
+ mInvitableAccountTypes = null;
+ mSendToVoicemail = sendToVoicemail;
+ mCustomRingtone = customRingtone;
+ mIsUserProfile = isUserProfile;
+ }
+
+ public Contact(Uri requestedUri, Contact from) {
+ mRequestedUri = requestedUri;
+
+ mStatus = from.mStatus;
+ mException = from.mException;
+ mLookupUri = from.mLookupUri;
+ mUri = from.mUri;
+ mDirectoryId = from.mDirectoryId;
+ mLookupKey = from.mLookupKey;
+ mId = from.mId;
+ mNameRawContactId = from.mNameRawContactId;
+ mDisplayNameSource = from.mDisplayNameSource;
+ mPhotoId = from.mPhotoId;
+ mPhotoUri = from.mPhotoUri;
+ mDisplayName = from.mDisplayName;
+ mAltDisplayName = from.mAltDisplayName;
+ mPhoneticName = from.mPhoneticName;
+ mStarred = from.mStarred;
+ mPresence = from.mPresence;
+ mRawContacts = from.mRawContacts;
+ mInvitableAccountTypes = from.mInvitableAccountTypes;
+
+ mDirectoryDisplayName = from.mDirectoryDisplayName;
+ mDirectoryType = from.mDirectoryType;
+ mDirectoryAccountType = from.mDirectoryAccountType;
+ mDirectoryAccountName = from.mDirectoryAccountName;
+ mDirectoryExportSupport = from.mDirectoryExportSupport;
+
+ mGroups = from.mGroups;
+
+ mPhotoBinaryData = from.mPhotoBinaryData;
+ mSendToVoicemail = from.mSendToVoicemail;
+ mCustomRingtone = from.mCustomRingtone;
+ mIsUserProfile = from.mIsUserProfile;
+ }
+
+ public static Contact forError(Uri requestedUri, Exception exception) {
+ return new Contact(requestedUri, Status.ERROR, exception);
+ }
+
+ public static Contact forNotFound(Uri requestedUri) {
+ return new Contact(requestedUri, Status.NOT_FOUND, null);
+ }
+
+ /** @param exportSupport See {@link Directory#EXPORT_SUPPORT}. */
+ public void setDirectoryMetaData(
+ String displayName,
+ String directoryType,
+ String accountType,
+ String accountName,
+ int exportSupport) {
+ mDirectoryDisplayName = displayName;
+ mDirectoryType = directoryType;
+ mDirectoryAccountType = accountType;
+ mDirectoryAccountName = accountName;
+ mDirectoryExportSupport = exportSupport;
+ }
+
+ /**
+ * Returns the URI for the contact that contains both the lookup key and the ID. This is the best
+ * URI to reference a contact. For directory contacts, this is the same a the URI as returned by
+ * {@link #getUri()}
+ */
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public String getLookupKey() {
+ return mLookupKey;
+ }
+
+ /**
+ * Returns the contact Uri that was passed to the provider to make the query. This is the same as
+ * the requested Uri, unless the requested Uri doesn't specify a Contact: If it either references
+ * a Raw-Contact or a Person (a pre-Eclair style Uri), this Uri will always reference the full
+ * aggregate contact.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /** Returns the contact ID. */
+ @VisibleForTesting
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * @return true when an exception happened during loading, in which case {@link #getException}
+ * returns the actual exception object.
+ */
+ public boolean isError() {
+ return mStatus == Status.ERROR;
+ }
+
+ public Exception getException() {
+ return mException;
+ }
+
+ /** @return true if the specified contact is successfully loaded. */
+ public boolean isLoaded() {
+ return mStatus == Status.LOADED;
+ }
+
+ public long getNameRawContactId() {
+ return mNameRawContactId;
+ }
+
+ public int getDisplayNameSource() {
+ return mDisplayNameSource;
+ }
+
+ public long getPhotoId() {
+ return mPhotoId;
+ }
+
+ public String getPhotoUri() {
+ return mPhotoUri;
+ }
+
+ public String getDisplayName() {
+ return mDisplayName;
+ }
+
+ public boolean getStarred() {
+ return mStarred;
+ }
+
+ public Integer getPresence() {
+ return mPresence;
+ }
+
+ /**
+ * This can return non-null invitable account types only if the {@link ContactLoader} was
+ * configured to load invitable account types in its constructor.
+ */
+ public ImmutableList<AccountType> getInvitableAccountTypes() {
+ return mInvitableAccountTypes;
+ }
+
+ /* package */ void setInvitableAccountTypes(ImmutableList<AccountType> accountTypes) {
+ mInvitableAccountTypes = accountTypes;
+ }
+
+ public ImmutableList<RawContact> getRawContacts() {
+ return mRawContacts;
+ }
+
+ /* package */ void setRawContacts(ImmutableList<RawContact> rawContacts) {
+ mRawContacts = rawContacts;
+ }
+
+ public long getDirectoryId() {
+ return mDirectoryId;
+ }
+
+ public boolean isDirectoryEntry() {
+ return mDirectoryId != -1
+ && mDirectoryId != Directory.DEFAULT
+ && mDirectoryId != Directory.LOCAL_INVISIBLE;
+ }
+
+ /* package */ void setPhotoBinaryData(byte[] photoBinaryData) {
+ mPhotoBinaryData = photoBinaryData;
+ }
+
+ public byte[] getThumbnailPhotoBinaryData() {
+ return mThumbnailPhotoBinaryData;
+ }
+
+ /* package */ void setThumbnailPhotoBinaryData(byte[] photoBinaryData) {
+ mThumbnailPhotoBinaryData = photoBinaryData;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ if (mRawContacts.size() != 1) {
+ throw new IllegalStateException("Cannot extract content values from an aggregated contact");
+ }
+
+ RawContact rawContact = mRawContacts.get(0);
+ ArrayList<ContentValues> result = rawContact.getContentValues();
+
+ // If the photo was loaded using the URI, create an entry for the photo
+ // binary data.
+ if (mPhotoId == 0 && mPhotoBinaryData != null) {
+ ContentValues photo = new ContentValues();
+ photo.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
+ photo.put(Photo.PHOTO, mPhotoBinaryData);
+ result.add(photo);
+ }
+
+ return result;
+ }
+
+ /**
+ * This can return non-null group meta-data only if the {@link ContactLoader} was configured to
+ * load group metadata in its constructor.
+ */
+ public ImmutableList<GroupMetaData> getGroupMetaData() {
+ return mGroups;
+ }
+
+ /* package */ void setGroupMetaData(ImmutableList<GroupMetaData> groups) {
+ mGroups = groups;
+ }
+
+ public boolean isUserProfile() {
+ return mIsUserProfile;
+ }
+
+ @Override
+ public String toString() {
+ return "{requested="
+ + mRequestedUri
+ + ",lookupkey="
+ + mLookupKey
+ + ",uri="
+ + mUri
+ + ",status="
+ + mStatus
+ + "}";
+ }
+
+ private enum Status {
+ /** Contact is successfully loaded */
+ LOADED,
+ /** There was an error loading the contact */
+ ERROR,
+ /** Contact is not found */
+ NOT_FOUND,
+ }
+}
diff --git a/java/com/android/contacts/common/model/ContactLoader.java b/java/com/android/contacts/common/model/ContactLoader.java
new file mode 100644
index 000000000..eb16bffcd
--- /dev/null
+++ b/java/com/android/contacts/common/model/ContactLoader.java
@@ -0,0 +1,998 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.Groups;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.GroupMetaData;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountTypeWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import com.android.contacts.common.model.dataitem.PhoneDataItem;
+import com.android.contacts.common.model.dataitem.PhotoDataItem;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.ContactLoaderUtils;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.compat.CompatUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Loads a single Contact and all it constituent RawContacts. */
+public class ContactLoader extends AsyncTaskLoader<Contact> {
+
+ private static final String TAG = ContactLoader.class.getSimpleName();
+
+ /** A short-lived cache that can be set by {@link #cacheResult()} */
+ private static Contact sCachedResult = null;
+
+ private final Uri mRequestedUri;
+ private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
+ private Uri mLookupUri;
+ private boolean mLoadGroupMetaData;
+ private boolean mLoadInvitableAccountTypes;
+ private boolean mPostViewNotification;
+ private boolean mComputeFormattedPhoneNumber;
+ private Contact mContact;
+ private ForceLoadContentObserver mObserver;
+
+ public ContactLoader(Context context, Uri lookupUri, boolean postViewNotification) {
+ this(context, lookupUri, false, false, postViewNotification, false);
+ }
+
+ public ContactLoader(
+ Context context,
+ Uri lookupUri,
+ boolean loadGroupMetaData,
+ boolean loadInvitableAccountTypes,
+ boolean postViewNotification,
+ boolean computeFormattedPhoneNumber) {
+ super(context);
+ mLookupUri = lookupUri;
+ mRequestedUri = lookupUri;
+ mLoadGroupMetaData = loadGroupMetaData;
+ mLoadInvitableAccountTypes = loadInvitableAccountTypes;
+ mPostViewNotification = postViewNotification;
+ mComputeFormattedPhoneNumber = computeFormattedPhoneNumber;
+ }
+
+ /**
+ * Parses a {@link Contact} stored as a JSON string in a lookup URI.
+ *
+ * @param lookupUri The contact information to parse .
+ * @return The parsed {@code Contact} information.
+ */
+ public static Contact parseEncodedContactEntity(Uri lookupUri) {
+ try {
+ return loadEncodedContactEntity(lookupUri, lookupUri);
+ } catch (JSONException je) {
+ return null;
+ }
+ }
+
+ private static Contact loadEncodedContactEntity(Uri uri, Uri lookupUri) throws JSONException {
+ final String jsonString = uri.getEncodedFragment();
+ final JSONObject json = new JSONObject(jsonString);
+
+ final long directoryId =
+ Long.valueOf(uri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY));
+
+ final String displayName = json.optString(Contacts.DISPLAY_NAME);
+ final String altDisplayName = json.optString(Contacts.DISPLAY_NAME_ALTERNATIVE, displayName);
+ final int displayNameSource = json.getInt(Contacts.DISPLAY_NAME_SOURCE);
+ final String photoUri = json.optString(Contacts.PHOTO_URI, null);
+ final Contact contact =
+ new Contact(
+ uri,
+ uri,
+ lookupUri,
+ directoryId,
+ null /* lookupKey */,
+ -1 /* id */,
+ -1 /* nameRawContactId */,
+ displayNameSource,
+ 0 /* photoId */,
+ photoUri,
+ displayName,
+ altDisplayName,
+ null /* phoneticName */,
+ false /* starred */,
+ null /* presence */,
+ false /* sendToVoicemail */,
+ null /* customRingtone */,
+ false /* isUserProfile */);
+
+ final String accountName = json.optString(RawContacts.ACCOUNT_NAME, null);
+ final String directoryName = uri.getQueryParameter(Directory.DISPLAY_NAME);
+ if (accountName != null) {
+ final String accountType = json.getString(RawContacts.ACCOUNT_TYPE);
+ contact.setDirectoryMetaData(
+ directoryName,
+ null,
+ accountName,
+ accountType,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY));
+ } else {
+ contact.setDirectoryMetaData(
+ directoryName,
+ null,
+ null,
+ null,
+ json.optInt(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT));
+ }
+
+ final ContentValues values = new ContentValues();
+ values.put(Data._ID, -1);
+ values.put(Data.CONTACT_ID, -1);
+ final RawContact rawContact = new RawContact(values);
+
+ final JSONObject items = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+ final Iterator keys = items.keys();
+ while (keys.hasNext()) {
+ final String mimetype = (String) keys.next();
+
+ // Could be single object or array.
+ final JSONObject obj = items.optJSONObject(mimetype);
+ if (obj == null) {
+ final JSONArray array = items.getJSONArray(mimetype);
+ for (int i = 0; i < array.length(); i++) {
+ final JSONObject item = array.getJSONObject(i);
+ processOneRecord(rawContact, item, mimetype);
+ }
+ } else {
+ processOneRecord(rawContact, obj, mimetype);
+ }
+ }
+
+ contact.setRawContacts(new ImmutableList.Builder<RawContact>().add(rawContact).build());
+ return contact;
+ }
+
+ private static void processOneRecord(RawContact rawContact, JSONObject item, String mimetype)
+ throws JSONException {
+ final ContentValues itemValues = new ContentValues();
+ itemValues.put(Data.MIMETYPE, mimetype);
+ itemValues.put(Data._ID, -1);
+
+ final Iterator iterator = item.keys();
+ while (iterator.hasNext()) {
+ String name = (String) iterator.next();
+ final Object o = item.get(name);
+ if (o instanceof String) {
+ itemValues.put(name, (String) o);
+ } else if (o instanceof Integer) {
+ itemValues.put(name, (Integer) o);
+ }
+ }
+ rawContact.addDataItemValues(itemValues);
+ }
+
+ @Override
+ public Contact loadInBackground() {
+ Log.e(TAG, "loadInBackground=" + mLookupUri);
+ try {
+ final ContentResolver resolver = getContext().getContentResolver();
+ final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mLookupUri);
+ final Contact cachedResult = sCachedResult;
+ sCachedResult = null;
+ // Is this the same Uri as what we had before already? In that case, reuse that result
+ final Contact result;
+ final boolean resultIsCached;
+ if (cachedResult != null && UriUtils.areEqual(cachedResult.getLookupUri(), mLookupUri)) {
+ // We are using a cached result from earlier. Below, we should make sure
+ // we are not doing any more network or disc accesses
+ result = new Contact(mRequestedUri, cachedResult);
+ resultIsCached = true;
+ } else {
+ if (uriCurrentFormat.getLastPathSegment().equals(Constants.LOOKUP_URI_ENCODED)) {
+ result = loadEncodedContactEntity(uriCurrentFormat, mLookupUri);
+ } else {
+ result = loadContactEntity(resolver, uriCurrentFormat);
+ }
+ resultIsCached = false;
+ }
+ if (result.isLoaded()) {
+ if (result.isDirectoryEntry()) {
+ if (!resultIsCached) {
+ loadDirectoryMetaData(result);
+ }
+ } else if (mLoadGroupMetaData) {
+ if (result.getGroupMetaData() == null) {
+ loadGroupMetaData(result);
+ }
+ }
+ if (mComputeFormattedPhoneNumber) {
+ computeFormattedPhoneNumbers(result);
+ }
+ if (!resultIsCached) {
+ loadPhotoBinaryData(result);
+ }
+
+ // Note ME profile should never have "Add connection"
+ if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
+ loadInvitableAccountTypes(result);
+ }
+ }
+ return result;
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading the contact: " + mLookupUri, e);
+ return Contact.forError(mRequestedUri, e);
+ }
+ }
+
+ private Contact loadContactEntity(ContentResolver resolver, Uri contactUri) {
+ Uri entityUri = Uri.withAppendedPath(contactUri, Contacts.Entity.CONTENT_DIRECTORY);
+ Cursor cursor =
+ resolver.query(entityUri, ContactQuery.COLUMNS, null, null, Contacts.Entity.RAW_CONTACT_ID);
+ if (cursor == null) {
+ Log.e(TAG, "No cursor returned in loadContactEntity");
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ try {
+ if (!cursor.moveToFirst()) {
+ cursor.close();
+ return Contact.forNotFound(mRequestedUri);
+ }
+
+ // Create the loaded contact starting with the header data.
+ Contact contact = loadContactHeaderData(cursor, contactUri);
+
+ // Fill in the raw contacts, which is wrapped in an Entity and any
+ // status data. Initially, result has empty entities and statuses.
+ long currentRawContactId = -1;
+ RawContact rawContact = null;
+ ImmutableList.Builder<RawContact> rawContactsBuilder =
+ new ImmutableList.Builder<RawContact>();
+ do {
+ long rawContactId = cursor.getLong(ContactQuery.RAW_CONTACT_ID);
+ if (rawContactId != currentRawContactId) {
+ // First time to see this raw contact id, so create a new entity, and
+ // add it to the result's entities.
+ currentRawContactId = rawContactId;
+ rawContact = new RawContact(loadRawContactValues(cursor));
+ rawContactsBuilder.add(rawContact);
+ }
+ if (!cursor.isNull(ContactQuery.DATA_ID)) {
+ ContentValues data = loadDataValues(cursor);
+ rawContact.addDataItemValues(data);
+ }
+ } while (cursor.moveToNext());
+
+ contact.setRawContacts(rawContactsBuilder.build());
+
+ return contact;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Looks for the photo data item in entities. If found, a thumbnail will be stored. A larger photo
+ * will also be stored if available.
+ */
+ private void loadPhotoBinaryData(Contact contactData) {
+ loadThumbnailBinaryData(contactData);
+
+ // Try to load the large photo from a file using the photo URI.
+ String photoUri = contactData.getPhotoUri();
+ if (photoUri != null) {
+ try {
+ final InputStream inputStream;
+ final AssetFileDescriptor fd;
+ final Uri uri = Uri.parse(photoUri);
+ final String scheme = uri.getScheme();
+ if ("http".equals(scheme) || "https".equals(scheme)) {
+ // Support HTTP urls that might come from extended directories
+ inputStream = new URL(photoUri).openStream();
+ fd = null;
+ } else {
+ fd = getContext().getContentResolver().openAssetFileDescriptor(uri, "r");
+ inputStream = fd.createInputStream();
+ }
+ byte[] buffer = new byte[16 * 1024];
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ int size;
+ while ((size = inputStream.read(buffer)) != -1) {
+ baos.write(buffer, 0, size);
+ }
+ contactData.setPhotoBinaryData(baos.toByteArray());
+ } finally {
+ inputStream.close();
+ if (fd != null) {
+ fd.close();
+ }
+ }
+ return;
+ } catch (IOException ioe) {
+ // Just fall back to the case below.
+ }
+ }
+
+ // If we couldn't load from a file, fall back to the data blob.
+ contactData.setPhotoBinaryData(contactData.getThumbnailPhotoBinaryData());
+ }
+
+ private void loadThumbnailBinaryData(Contact contactData) {
+ final long photoId = contactData.getPhotoId();
+ if (photoId <= 0) {
+ // No photo ID
+ return;
+ }
+
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ for (DataItem dataItem : rawContact.getDataItems()) {
+ if (dataItem.getId() == photoId) {
+ if (!(dataItem instanceof PhotoDataItem)) {
+ break;
+ }
+
+ final PhotoDataItem photo = (PhotoDataItem) dataItem;
+ contactData.setThumbnailPhotoBinaryData(photo.getPhoto());
+ break;
+ }
+ }
+ }
+ }
+
+ /** Sets the "invitable" account types to {@link Contact#mInvitableAccountTypes}. */
+ private void loadInvitableAccountTypes(Contact contactData) {
+ final ImmutableList.Builder<AccountType> resultListBuilder =
+ new ImmutableList.Builder<AccountType>();
+ if (!contactData.isUserProfile()) {
+ Map<AccountTypeWithDataSet, AccountType> invitables =
+ AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+ if (!invitables.isEmpty()) {
+ final Map<AccountTypeWithDataSet, AccountType> resultMap = Maps.newHashMap(invitables);
+
+ // Remove the ones that already have a raw contact in the current contact
+ for (RawContact rawContact : contactData.getRawContacts()) {
+ final AccountTypeWithDataSet type =
+ AccountTypeWithDataSet.get(
+ rawContact.getAccountTypeString(), rawContact.getDataSet());
+ resultMap.remove(type);
+ }
+
+ resultListBuilder.addAll(resultMap.values());
+ }
+ }
+
+ // Set to mInvitableAccountTypes
+ contactData.setInvitableAccountTypes(resultListBuilder.build());
+ }
+
+ /** Extracts Contact level columns from the cursor. */
+ private Contact loadContactHeaderData(final Cursor cursor, Uri contactUri) {
+ final String directoryParameter =
+ contactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ final long directoryId =
+ directoryParameter == null ? Directory.DEFAULT : Long.parseLong(directoryParameter);
+ final long contactId = cursor.getLong(ContactQuery.CONTACT_ID);
+ final String lookupKey = cursor.getString(ContactQuery.LOOKUP_KEY);
+ final long nameRawContactId = cursor.getLong(ContactQuery.NAME_RAW_CONTACT_ID);
+ final int displayNameSource = cursor.getInt(ContactQuery.DISPLAY_NAME_SOURCE);
+ final String displayName = cursor.getString(ContactQuery.DISPLAY_NAME);
+ final String altDisplayName = cursor.getString(ContactQuery.ALT_DISPLAY_NAME);
+ final String phoneticName = cursor.getString(ContactQuery.PHONETIC_NAME);
+ final long photoId = cursor.getLong(ContactQuery.PHOTO_ID);
+ final String photoUri = cursor.getString(ContactQuery.PHOTO_URI);
+ final boolean starred = cursor.getInt(ContactQuery.STARRED) != 0;
+ final Integer presence =
+ cursor.isNull(ContactQuery.CONTACT_PRESENCE)
+ ? null
+ : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
+ final boolean sendToVoicemail = cursor.getInt(ContactQuery.SEND_TO_VOICEMAIL) == 1;
+ final String customRingtone = cursor.getString(ContactQuery.CUSTOM_RINGTONE);
+ final boolean isUserProfile = cursor.getInt(ContactQuery.IS_USER_PROFILE) == 1;
+
+ Uri lookupUri;
+ if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
+ lookupUri =
+ ContentUris.withAppendedId(
+ Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey), contactId);
+ } else {
+ lookupUri = contactUri;
+ }
+
+ return new Contact(
+ mRequestedUri,
+ contactUri,
+ lookupUri,
+ directoryId,
+ lookupKey,
+ contactId,
+ nameRawContactId,
+ displayNameSource,
+ photoId,
+ photoUri,
+ displayName,
+ altDisplayName,
+ phoneticName,
+ starred,
+ presence,
+ sendToVoicemail,
+ customRingtone,
+ isUserProfile);
+ }
+
+ /** Extracts RawContact level columns from the cursor. */
+ private ContentValues loadRawContactValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(RawContacts._ID, cursor.getLong(ContactQuery.RAW_CONTACT_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_NAME);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.ACCOUNT_TYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SET);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DIRTY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DELETED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CONTACT_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.STARRED);
+
+ return cv;
+ }
+
+ /** Extracts Data level columns from the cursor. */
+ private ContentValues loadDataValues(Cursor cursor) {
+ ContentValues cv = new ContentValues();
+
+ cv.put(Data._ID, cursor.getLong(ContactQuery.DATA_ID));
+
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA5);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA6);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA7);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA8);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA9);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA10);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA11);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA12);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA13);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA14);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA15);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC1);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC2);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC3);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_SYNC4);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.DATA_VERSION);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_PRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.IS_SUPERPRIMARY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.MIMETYPE);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.GROUP_SOURCE_ID);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CHAT_CAPABILITY);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.TIMES_USED);
+ cursorColumnToContentValues(cursor, cv, ContactQuery.LAST_TIME_USED);
+ if (CompatUtils.isMarshmallowCompatible()) {
+ cursorColumnToContentValues(cursor, cv, ContactQuery.CARRIER_PRESENCE);
+ }
+
+ return cv;
+ }
+
+ private void cursorColumnToContentValues(Cursor cursor, ContentValues values, int index) {
+ switch (cursor.getType(index)) {
+ case Cursor.FIELD_TYPE_NULL:
+ // don't put anything in the content values
+ break;
+ case Cursor.FIELD_TYPE_INTEGER:
+ values.put(ContactQuery.COLUMNS[index], cursor.getLong(index));
+ break;
+ case Cursor.FIELD_TYPE_STRING:
+ values.put(ContactQuery.COLUMNS[index], cursor.getString(index));
+ break;
+ case Cursor.FIELD_TYPE_BLOB:
+ values.put(ContactQuery.COLUMNS[index], cursor.getBlob(index));
+ break;
+ default:
+ throw new IllegalStateException("Invalid or unhandled data type");
+ }
+ }
+
+ private void loadDirectoryMetaData(Contact result) {
+ long directoryId = result.getDirectoryId();
+
+ Cursor cursor =
+ getContext()
+ .getContentResolver()
+ .query(
+ ContentUris.withAppendedId(Directory.CONTENT_URI, directoryId),
+ DirectoryQuery.COLUMNS,
+ null,
+ null,
+ null);
+ if (cursor == null) {
+ return;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ final String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
+ final String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME);
+ final int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
+ final String accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
+ final String accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
+ final int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
+ String directoryType = null;
+ if (!TextUtils.isEmpty(packageName)) {
+ PackageManager pm = getContext().getPackageManager();
+ try {
+ Resources resources = pm.getResourcesForApplication(packageName);
+ directoryType = resources.getString(typeResourceId);
+ } catch (NameNotFoundException e) {
+ Log.w(
+ TAG, "Contact directory resource not found: " + packageName + "." + typeResourceId);
+ }
+ }
+
+ result.setDirectoryMetaData(
+ displayName, directoryType, accountType, accountName, exportSupport);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Loads groups meta-data for all groups associated with all constituent raw contacts' accounts.
+ */
+ private void loadGroupMetaData(Contact result) {
+ StringBuilder selection = new StringBuilder();
+ ArrayList<String> selectionArgs = new ArrayList<String>();
+ final HashSet<AccountKey> accountsSeen = new HashSet<>();
+ for (RawContact rawContact : result.getRawContacts()) {
+ final String accountName = rawContact.getAccountName();
+ final String accountType = rawContact.getAccountTypeString();
+ final String dataSet = rawContact.getDataSet();
+ final AccountKey accountKey = new AccountKey(accountName, accountType, dataSet);
+ if (accountName != null && accountType != null && !accountsSeen.contains(accountKey)) {
+ accountsSeen.add(accountKey);
+ if (selection.length() != 0) {
+ selection.append(" OR ");
+ }
+ selection.append("(" + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?");
+ selectionArgs.add(accountName);
+ selectionArgs.add(accountType);
+
+ if (dataSet != null) {
+ selection.append(" AND " + Groups.DATA_SET + "=?");
+ selectionArgs.add(dataSet);
+ } else {
+ selection.append(" AND " + Groups.DATA_SET + " IS NULL");
+ }
+ selection.append(")");
+ }
+ }
+ final ImmutableList.Builder<GroupMetaData> groupListBuilder =
+ new ImmutableList.Builder<GroupMetaData>();
+ final Cursor cursor =
+ getContext()
+ .getContentResolver()
+ .query(
+ Groups.CONTENT_URI,
+ GroupQuery.COLUMNS,
+ selection.toString(),
+ selectionArgs.toArray(new String[0]),
+ null);
+ if (cursor != null) {
+ try {
+ while (cursor.moveToNext()) {
+ final String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
+ final String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
+ final String dataSet = cursor.getString(GroupQuery.DATA_SET);
+ final long groupId = cursor.getLong(GroupQuery.ID);
+ final String title = cursor.getString(GroupQuery.TITLE);
+ final boolean defaultGroup =
+ !cursor.isNull(GroupQuery.AUTO_ADD) && cursor.getInt(GroupQuery.AUTO_ADD) != 0;
+ final boolean favorites =
+ !cursor.isNull(GroupQuery.FAVORITES) && cursor.getInt(GroupQuery.FAVORITES) != 0;
+
+ groupListBuilder.add(
+ new GroupMetaData(
+ accountName, accountType, dataSet, groupId, title, defaultGroup, favorites));
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ result.setGroupMetaData(groupListBuilder.build());
+ }
+
+ /**
+ * Iterates over all data items that represent phone numbers are tries to calculate a formatted
+ * number. This function can safely be called several times as no unformatted data is overwritten
+ */
+ private void computeFormattedPhoneNumbers(Contact contactData) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(getContext());
+ final ImmutableList<RawContact> rawContacts = contactData.getRawContacts();
+ final int rawContactCount = rawContacts.size();
+ for (int rawContactIndex = 0; rawContactIndex < rawContactCount; rawContactIndex++) {
+ final RawContact rawContact = rawContacts.get(rawContactIndex);
+ final List<DataItem> dataItems = rawContact.getDataItems();
+ final int dataCount = dataItems.size();
+ for (int dataIndex = 0; dataIndex < dataCount; dataIndex++) {
+ final DataItem dataItem = dataItems.get(dataIndex);
+ if (dataItem instanceof PhoneDataItem) {
+ final PhoneDataItem phoneDataItem = (PhoneDataItem) dataItem;
+ phoneDataItem.computeFormattedPhoneNumber(countryIso);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void deliverResult(Contact result) {
+ unregisterObserver();
+
+ // The creator isn't interested in any further updates
+ if (isReset() || result == null) {
+ return;
+ }
+
+ mContact = result;
+
+ if (result.isLoaded()) {
+ mLookupUri = result.getLookupUri();
+
+ if (!result.isDirectoryEntry()) {
+ Log.i(TAG, "Registering content observer for " + mLookupUri);
+ if (mObserver == null) {
+ mObserver = new ForceLoadContentObserver();
+ }
+ getContext().getContentResolver().registerContentObserver(mLookupUri, true, mObserver);
+ }
+
+ if (mPostViewNotification) {
+ // inform the source of the data that this contact is being looked at
+ postViewNotificationToSyncAdapter();
+ }
+ }
+
+ super.deliverResult(mContact);
+ }
+
+ /**
+ * Posts a message to the contributing sync adapters that have opted-in, notifying them that the
+ * contact has just been loaded
+ */
+ private void postViewNotificationToSyncAdapter() {
+ Context context = getContext();
+ for (RawContact rawContact : mContact.getRawContacts()) {
+ final long rawContactId = rawContact.getId();
+ if (mNotifiedRawContactIds.contains(rawContactId)) {
+ continue; // Already notified for this raw contact.
+ }
+ mNotifiedRawContactIds.add(rawContactId);
+ final AccountType accountType = rawContact.getAccountType(context);
+ final String serviceName = accountType.getViewContactNotifyServiceClassName();
+ final String servicePackageName = accountType.getViewContactNotifyServicePackageName();
+ if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(servicePackageName)) {
+ final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
+ final Intent intent = new Intent();
+ intent.setClassName(servicePackageName, serviceName);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.setDataAndType(uri, RawContacts.CONTENT_ITEM_TYPE);
+ try {
+ context.startService(intent);
+ } catch (Exception e) {
+ Log.e(TAG, "Error sending message to source-app", e);
+ }
+ }
+ }
+ }
+
+ private void unregisterObserver() {
+ if (mObserver != null) {
+ getContext().getContentResolver().unregisterContentObserver(mObserver);
+ mObserver = null;
+ }
+ }
+
+ public Uri getLookupUri() {
+ return mLookupUri;
+ }
+
+ public void setLookupUri(Uri lookupUri) {
+ mLookupUri = lookupUri;
+ }
+
+ @Override
+ protected void onStartLoading() {
+ if (mContact != null) {
+ deliverResult(mContact);
+ }
+
+ if (takeContentChanged() || mContact == null) {
+ forceLoad();
+ }
+ }
+
+ @Override
+ protected void onStopLoading() {
+ cancelLoad();
+ }
+
+ @Override
+ protected void onReset() {
+ super.onReset();
+ cancelLoad();
+ unregisterObserver();
+ mContact = null;
+ }
+
+ /**
+ * Projection used for the query that loads all data for the entire contact (except for social
+ * stream items).
+ */
+ private static class ContactQuery {
+
+ public static final int NAME_RAW_CONTACT_ID = 0;
+ public static final int DISPLAY_NAME_SOURCE = 1;
+ public static final int LOOKUP_KEY = 2;
+ public static final int DISPLAY_NAME = 3;
+ public static final int ALT_DISPLAY_NAME = 4;
+ public static final int PHONETIC_NAME = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int STARRED = 7;
+ public static final int CONTACT_PRESENCE = 8;
+ public static final int CONTACT_STATUS = 9;
+ public static final int CONTACT_STATUS_TIMESTAMP = 10;
+ public static final int CONTACT_STATUS_RES_PACKAGE = 11;
+ public static final int CONTACT_STATUS_LABEL = 12;
+ public static final int CONTACT_ID = 13;
+ public static final int RAW_CONTACT_ID = 14;
+ public static final int ACCOUNT_NAME = 15;
+ public static final int ACCOUNT_TYPE = 16;
+ public static final int DATA_SET = 17;
+ public static final int DIRTY = 18;
+ public static final int VERSION = 19;
+ public static final int SOURCE_ID = 20;
+ public static final int SYNC1 = 21;
+ public static final int SYNC2 = 22;
+ public static final int SYNC3 = 23;
+ public static final int SYNC4 = 24;
+ public static final int DELETED = 25;
+ public static final int DATA_ID = 26;
+ public static final int DATA1 = 27;
+ public static final int DATA2 = 28;
+ public static final int DATA3 = 29;
+ public static final int DATA4 = 30;
+ public static final int DATA5 = 31;
+ public static final int DATA6 = 32;
+ public static final int DATA7 = 33;
+ public static final int DATA8 = 34;
+ public static final int DATA9 = 35;
+ public static final int DATA10 = 36;
+ public static final int DATA11 = 37;
+ public static final int DATA12 = 38;
+ public static final int DATA13 = 39;
+ public static final int DATA14 = 40;
+ public static final int DATA15 = 41;
+ public static final int DATA_SYNC1 = 42;
+ public static final int DATA_SYNC2 = 43;
+ public static final int DATA_SYNC3 = 44;
+ public static final int DATA_SYNC4 = 45;
+ public static final int DATA_VERSION = 46;
+ public static final int IS_PRIMARY = 47;
+ public static final int IS_SUPERPRIMARY = 48;
+ public static final int MIMETYPE = 49;
+ public static final int GROUP_SOURCE_ID = 50;
+ public static final int PRESENCE = 51;
+ public static final int CHAT_CAPABILITY = 52;
+ public static final int STATUS = 53;
+ public static final int STATUS_RES_PACKAGE = 54;
+ public static final int STATUS_ICON = 55;
+ public static final int STATUS_LABEL = 56;
+ public static final int STATUS_TIMESTAMP = 57;
+ public static final int PHOTO_URI = 58;
+ public static final int SEND_TO_VOICEMAIL = 59;
+ public static final int CUSTOM_RINGTONE = 60;
+ public static final int IS_USER_PROFILE = 61;
+ public static final int TIMES_USED = 62;
+ public static final int LAST_TIME_USED = 63;
+ public static final int CARRIER_PRESENCE = 64;
+ static final String[] COLUMNS_INTERNAL =
+ new String[] {
+ Contacts.NAME_RAW_CONTACT_ID,
+ Contacts.DISPLAY_NAME_SOURCE,
+ Contacts.LOOKUP_KEY,
+ Contacts.DISPLAY_NAME,
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ Contacts.PHONETIC_NAME,
+ Contacts.PHOTO_ID,
+ Contacts.STARRED,
+ Contacts.CONTACT_PRESENCE,
+ Contacts.CONTACT_STATUS,
+ Contacts.CONTACT_STATUS_TIMESTAMP,
+ Contacts.CONTACT_STATUS_RES_PACKAGE,
+ Contacts.CONTACT_STATUS_LABEL,
+ Contacts.Entity.CONTACT_ID,
+ Contacts.Entity.RAW_CONTACT_ID,
+ RawContacts.ACCOUNT_NAME,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ RawContacts.DIRTY,
+ RawContacts.VERSION,
+ RawContacts.SOURCE_ID,
+ RawContacts.SYNC1,
+ RawContacts.SYNC2,
+ RawContacts.SYNC3,
+ RawContacts.SYNC4,
+ RawContacts.DELETED,
+ Contacts.Entity.DATA_ID,
+ Data.DATA1,
+ Data.DATA2,
+ Data.DATA3,
+ Data.DATA4,
+ Data.DATA5,
+ Data.DATA6,
+ Data.DATA7,
+ Data.DATA8,
+ Data.DATA9,
+ Data.DATA10,
+ Data.DATA11,
+ Data.DATA12,
+ Data.DATA13,
+ Data.DATA14,
+ Data.DATA15,
+ Data.SYNC1,
+ Data.SYNC2,
+ Data.SYNC3,
+ Data.SYNC4,
+ Data.DATA_VERSION,
+ Data.IS_PRIMARY,
+ Data.IS_SUPER_PRIMARY,
+ Data.MIMETYPE,
+ GroupMembership.GROUP_SOURCE_ID,
+ Data.PRESENCE,
+ Data.CHAT_CAPABILITY,
+ Data.STATUS,
+ Data.STATUS_RES_PACKAGE,
+ Data.STATUS_ICON,
+ Data.STATUS_LABEL,
+ Data.STATUS_TIMESTAMP,
+ Contacts.PHOTO_URI,
+ Contacts.SEND_TO_VOICEMAIL,
+ Contacts.CUSTOM_RINGTONE,
+ Contacts.IS_USER_PROFILE,
+ Data.TIMES_USED,
+ Data.LAST_TIME_USED
+ };
+ static final String[] COLUMNS;
+
+ static {
+ List<String> projectionList = Lists.newArrayList(COLUMNS_INTERNAL);
+ if (CompatUtils.isMarshmallowCompatible()) {
+ projectionList.add(Data.CARRIER_PRESENCE);
+ }
+ COLUMNS = projectionList.toArray(new String[projectionList.size()]);
+ }
+ }
+
+ /** Projection used for the query that loads all data for the entire contact. */
+ private static class DirectoryQuery {
+
+ public static final int DISPLAY_NAME = 0;
+ public static final int PACKAGE_NAME = 1;
+ public static final int TYPE_RESOURCE_ID = 2;
+ public static final int ACCOUNT_TYPE = 3;
+ public static final int ACCOUNT_NAME = 4;
+ public static final int EXPORT_SUPPORT = 5;
+ static final String[] COLUMNS =
+ new String[] {
+ Directory.DISPLAY_NAME,
+ Directory.PACKAGE_NAME,
+ Directory.TYPE_RESOURCE_ID,
+ Directory.ACCOUNT_TYPE,
+ Directory.ACCOUNT_NAME,
+ Directory.EXPORT_SUPPORT,
+ };
+ }
+
+ private static class GroupQuery {
+
+ public static final int ACCOUNT_NAME = 0;
+ public static final int ACCOUNT_TYPE = 1;
+ public static final int DATA_SET = 2;
+ public static final int ID = 3;
+ public static final int TITLE = 4;
+ public static final int AUTO_ADD = 5;
+ public static final int FAVORITES = 6;
+ static final String[] COLUMNS =
+ new String[] {
+ Groups.ACCOUNT_NAME,
+ Groups.ACCOUNT_TYPE,
+ Groups.DATA_SET,
+ Groups._ID,
+ Groups.TITLE,
+ Groups.AUTO_ADD,
+ Groups.FAVORITES,
+ };
+ }
+
+ private static class AccountKey {
+
+ private final String mAccountName;
+ private final String mAccountType;
+ private final String mDataSet;
+
+ public AccountKey(String accountName, String accountType, String dataSet) {
+ mAccountName = accountName;
+ mAccountType = accountType;
+ mDataSet = dataSet;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mAccountName, mAccountType, mDataSet);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof AccountKey)) {
+ return false;
+ }
+ final AccountKey other = (AccountKey) obj;
+ return Objects.equals(mAccountName, other.mAccountName)
+ && Objects.equals(mAccountType, other.mAccountType)
+ && Objects.equals(mDataSet, other.mDataSet);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/RawContact.java b/java/com/android/contacts/common/model/RawContact.java
new file mode 100644
index 000000000..9efc8a878
--- /dev/null
+++ b/java/com/android/contacts/common/model/RawContact.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Entity;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import com.android.contacts.common.model.account.AccountType;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+import com.android.contacts.common.model.dataitem.DataItem;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * RawContact represents a single raw contact in the raw contacts database. It has specialized
+ * getters/setters for raw contact items, and also contains a collection of DataItem objects. A
+ * RawContact contains the information from a single account.
+ *
+ * <p>This allows RawContact objects to be thought of as a class with raw contact fields (like
+ * account type, name, data set, sync state, etc.) and a list of DataItem objects that represent
+ * contact information elements (like phone numbers, email, address, etc.).
+ */
+public final class RawContact implements Parcelable {
+
+ /** Create for building the parcelable. */
+ public static final Parcelable.Creator<RawContact> CREATOR =
+ new Parcelable.Creator<RawContact>() {
+
+ @Override
+ public RawContact createFromParcel(Parcel parcel) {
+ return new RawContact(parcel);
+ }
+
+ @Override
+ public RawContact[] newArray(int i) {
+ return new RawContact[i];
+ }
+ };
+
+ private final ContentValues mValues;
+ private final ArrayList<NamedDataItem> mDataItems;
+ private AccountTypeManager mAccountTypeManager;
+
+ /** A RawContact object can be created with or without a context. */
+ public RawContact() {
+ this(new ContentValues());
+ }
+
+ public RawContact(ContentValues values) {
+ mValues = values;
+ mDataItems = new ArrayList<NamedDataItem>();
+ }
+
+ /**
+ * Constructor for the parcelable.
+ *
+ * @param parcel The parcel to de-serialize from.
+ */
+ private RawContact(Parcel parcel) {
+ mValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ mDataItems = new ArrayList<>();
+ parcel.readTypedList(mDataItems, NamedDataItem.CREATOR);
+ }
+
+ public static RawContact createFrom(Entity entity) {
+ final ContentValues values = entity.getEntityValues();
+ final ArrayList<Entity.NamedContentValues> subValues = entity.getSubValues();
+
+ RawContact rawContact = new RawContact(values);
+ for (Entity.NamedContentValues subValue : subValues) {
+ rawContact.addNamedDataItemValues(subValue.uri, subValue.values);
+ }
+ return rawContact;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mValues, i);
+ parcel.writeTypedList(mDataItems);
+ }
+
+ public AccountTypeManager getAccountTypeManager(Context context) {
+ if (mAccountTypeManager == null) {
+ mAccountTypeManager = AccountTypeManager.getInstance(context);
+ }
+ return mAccountTypeManager;
+ }
+
+ public ContentValues getValues() {
+ return mValues;
+ }
+
+ /** Returns the id of the raw contact. */
+ public Long getId() {
+ return getValues().getAsLong(RawContacts._ID);
+ }
+
+ /** Returns the account name of the raw contact. */
+ public String getAccountName() {
+ return getValues().getAsString(RawContacts.ACCOUNT_NAME);
+ }
+
+ /** Returns the account type of the raw contact. */
+ public String getAccountTypeString() {
+ return getValues().getAsString(RawContacts.ACCOUNT_TYPE);
+ }
+
+ /** Returns the data set of the raw contact. */
+ public String getDataSet() {
+ return getValues().getAsString(RawContacts.DATA_SET);
+ }
+
+ public boolean isDirty() {
+ return getValues().getAsBoolean(RawContacts.DIRTY);
+ }
+
+ public String getSourceId() {
+ return getValues().getAsString(RawContacts.SOURCE_ID);
+ }
+
+ public String getSync1() {
+ return getValues().getAsString(RawContacts.SYNC1);
+ }
+
+ public String getSync2() {
+ return getValues().getAsString(RawContacts.SYNC2);
+ }
+
+ public String getSync3() {
+ return getValues().getAsString(RawContacts.SYNC3);
+ }
+
+ public String getSync4() {
+ return getValues().getAsString(RawContacts.SYNC4);
+ }
+
+ public boolean isDeleted() {
+ return getValues().getAsBoolean(RawContacts.DELETED);
+ }
+
+ public long getContactId() {
+ return getValues().getAsLong(Contacts.Entity.CONTACT_ID);
+ }
+
+ public boolean isStarred() {
+ return getValues().getAsBoolean(Contacts.STARRED);
+ }
+
+ public AccountType getAccountType(Context context) {
+ return getAccountTypeManager(context).getAccountType(getAccountTypeString(), getDataSet());
+ }
+
+ /**
+ * Sets the account name, account type, and data set strings. Valid combinations for account-name,
+ * account-type, data-set 1) null, null, null (local account) 2) non-null, non-null, null (valid
+ * account without data-set) 3) non-null, non-null, non-null (valid account with data-set)
+ */
+ private void setAccount(String accountName, String accountType, String dataSet) {
+ final ContentValues values = getValues();
+ if (accountName == null) {
+ if (accountType == null && dataSet == null) {
+ // This is a local account
+ values.putNull(RawContacts.ACCOUNT_NAME);
+ values.putNull(RawContacts.ACCOUNT_TYPE);
+ values.putNull(RawContacts.DATA_SET);
+ return;
+ }
+ } else {
+ if (accountType != null) {
+ // This is a valid account, either with or without a dataSet.
+ values.put(RawContacts.ACCOUNT_NAME, accountName);
+ values.put(RawContacts.ACCOUNT_TYPE, accountType);
+ if (dataSet == null) {
+ values.putNull(RawContacts.DATA_SET);
+ } else {
+ values.put(RawContacts.DATA_SET, dataSet);
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException(
+ "Not a valid combination of account name, type, and data set.");
+ }
+
+ public void setAccount(AccountWithDataSet accountWithDataSet) {
+ if (accountWithDataSet != null) {
+ setAccount(accountWithDataSet.name, accountWithDataSet.type, accountWithDataSet.dataSet);
+ } else {
+ setAccount(null, null, null);
+ }
+ }
+
+ public void setAccountToLocal() {
+ setAccount(null, null, null);
+ }
+
+ /** Creates and inserts a DataItem object that wraps the content values, and returns it. */
+ public void addDataItemValues(ContentValues values) {
+ addNamedDataItemValues(Data.CONTENT_URI, values);
+ }
+
+ public NamedDataItem addNamedDataItemValues(Uri uri, ContentValues values) {
+ final NamedDataItem namedItem = new NamedDataItem(uri, values);
+ mDataItems.add(namedItem);
+ return namedItem;
+ }
+
+ public ArrayList<ContentValues> getContentValues() {
+ final ArrayList<ContentValues> list = new ArrayList<>(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(dataItem.mContentValues);
+ }
+ }
+ return list;
+ }
+
+ public List<DataItem> getDataItems() {
+ final ArrayList<DataItem> list = new ArrayList<>(mDataItems.size());
+ for (NamedDataItem dataItem : mDataItems) {
+ if (Data.CONTENT_URI.equals(dataItem.mUri)) {
+ list.add(DataItem.createFrom(dataItem.mContentValues));
+ }
+ }
+ return list;
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("RawContact: ").append(mValues);
+ for (RawContact.NamedDataItem namedDataItem : mDataItems) {
+ sb.append("\n ").append(namedDataItem.mUri);
+ sb.append("\n -> ").append(namedDataItem.mContentValues);
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mValues, mDataItems);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ RawContact other = (RawContact) obj;
+ return Objects.equals(mValues, other.mValues) && Objects.equals(mDataItems, other.mDataItems);
+ }
+
+ public static final class NamedDataItem implements Parcelable {
+
+ public static final Parcelable.Creator<NamedDataItem> CREATOR =
+ new Parcelable.Creator<NamedDataItem>() {
+
+ @Override
+ public NamedDataItem createFromParcel(Parcel parcel) {
+ return new NamedDataItem(parcel);
+ }
+
+ @Override
+ public NamedDataItem[] newArray(int i) {
+ return new NamedDataItem[i];
+ }
+ };
+ public final Uri mUri;
+ // This use to be a DataItem. DataItem creation is now delayed until the point of request
+ // since there is no benefit to storing them here due to the multiple inheritance.
+ // Eventually instanceof still has to be used anyways to determine which sub-class of
+ // DataItem it is. And having parent DataItem's here makes it very difficult to serialize or
+ // parcelable.
+ //
+ // Instead of having a common DataItem super class, we should refactor this to be a generic
+ // Object where the object is a concrete class that no longer relies on ContentValues.
+ // (this will also make the classes easier to use).
+ // Since instanceof is used later anyways, having a list of Objects won't hurt and is no
+ // worse than having a DataItem.
+ public final ContentValues mContentValues;
+
+ public NamedDataItem(Uri uri, ContentValues values) {
+ this.mUri = uri;
+ this.mContentValues = values;
+ }
+
+ public NamedDataItem(Parcel parcel) {
+ this.mUri = parcel.readParcelable(Uri.class.getClassLoader());
+ this.mContentValues = parcel.readParcelable(ContentValues.class.getClassLoader());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeParcelable(mUri, i);
+ parcel.writeParcelable(mContentValues, i);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUri, mContentValues);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final NamedDataItem other = (NamedDataItem) obj;
+ return Objects.equals(mUri, other.mUri)
+ && Objects.equals(mContentValues, other.mContentValues);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountType.java b/java/com/android/contacts/common/model/account/AccountType.java
new file mode 100644
index 000000000..1ae485a5f
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountType.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Internal structure that represents constraints and styles for a specific data source, such as the
+ * various data types they support, including details on how those types should be rendered and
+ * edited.
+ *
+ * <p>In the future this may be inflated from XML defined by a data source.
+ */
+public abstract class AccountType {
+
+ private static final String TAG = "AccountType";
+ /** {@link Comparator} to sort by {@link DataKind#weight}. */
+ private static Comparator<DataKind> sWeightComparator =
+ new Comparator<DataKind>() {
+ @Override
+ public int compare(DataKind object1, DataKind object2) {
+ return object1.weight - object2.weight;
+ }
+ };
+ /** The {@link RawContacts#ACCOUNT_TYPE} these constraints apply to. */
+ public String accountType = null;
+ /** The {@link RawContacts#DATA_SET} these constraints apply to. */
+ public String dataSet = null;
+ /**
+ * Package that resources should be loaded from. Will be null for embedded types, in which case
+ * resources are stored in this package itself.
+ *
+ * <p>TODO Clean up {@link #resourcePackageName}, {@link #syncAdapterPackageName} and {@link
+ * #getViewContactNotifyServicePackageName()}.
+ *
+ * <p>There's the following invariants: - {@link #syncAdapterPackageName} is always set to the
+ * actual sync adapter package name. - {@link #resourcePackageName} too is set to the same value,
+ * unless {@link #isEmbedded()}, in which case it'll be null. There's an unfortunate exception of
+ * {@link FallbackAccountType}. Even though it {@link #isEmbedded()}, but we set non-null to
+ * {@link #resourcePackageName} for unit tests.
+ */
+ public String resourcePackageName;
+ /**
+ * The package name for the authenticator (for the embedded types, i.e. Google and Exchange) or
+ * the sync adapter (for external type, including extensions).
+ */
+ public String syncAdapterPackageName;
+
+ public int titleRes;
+ public int iconRes;
+ protected boolean mIsInitialized;
+ /** Set of {@link DataKind} supported by this source. */
+ private ArrayList<DataKind> mKinds = new ArrayList<>();
+ /** Lookup map of {@link #mKinds} on {@link DataKind#mimeType}. */
+ private Map<String, DataKind> mMimeKinds = new ArrayMap<>();
+
+ /**
+ * Return a string resource loaded from the given package (or the current package if {@code
+ * packageName} is null), unless {@code resId} is -1, in which case it returns {@code
+ * defaultValue}.
+ *
+ * <p>(The behavior is undefined if the resource or package doesn't exist.)
+ */
+ @VisibleForTesting
+ static CharSequence getResourceText(
+ Context context, String packageName, int resId, String defaultValue) {
+ if (resId != -1 && packageName != null) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getText(packageName, resId, null);
+ } else if (resId != -1) {
+ return context.getText(resId);
+ } else {
+ return defaultValue;
+ }
+ }
+
+ public static Drawable getDisplayIcon(
+ Context context, int titleRes, int iconRes, String syncAdapterPackageName) {
+ if (titleRes != -1 && syncAdapterPackageName != null) {
+ final PackageManager pm = context.getPackageManager();
+ return pm.getDrawable(syncAdapterPackageName, iconRes, null);
+ } else if (titleRes != -1) {
+ return context.getResources().getDrawable(iconRes);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Whether this account type was able to be fully initialized. This may be false if (for example)
+ * the package name associated with the account type could not be found.
+ */
+ public final boolean isInitialized() {
+ return mIsInitialized;
+ }
+
+ /**
+ * @return Whether this type is an "embedded" type. i.e. any of {@link FallbackAccountType},
+ * {@link GoogleAccountType} or {@link ExternalAccountType}.
+ * <p>If an embedded type cannot be initialized (i.e. if {@link #isInitialized()} returns
+ * {@code false}) it's considered critical, and the application will crash. On the other hand
+ * if it's not an embedded type, we just skip loading the type.
+ */
+ public boolean isEmbedded() {
+ return true;
+ }
+
+ public boolean isExtension() {
+ return false;
+ }
+
+ /**
+ * @return True if contacts can be created and edited using this app. If false, there could still
+ * be an external editor as provided by {@link #getEditContactActivityClassName()} or {@link
+ * #getCreateContactActivityClassName()}
+ */
+ public abstract boolean areContactsWritable();
+
+ /**
+ * Returns an optional custom edit activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getEditContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional custom new contact activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getCreateContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional custom invite contact activity.
+ *
+ * <p>Only makes sense for non-embedded account types. The activity class should reside in the
+ * sync adapter package as determined by {@link #syncAdapterPackageName}.
+ */
+ public String getInviteContactActivityClassName() {
+ return null;
+ }
+
+ /**
+ * Returns an optional service that can be launched whenever a contact is being looked at. This
+ * allows the sync adapter to provide more up-to-date information.
+ *
+ * <p>The service class should reside in the sync adapter package as determined by {@link
+ * #getViewContactNotifyServicePackageName()}.
+ */
+ public String getViewContactNotifyServiceClassName() {
+ return null;
+ }
+
+ /**
+ * TODO This is way too hacky should be removed.
+ *
+ * <p>This is introduced for {@link GoogleAccountType} where {@link #syncAdapterPackageName} is
+ * the authenticator package name but the notification service is in the sync adapter package. See
+ * {@link #resourcePackageName} -- we should clean up those.
+ */
+ public String getViewContactNotifyServicePackageName() {
+ return syncAdapterPackageName;
+ }
+
+ /** Returns an optional Activity string that can be used to view the group. */
+ public String getViewGroupActivity() {
+ return null;
+ }
+
+ public CharSequence getDisplayLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ return getResourceText(context, syncAdapterPackageName, titleRes, accountType);
+ }
+
+ /** @return resource ID for the "invite contact" action label, or -1 if not defined. */
+ protected int getInviteContactActionResId() {
+ return -1;
+ }
+
+ /** @return resource ID for the "view group" label, or -1 if not defined. */
+ protected int getViewGroupLabelResId() {
+ return -1;
+ }
+
+ /** Returns {@link AccountTypeWithDataSet} for this type. */
+ public AccountTypeWithDataSet getAccountTypeAndDataSet() {
+ return AccountTypeWithDataSet.get(accountType, dataSet);
+ }
+
+ /**
+ * Returns a list of additional package names that should be inspected as additional external
+ * account types. This allows for a primary account type to indicate other packages that may not
+ * be sync adapters but which still provide contact data, perhaps under a separate data set within
+ * the account.
+ */
+ public List<String> getExtensionPackageNames() {
+ return new ArrayList<String>();
+ }
+
+ /**
+ * Returns an optional custom label for the "invite contact" action, which will be shown on the
+ * contact card. (If not defined, returns null.)
+ */
+ public CharSequence getInviteContactActionLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ return getResourceText(context, syncAdapterPackageName, getInviteContactActionResId(), "");
+ }
+
+ /**
+ * Returns a label for the "view group" action. If not defined, this falls back to our own "View
+ * Updates" string
+ */
+ public CharSequence getViewGroupLabel(Context context) {
+ // Note this resource is defined in the sync adapter package, not resourcePackageName.
+ final CharSequence customTitle =
+ getResourceText(context, syncAdapterPackageName, getViewGroupLabelResId(), null);
+
+ return customTitle == null ? context.getText(R.string.view_updates_from_group) : customTitle;
+ }
+
+ public Drawable getDisplayIcon(Context context) {
+ return getDisplayIcon(context, titleRes, iconRes, syncAdapterPackageName);
+ }
+
+ /** Whether or not groups created under this account type have editable membership lists. */
+ public abstract boolean isGroupMembershipEditable();
+
+ /** Return list of {@link DataKind} supported, sorted by {@link DataKind#weight}. */
+ public ArrayList<DataKind> getSortedDataKinds() {
+ // TODO: optimize by marking if already sorted
+ Collections.sort(mKinds, sWeightComparator);
+ return mKinds;
+ }
+
+ /** Find the {@link DataKind} for a specific MIME-type, if it's handled by this data source. */
+ public DataKind getKindForMimetype(String mimeType) {
+ return this.mMimeKinds.get(mimeType);
+ }
+
+ /** Add given {@link DataKind} to list of those provided by this source. */
+ public DataKind addKind(DataKind kind) throws DefinitionException {
+ if (kind.mimeType == null) {
+ throw new DefinitionException("null is not a valid mime type");
+ }
+ if (mMimeKinds.get(kind.mimeType) != null) {
+ throw new DefinitionException("mime type '" + kind.mimeType + "' is already registered");
+ }
+
+ kind.resourcePackageName = this.resourcePackageName;
+ this.mKinds.add(kind);
+ this.mMimeKinds.put(kind.mimeType, kind);
+ return kind;
+ }
+
+ /**
+ * Generic method of inflating a given {@link ContentValues} into a user-readable {@link
+ * CharSequence}. For example, an inflater could combine the multiple columns of {@link
+ * StructuredPostal} together using a string resource before presenting to the user.
+ */
+ public interface StringInflater {
+
+ CharSequence inflateUsing(Context context, ContentValues values);
+ }
+
+ protected static class DefinitionException extends Exception {
+
+ public DefinitionException(String message) {
+ super(message);
+ }
+
+ public DefinitionException(String message, Exception inner) {
+ super(message, inner);
+ }
+ }
+
+ /**
+ * Description of a specific "type" or "label" of a {@link DataKind} row, such as {@link
+ * Phone#TYPE_WORK}. Includes constraints on total number of rows a {@link Contacts} may have of
+ * this type, and details on how user-defined labels are stored.
+ */
+ public static class EditType {
+
+ public int rawValue;
+ public int labelRes;
+ public boolean secondary;
+ /**
+ * The number of entries allowed for the type. -1 if not specified.
+ *
+ * @see DataKind#typeOverallMax
+ */
+ public int specificMax;
+
+ public String customColumn;
+
+ public EditType(int rawValue, int labelRes) {
+ this.rawValue = rawValue;
+ this.labelRes = labelRes;
+ this.specificMax = -1;
+ }
+
+ public EditType setSecondary(boolean secondary) {
+ this.secondary = secondary;
+ return this;
+ }
+
+ public EditType setSpecificMax(int specificMax) {
+ this.specificMax = specificMax;
+ return this;
+ }
+
+ public EditType setCustomColumn(String customColumn) {
+ this.customColumn = customColumn;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (object instanceof EditType) {
+ final EditType other = (EditType) object;
+ return other.rawValue == rawValue;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return rawValue;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + " rawValue="
+ + rawValue
+ + " labelRes="
+ + labelRes
+ + " secondary="
+ + secondary
+ + " specificMax="
+ + specificMax
+ + " customColumn="
+ + customColumn;
+ }
+ }
+
+ public static class EventEditType extends EditType {
+
+ private boolean mYearOptional;
+
+ public EventEditType(int rawValue, int labelRes) {
+ super(rawValue, labelRes);
+ }
+
+ public boolean isYearOptional() {
+ return mYearOptional;
+ }
+
+ public EventEditType setYearOptional(boolean yearOptional) {
+ mYearOptional = yearOptional;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + " mYearOptional=" + mYearOptional;
+ }
+ }
+
+ /**
+ * Description of a user-editable field on a {@link DataKind} row, such as {@link Phone#NUMBER}.
+ * Includes flags to apply to an {@link EditText}, and the column where this field is stored.
+ */
+ public static final class EditField {
+
+ public String column;
+ public int titleRes;
+ public int inputType;
+ public int minLines;
+ public boolean optional;
+ public boolean shortForm;
+ public boolean longForm;
+
+ public EditField(String column, int titleRes) {
+ this.column = column;
+ this.titleRes = titleRes;
+ }
+
+ public EditField(String column, int titleRes, int inputType) {
+ this(column, titleRes);
+ this.inputType = inputType;
+ }
+
+ public EditField setOptional(boolean optional) {
+ this.optional = optional;
+ return this;
+ }
+
+ public EditField setShortForm(boolean shortForm) {
+ this.shortForm = shortForm;
+ return this;
+ }
+
+ public EditField setLongForm(boolean longForm) {
+ this.longForm = longForm;
+ return this;
+ }
+
+ public EditField setMinLines(int minLines) {
+ this.minLines = minLines;
+ return this;
+ }
+
+ public boolean isMultiLine() {
+ return (inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) != 0;
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + ":"
+ + " column="
+ + column
+ + " titleRes="
+ + titleRes
+ + " inputType="
+ + inputType
+ + " minLines="
+ + minLines
+ + " optional="
+ + optional
+ + " shortForm="
+ + shortForm
+ + " longForm="
+ + longForm;
+ }
+ }
+
+ /**
+ * Compare two {@link AccountType} by their {@link AccountType#getDisplayLabel} with the current
+ * locale.
+ */
+ public static class DisplayLabelComparator implements Comparator<AccountType> {
+
+ private final Context mContext;
+ /** {@link Comparator} for the current locale. */
+ private final Collator mCollator = Collator.getInstance();
+
+ public DisplayLabelComparator(Context context) {
+ mContext = context;
+ }
+
+ private String getDisplayLabel(AccountType type) {
+ CharSequence label = type.getDisplayLabel(mContext);
+ return (label == null) ? "" : label.toString();
+ }
+
+ @Override
+ public int compare(AccountType lhs, AccountType rhs) {
+ return mCollator.compare(getDisplayLabel(lhs), getDisplayLabel(rhs));
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
new file mode 100644
index 000000000..a32ebe139
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountTypeWithDataSet.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import java.util.Objects;
+
+/** Encapsulates an "account type" string and a "data set" string. */
+public class AccountTypeWithDataSet {
+
+ private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+ private static final Uri RAW_CONTACTS_URI_LIMIT_1 =
+ RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1")
+ .build();
+
+ /** account type. Can be null for fallback type. */
+ public final String accountType;
+
+ /** dataSet may be null, but never be "". */
+ public final String dataSet;
+
+ private AccountTypeWithDataSet(String accountType, String dataSet) {
+ this.accountType = TextUtils.isEmpty(accountType) ? null : accountType;
+ this.dataSet = TextUtils.isEmpty(dataSet) ? null : dataSet;
+ }
+
+ public static AccountTypeWithDataSet get(String accountType, String dataSet) {
+ return new AccountTypeWithDataSet(accountType, dataSet);
+ }
+
+ /**
+ * Return true if there are any contacts in the database with this account type and data set.
+ * Touches DB. Don't use in the UI thread.
+ */
+ public boolean hasData(Context context) {
+ final String BASE_SELECTION = RawContacts.ACCOUNT_TYPE + " = ?";
+ final String selection;
+ final String[] args;
+ if (TextUtils.isEmpty(dataSet)) {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+ args = new String[] {accountType};
+ } else {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+ args = new String[] {accountType, dataSet};
+ }
+
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null);
+ if (c == null) {
+ return false;
+ }
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof AccountTypeWithDataSet)) {
+ return false;
+ }
+
+ AccountTypeWithDataSet other = (AccountTypeWithDataSet) o;
+ return Objects.equals(accountType, other.accountType) && Objects.equals(dataSet, other.dataSet);
+ }
+
+ @Override
+ public int hashCode() {
+ return (accountType == null ? 0 : accountType.hashCode())
+ ^ (dataSet == null ? 0 : dataSet.hashCode());
+ }
+
+ @Override
+ public String toString() {
+ return "[" + accountType + "/" + dataSet + "]";
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/AccountWithDataSet.java b/java/com/android/contacts/common/model/account/AccountWithDataSet.java
new file mode 100644
index 000000000..71faf509c
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/AccountWithDataSet.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/** Wrapper for an account that includes a data set (which may be null). */
+public class AccountWithDataSet implements Parcelable {
+
+ // For Parcelable
+ public static final Creator<AccountWithDataSet> CREATOR =
+ new Creator<AccountWithDataSet>() {
+ public AccountWithDataSet createFromParcel(Parcel source) {
+ return new AccountWithDataSet(source);
+ }
+
+ public AccountWithDataSet[] newArray(int size) {
+ return new AccountWithDataSet[size];
+ }
+ };
+ private static final String STRINGIFY_SEPARATOR = "\u0001";
+ private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002";
+ private static final Pattern STRINGIFY_SEPARATOR_PAT =
+ Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR));
+ private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT =
+ Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR));
+ private static final String[] ID_PROJECTION = new String[] {BaseColumns._ID};
+ private static final Uri RAW_CONTACTS_URI_LIMIT_1 =
+ RawContacts.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, "1")
+ .build();
+ public final String name;
+ public final String type;
+ public final String dataSet;
+ private final AccountTypeWithDataSet mAccountTypeWithDataSet;
+
+ public AccountWithDataSet(String name, String type, String dataSet) {
+ this.name = emptyToNull(name);
+ this.type = emptyToNull(type);
+ this.dataSet = emptyToNull(dataSet);
+ mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+ }
+
+ public AccountWithDataSet(Parcel in) {
+ this.name = in.readString();
+ this.type = in.readString();
+ this.dataSet = in.readString();
+ mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
+ }
+
+ private static String emptyToNull(String text) {
+ return TextUtils.isEmpty(text) ? null : text;
+ }
+
+ private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+ if (!TextUtils.isEmpty(account.name)) {
+ sb.append(account.name);
+ }
+ sb.append(STRINGIFY_SEPARATOR);
+ if (!TextUtils.isEmpty(account.type)) {
+ sb.append(account.type);
+ }
+ sb.append(STRINGIFY_SEPARATOR);
+ if (!TextUtils.isEmpty(account.dataSet)) {
+ sb.append(account.dataSet);
+ }
+
+ return sb;
+ }
+
+ /**
+ * Unpack a string created by {@link #stringify}.
+ *
+ * @throws IllegalArgumentException if it's an invalid string.
+ */
+ public static AccountWithDataSet unstringify(String s) {
+ final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3);
+ if (array.length < 3) {
+ throw new IllegalArgumentException("Invalid string " + s);
+ }
+ return new AccountWithDataSet(
+ array[0], array[1], TextUtils.isEmpty(array[2]) ? null : array[2]);
+ }
+
+ /** Pack a list of {@link AccountWithDataSet} into a string. */
+ public static String stringifyList(List<AccountWithDataSet> accounts) {
+ final StringBuilder sb = new StringBuilder();
+
+ for (AccountWithDataSet account : accounts) {
+ if (sb.length() > 0) {
+ sb.append(ARRAY_STRINGIFY_SEPARATOR);
+ }
+ addStringified(sb, account);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Unpack a list of {@link AccountWithDataSet} into a string.
+ *
+ * @throws IllegalArgumentException if it's an invalid string.
+ */
+ public static List<AccountWithDataSet> unstringifyList(String s) {
+ final ArrayList<AccountWithDataSet> ret = new ArrayList<>();
+ if (TextUtils.isEmpty(s)) {
+ return ret;
+ }
+
+ final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s);
+
+ for (int i = 0; i < array.length; i++) {
+ ret.add(unstringify(array[i]));
+ }
+
+ return ret;
+ }
+
+ public boolean isLocalAccount() {
+ return name == null && type == null;
+ }
+
+ public Account getAccountOrNull() {
+ if (name != null && type != null) {
+ return new Account(name, type);
+ }
+ return null;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(name);
+ dest.writeString(type);
+ dest.writeString(dataSet);
+ }
+
+ public AccountTypeWithDataSet getAccountTypeWithDataSet() {
+ return mAccountTypeWithDataSet;
+ }
+
+ /**
+ * Return {@code true} if this account has any contacts in the database. Touches DB. Don't use in
+ * the UI thread.
+ */
+ public boolean hasData(Context context) {
+ final String BASE_SELECTION =
+ RawContacts.ACCOUNT_TYPE + " = ?" + " AND " + RawContacts.ACCOUNT_NAME + " = ?";
+ final String selection;
+ final String[] args;
+ if (TextUtils.isEmpty(dataSet)) {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " IS NULL";
+ args = new String[] {type, name};
+ } else {
+ selection = BASE_SELECTION + " AND " + RawContacts.DATA_SET + " = ?";
+ args = new String[] {type, name, dataSet};
+ }
+
+ final Cursor c =
+ context
+ .getContentResolver()
+ .query(RAW_CONTACTS_URI_LIMIT_1, ID_PROJECTION, selection, args, null);
+ if (c == null) {
+ return false;
+ }
+ try {
+ return c.moveToFirst();
+ } finally {
+ c.close();
+ }
+ }
+
+ public boolean equals(Object obj) {
+ if (obj instanceof AccountWithDataSet) {
+ AccountWithDataSet other = (AccountWithDataSet) obj;
+ return Objects.equals(name, other.name)
+ && Objects.equals(type, other.type)
+ && Objects.equals(dataSet, other.dataSet);
+ }
+ return false;
+ }
+
+ public int hashCode() {
+ int result = 17;
+ result = 31 * result + (name != null ? name.hashCode() : 0);
+ result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + (dataSet != null ? dataSet.hashCode() : 0);
+ return result;
+ }
+
+ public String toString() {
+ return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
+ }
+
+ /** Pack the instance into a string. */
+ public String stringify() {
+ return addStringified(new StringBuilder(), this).toString();
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/BaseAccountType.java b/java/com/android/contacts/common/model/account/BaseAccountType.java
new file mode 100644
index 000000000..21b555917
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/BaseAccountType.java
@@ -0,0 +1,1890 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.Resources;
+import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.inputmethod.EditorInfo;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public abstract class BaseAccountType extends AccountType {
+
+ public static final StringInflater ORGANIZATION_BODY_INFLATER =
+ new StringInflater() {
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final CharSequence companyValue =
+ values.containsKey(Organization.COMPANY)
+ ? values.getAsString(Organization.COMPANY)
+ : null;
+ final CharSequence titleValue =
+ values.containsKey(Organization.TITLE)
+ ? values.getAsString(Organization.TITLE)
+ : null;
+
+ if (companyValue != null && titleValue != null) {
+ return companyValue + ": " + titleValue;
+ } else if (companyValue == null) {
+ return titleValue;
+ } else {
+ return companyValue;
+ }
+ }
+ };
+ protected static final int FLAGS_PHONE = EditorInfo.TYPE_CLASS_PHONE;
+ protected static final int FLAGS_EMAIL =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+ protected static final int FLAGS_PERSON_NAME =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+ protected static final int FLAGS_PHONETIC =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PHONETIC;
+ protected static final int FLAGS_GENERIC_NAME =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+ protected static final int FLAGS_NOTE =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ protected static final int FLAGS_EVENT = EditorInfo.TYPE_CLASS_TEXT;
+ protected static final int FLAGS_WEBSITE =
+ EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_URI;
+ protected static final int FLAGS_POSTAL =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_POSTAL_ADDRESS
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+ protected static final int FLAGS_SIP_ADDRESS =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; // since SIP addresses have the same
+ // basic format as email addresses
+ protected static final int FLAGS_RELATION =
+ EditorInfo.TYPE_CLASS_TEXT
+ | EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS
+ | EditorInfo.TYPE_TEXT_VARIATION_PERSON_NAME;
+
+ // Specify the maximum number of lines that can be used to display various field types. If no
+ // value is specified for a particular type, we use the default value from {@link DataKind}.
+ protected static final int MAX_LINES_FOR_POSTAL_ADDRESS = 10;
+ protected static final int MAX_LINES_FOR_GROUP = 10;
+ protected static final int MAX_LINES_FOR_NOTE = 100;
+ private static final String TAG = "BaseAccountType";
+
+ public BaseAccountType() {
+ this.accountType = null;
+ this.dataSet = null;
+ this.titleRes = R.string.account_phone;
+ this.iconRes = R.mipmap.ic_contacts_launcher;
+ }
+
+ protected static EditType buildPhoneType(int type) {
+ return new EditType(type, Phone.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildEmailType(int type) {
+ return new EditType(type, Email.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildPostalType(int type) {
+ return new EditType(type, StructuredPostal.getTypeLabelResource(type));
+ }
+
+ protected static EditType buildImType(int type) {
+ return new EditType(type, Im.getProtocolLabelResource(type));
+ }
+
+ protected static EditType buildEventType(int type, boolean yearOptional) {
+ return new EventEditType(type, Event.getTypeResource(type)).setYearOptional(yearOptional);
+ }
+
+ protected static EditType buildRelationType(int type) {
+ return new EditType(type, Relation.getTypeLabelResource(type));
+ }
+
+ // Utility methods to keep code shorter.
+ private static boolean getAttr(AttributeSet attrs, String attribute, boolean defaultValue) {
+ return attrs.getAttributeBooleanValue(null, attribute, defaultValue);
+ }
+
+ private static int getAttr(AttributeSet attrs, String attribute, int defaultValue) {
+ return attrs.getAttributeIntValue(null, attribute, defaultValue);
+ }
+
+ private static String getAttr(AttributeSet attrs, String attribute) {
+ return attrs.getAttributeValue(null, attribute);
+ }
+
+ protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
+ .setShortForm(true));
+
+ boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ if (!displayOrderPrimary) {
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ }
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ R.string.name_phonetic,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
+ .setShortForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC)
+ .setLongForm(true));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
+ .setLongForm(true));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Nickname.CONTENT_ITEM_TYPE, R.string.nicknameLabelsGroup, Weight.NICKNAME, true));
+ kind.typeOverallMax = 1;
+ kind.actionHeader = new SimpleInflater(R.string.nicknameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Phone.CONTENT_ITEM_TYPE, R.string.phoneLabelsGroup, Weight.PHONE, true));
+ kind.iconAltRes = R.drawable.ic_message_24dp;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionHeader = new PhoneActionInflater();
+ kind.actionAltHeader = new PhoneActionAltInflater();
+ kind.actionBody = new SimpleInflater(Phone.NUMBER);
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CALLBACK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ISDN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER_FAX).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_TELEX).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_TTY_TDD).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_MOBILE).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Email.CONTENT_ITEM_TYPE, R.string.emailLabelsGroup, Weight.EMAIL, true));
+ kind.actionHeader = new EmailActionInflater();
+ kind.actionBody = new SimpleInflater(Email.DATA);
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(buildEmailType(Email.TYPE_MOBILE));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ R.string.postalLabelsGroup,
+ Weight.STRUCTURED_POSTAL,
+ true));
+ kind.actionHeader = new PostalActionInflater();
+ kind.actionBody = new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS);
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER));
+ kind.typeList.add(
+ buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(StructuredPostal.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindIm(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Im.CONTENT_ITEM_TYPE, R.string.imLabelsGroup, Weight.IM, true));
+ kind.actionHeader = new ImActionInflater();
+ kind.actionBody = new SimpleInflater(Im.DATA);
+
+ // NOTE: even though a traditional "type" exists, for editing
+ // purposes we're using the protocol to pick labels
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.typeColumn = Im.PROTOCOL;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildImType(Im.PROTOCOL_AIM));
+ kind.typeList.add(buildImType(Im.PROTOCOL_MSN));
+ kind.typeList.add(buildImType(Im.PROTOCOL_YAHOO));
+ kind.typeList.add(buildImType(Im.PROTOCOL_SKYPE));
+ kind.typeList.add(buildImType(Im.PROTOCOL_QQ));
+ kind.typeList.add(buildImType(Im.PROTOCOL_GOOGLE_TALK));
+ kind.typeList.add(buildImType(Im.PROTOCOL_ICQ));
+ kind.typeList.add(buildImType(Im.PROTOCOL_JABBER));
+ kind.typeList.add(
+ buildImType(Im.PROTOCOL_CUSTOM).setSecondary(true).setCustomColumn(Im.CUSTOM_PROTOCOL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Organization.CONTENT_ITEM_TYPE,
+ R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.organizationLabelsGroup);
+ kind.actionBody = ORGANIZATION_BODY_INFLATER;
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+ DataKind kind = addKind(new DataKind(Photo.CONTENT_ITEM_TYPE, -1, Weight.NONE, true));
+ kind.typeOverallMax = 1;
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+ return kind;
+ }
+
+ protected DataKind addDataKindNote(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Note.CONTENT_ITEM_TYPE, R.string.label_notes, Weight.NOTE, true));
+ kind.typeOverallMax = 1;
+ kind.actionHeader = new SimpleInflater(R.string.label_notes);
+ kind.actionBody = new SimpleInflater(Note.NOTE);
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Website.CONTENT_ITEM_TYPE, R.string.websiteLabelsGroup, Weight.WEBSITE, true));
+ kind.actionHeader = new SimpleInflater(R.string.websiteLabelsGroup);
+ kind.actionBody = new SimpleInflater(Website.URL);
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindSipAddress(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ SipAddress.CONTENT_ITEM_TYPE,
+ R.string.label_sip_address,
+ Weight.SIP_ADDRESS,
+ true));
+
+ kind.actionHeader = new SimpleInflater(R.string.label_sip_address);
+ kind.actionBody = new SimpleInflater(SipAddress.SIP_ADDRESS);
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+ kind.typeOverallMax = 1;
+
+ return kind;
+ }
+
+ protected DataKind addDataKindGroupMembership(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ GroupMembership.CONTENT_ITEM_TYPE,
+ R.string.groupsLabel,
+ Weight.GROUP_MEMBERSHIP,
+ true));
+
+ kind.typeOverallMax = 1;
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+
+ kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return false;
+ }
+
+ /** Parses the content of the EditSchema tag in contacts.xml. */
+ protected final void parseEditSchema(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws XmlPullParserException, IOException, DefinitionException {
+
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+
+ if (Tag.DATA_KIND.equals(tag)) {
+ for (DataKind kind : KindParser.INSTANCE.parseDataKindTag(context, parser, attrs)) {
+ addKind(kind);
+ }
+ } else {
+ Log.w(TAG, "Skipping unknown tag " + tag);
+ }
+ }
+ }
+
+ private interface Tag {
+
+ String DATA_KIND = "DataKind";
+ String TYPE = "Type";
+ }
+
+ private interface Attr {
+
+ String MAX_OCCURRENCE = "maxOccurs";
+ String DATE_WITH_TIME = "dateWithTime";
+ String YEAR_OPTIONAL = "yearOptional";
+ String KIND = "kind";
+ String TYPE = "type";
+ }
+
+ protected interface Weight {
+
+ int NONE = -1;
+ int PHONE = 10;
+ int EMAIL = 15;
+ int STRUCTURED_POSTAL = 25;
+ int NICKNAME = 111;
+ int EVENT = 120;
+ int ORGANIZATION = 125;
+ int NOTE = 130;
+ int IM = 140;
+ int SIP_ADDRESS = 145;
+ int GROUP_MEMBERSHIP = 150;
+ int WEBSITE = 160;
+ int RELATIONSHIP = 999;
+ }
+
+ /**
+ * Simple inflater that assumes a string resource has a "%s" that will be filled from the given
+ * column.
+ */
+ public static class SimpleInflater implements StringInflater {
+
+ private final int mStringRes;
+ private final String mColumnName;
+
+ public SimpleInflater(int stringRes) {
+ this(stringRes, null);
+ }
+
+ public SimpleInflater(String columnName) {
+ this(-1, columnName);
+ }
+
+ public SimpleInflater(int stringRes, String columnName) {
+ mStringRes = stringRes;
+ mColumnName = columnName;
+ }
+
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final boolean validColumn = values.containsKey(mColumnName);
+ final boolean validString = mStringRes > 0;
+
+ final CharSequence stringValue = validString ? context.getText(mStringRes) : null;
+ final CharSequence columnValue = validColumn ? values.getAsString(mColumnName) : null;
+
+ if (validString && validColumn) {
+ return String.format(stringValue.toString(), columnValue);
+ } else if (validString) {
+ return stringValue;
+ } else if (validColumn) {
+ return columnValue;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName()
+ + " mStringRes="
+ + mStringRes
+ + " mColumnName"
+ + mColumnName;
+ }
+
+ public String getColumnNameForTest() {
+ return mColumnName;
+ }
+ }
+
+ public abstract static class CommonInflater implements StringInflater {
+
+ protected abstract int getTypeLabelResource(Integer type);
+
+ protected boolean isCustom(Integer type) {
+ return type == BaseTypes.TYPE_CUSTOM;
+ }
+
+ protected String getTypeColumn() {
+ return Phone.TYPE;
+ }
+
+ protected String getLabelColumn() {
+ return Phone.LABEL;
+ }
+
+ protected CharSequence getTypeLabel(Resources res, Integer type, CharSequence label) {
+ final int labelRes = getTypeLabelResource(type);
+ if (type == null) {
+ return res.getText(labelRes);
+ } else if (isCustom(type)) {
+ return res.getString(labelRes, label == null ? "" : label);
+ } else {
+ return res.getText(labelRes);
+ }
+ }
+
+ @Override
+ public CharSequence inflateUsing(Context context, ContentValues values) {
+ final Integer type = values.getAsInteger(getTypeColumn());
+ final String label = values.getAsString(getLabelColumn());
+ return getTypeLabel(context.getResources(), type, label);
+ }
+
+ @Override
+ public String toString() {
+ return this.getClass().getSimpleName();
+ }
+ }
+
+ public static class PhoneActionInflater extends CommonInflater {
+
+ @Override
+ protected boolean isCustom(Integer type) {
+ return ContactDisplayUtils.isCustomPhoneType(type);
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return ContactDisplayUtils.getPhoneLabelResourceId(type);
+ }
+ }
+
+ public static class PhoneActionAltInflater extends CommonInflater {
+
+ @Override
+ protected boolean isCustom(Integer type) {
+ return ContactDisplayUtils.isCustomPhoneType(type);
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return ContactDisplayUtils.getSmsLabelResourceId(type);
+ }
+ }
+
+ public static class EmailActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.email;
+ }
+ switch (type) {
+ case Email.TYPE_HOME:
+ return R.string.email_home;
+ case Email.TYPE_WORK:
+ return R.string.email_work;
+ case Email.TYPE_OTHER:
+ return R.string.email_other;
+ case Email.TYPE_MOBILE:
+ return R.string.email_mobile;
+ default:
+ return R.string.email_custom;
+ }
+ }
+ }
+
+ public static class EventActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return Event.getTypeResource(type);
+ }
+ }
+
+ public static class RelationActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ return Relation.getTypeLabelResource(type == null ? Relation.TYPE_CUSTOM : type);
+ }
+ }
+
+ public static class PostalActionInflater extends CommonInflater {
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.map_other;
+ }
+ switch (type) {
+ case StructuredPostal.TYPE_HOME:
+ return R.string.map_home;
+ case StructuredPostal.TYPE_WORK:
+ return R.string.map_work;
+ case StructuredPostal.TYPE_OTHER:
+ return R.string.map_other;
+ default:
+ return R.string.map_custom;
+ }
+ }
+ }
+
+ public static class ImActionInflater extends CommonInflater {
+
+ @Override
+ protected String getTypeColumn() {
+ return Im.PROTOCOL;
+ }
+
+ @Override
+ protected String getLabelColumn() {
+ return Im.CUSTOM_PROTOCOL;
+ }
+
+ @Override
+ protected int getTypeLabelResource(Integer type) {
+ if (type == null) {
+ return R.string.chat;
+ }
+ switch (type) {
+ case Im.PROTOCOL_AIM:
+ return R.string.chat_aim;
+ case Im.PROTOCOL_MSN:
+ return R.string.chat_msn;
+ case Im.PROTOCOL_YAHOO:
+ return R.string.chat_yahoo;
+ case Im.PROTOCOL_SKYPE:
+ return R.string.chat_skype;
+ case Im.PROTOCOL_QQ:
+ return R.string.chat_qq;
+ case Im.PROTOCOL_GOOGLE_TALK:
+ return R.string.chat_gtalk;
+ case Im.PROTOCOL_ICQ:
+ return R.string.chat_icq;
+ case Im.PROTOCOL_JABBER:
+ return R.string.chat_jabber;
+ case Im.PROTOCOL_NETMEETING:
+ return R.string.chat;
+ default:
+ return R.string.chat;
+ }
+ }
+ }
+
+ // TODO Extract it to its own class, and move all KindBuilders to it as well.
+ private static class KindParser {
+
+ public static final KindParser INSTANCE = new KindParser();
+
+ private final Map<String, KindBuilder> mBuilders = new ArrayMap<>();
+
+ private KindParser() {
+ addBuilder(new NameKindBuilder());
+ addBuilder(new NicknameKindBuilder());
+ addBuilder(new PhoneKindBuilder());
+ addBuilder(new EmailKindBuilder());
+ addBuilder(new StructuredPostalKindBuilder());
+ addBuilder(new ImKindBuilder());
+ addBuilder(new OrganizationKindBuilder());
+ addBuilder(new PhotoKindBuilder());
+ addBuilder(new NoteKindBuilder());
+ addBuilder(new WebsiteKindBuilder());
+ addBuilder(new SipAddressKindBuilder());
+ addBuilder(new GroupMembershipKindBuilder());
+ addBuilder(new EventKindBuilder());
+ addBuilder(new RelationshipKindBuilder());
+ }
+
+ private void addBuilder(KindBuilder builder) {
+ mBuilders.put(builder.getTagName(), builder);
+ }
+
+ /**
+ * Takes a {@link XmlPullParser} at the start of a DataKind tag, parses it and returns {@link
+ * DataKind}s. (Usually just one, but there are three for the "name" kind.)
+ *
+ * <p>This method returns a list, because we need to add 3 kinds for the name data kind.
+ * (structured, display and phonetic)
+ */
+ public List<DataKind> parseDataKindTag(
+ Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final String kind = getAttr(attrs, Attr.KIND);
+ final KindBuilder builder = mBuilders.get(kind);
+ if (builder != null) {
+ return builder.parseDataKind(context, parser, attrs);
+ } else {
+ throw new DefinitionException("Undefined data kind '" + kind + "'");
+ }
+ }
+ }
+
+ private abstract static class KindBuilder {
+
+ public abstract String getTagName();
+
+ /** DataKind tag parser specific to each kind. Subclasses must implement it. */
+ public abstract List<DataKind> parseDataKind(
+ Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException;
+
+ /** Creates a new {@link DataKind}, and also parses the child Type tags in the DataKind tag. */
+ protected final DataKind newDataKind(
+ Context context,
+ XmlPullParser parser,
+ AttributeSet attrs,
+ boolean isPseudo,
+ String mimeType,
+ String typeColumn,
+ int titleRes,
+ int weight,
+ StringInflater actionHeader,
+ StringInflater actionBody)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Adding DataKind: " + mimeType);
+ }
+
+ final DataKind kind = new DataKind(mimeType, titleRes, weight, true);
+ kind.typeColumn = typeColumn;
+ kind.actionHeader = actionHeader;
+ kind.actionBody = actionBody;
+ kind.fieldList = new ArrayList<>();
+
+ // Get more information from the tag...
+ // A pseudo data kind doesn't have corresponding tag the XML, so we skip this.
+ if (!isPseudo) {
+ kind.typeOverallMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ // Process "Type" tags.
+ // If a kind has the type column, contacts.xml must have at least one type
+ // definition. Otherwise, it mustn't have a type definition.
+ if (kind.typeColumn != null) {
+ // Parse and add types.
+ kind.typeList = new ArrayList<>();
+ parseTypes(context, parser, attrs, kind, true);
+ if (kind.typeList.size() == 0) {
+ throw new DefinitionException("Kind " + kind.mimeType + " must have at least one type");
+ }
+ } else {
+ // Make sure it has no types.
+ parseTypes(context, parser, attrs, kind, false /* can't have types */);
+ }
+ }
+
+ return kind;
+ }
+
+ /**
+ * Parses Type elements in a DataKind element, and if {@code canHaveTypes} is true adds them to
+ * the given {@link DataKind}. Otherwise the {@link DataKind} can't have a type, so throws
+ * {@link DefinitionException}.
+ */
+ private void parseTypes(
+ Context context,
+ XmlPullParser parser,
+ AttributeSet attrs,
+ DataKind kind,
+ boolean canHaveTypes)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final int outerDepth = parser.getDepth();
+ int type;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ final int depth = parser.getDepth();
+ if (type != XmlPullParser.START_TAG || depth != outerDepth + 1) {
+ continue; // Not direct child tag
+ }
+
+ final String tag = parser.getName();
+ if (Tag.TYPE.equals(tag)) {
+ if (canHaveTypes) {
+ kind.typeList.add(parseTypeTag(parser, attrs, kind));
+ } else {
+ throw new DefinitionException("Kind " + kind.mimeType + " can't have types");
+ }
+ } else {
+ throw new DefinitionException("Unknown tag: " + tag);
+ }
+ }
+ }
+
+ /**
+ * Parses a single Type element and returns an {@link EditType} built from it. Uses {@link
+ * #buildEditTypeForTypeTag} defined in subclasses to actually build an {@link EditType}.
+ */
+ private EditType parseTypeTag(XmlPullParser parser, AttributeSet attrs, DataKind kind)
+ throws DefinitionException {
+
+ final String typeName = getAttr(attrs, Attr.TYPE);
+
+ final EditType et = buildEditTypeForTypeTag(attrs, typeName);
+ if (et == null) {
+ throw new DefinitionException(
+ "Undefined type '" + typeName + "' for data kind '" + kind.mimeType + "'");
+ }
+ et.specificMax = getAttr(attrs, Attr.MAX_OCCURRENCE, -1);
+
+ return et;
+ }
+
+ /**
+ * Returns an {@link EditType} for the given "type". Subclasses may optionally use the
+ * attributes in the tag to set optional values. (e.g. "yearOptional" for the event kind)
+ */
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ return null;
+ }
+
+ protected final void throwIfList(DataKind kind) throws DefinitionException {
+ if (kind.typeOverallMax != 1) {
+ throw new DefinitionException("Kind " + kind.mimeType + " must have 'overallMax=\"1\"'");
+ }
+ }
+ }
+
+ /** DataKind parser for Name. (structured, display, phonetic) */
+ private static class NameKindBuilder extends KindBuilder {
+
+ private static void checkAttributeTrue(boolean value, String attrName)
+ throws DefinitionException {
+ if (!value) {
+ throw new DefinitionException(attrName + " must be true");
+ }
+ }
+
+ @Override
+ public String getTagName() {
+ return "name";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ // Build 3 data kinds:
+ // - StructuredName.CONTENT_ITEM_TYPE
+ // - DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME
+ // - DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME
+
+ final boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+
+ final boolean supportsDisplayName = getAttr(attrs, "supportsDisplayName", false);
+ final boolean supportsPrefix = getAttr(attrs, "supportsPrefix", false);
+ final boolean supportsMiddleName = getAttr(attrs, "supportsMiddleName", false);
+ final boolean supportsSuffix = getAttr(attrs, "supportsSuffix", false);
+ final boolean supportsPhoneticFamilyName =
+ getAttr(attrs, "supportsPhoneticFamilyName", false);
+ final boolean supportsPhoneticMiddleName =
+ getAttr(attrs, "supportsPhoneticMiddleName", false);
+ final boolean supportsPhoneticGivenName = getAttr(attrs, "supportsPhoneticGivenName", false);
+
+ // For now, every things must be supported.
+ checkAttributeTrue(supportsDisplayName, "supportsDisplayName");
+ checkAttributeTrue(supportsPrefix, "supportsPrefix");
+ checkAttributeTrue(supportsMiddleName, "supportsMiddleName");
+ checkAttributeTrue(supportsSuffix, "supportsSuffix");
+ checkAttributeTrue(supportsPhoneticFamilyName, "supportsPhoneticFamilyName");
+ checkAttributeTrue(supportsPhoneticMiddleName, "supportsPhoneticMiddleName");
+ checkAttributeTrue(supportsPhoneticGivenName, "supportsPhoneticGivenName");
+
+ final List<DataKind> kinds = new ArrayList<>();
+
+ // Structured name
+ final DataKind ks =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ StructuredName.CONTENT_ITEM_TYPE,
+ null,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ throwIfList(ks);
+ kinds.add(ks);
+
+ // Note about setLongForm/setShortForm below.
+ // We need to set this only when the type supports display name. (=supportsDisplayName)
+ // Otherwise (i.e. Exchange) we don't set these flags, but instead make some fields
+ // "optional".
+
+ ks.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME));
+ ks.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME, R.string.name_phonetic_middle, FLAGS_PHONETIC));
+ ks.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ // Display name
+ final DataKind kd =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ true,
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ null,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kd.typeOverallMax = 1;
+ kinds.add(kd);
+
+ kd.fieldList.add(
+ new EditField(StructuredName.DISPLAY_NAME, R.string.full_name, FLAGS_PERSON_NAME)
+ .setShortForm(true));
+
+ if (!displayOrderPrimary) {
+ kd.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ } else {
+ kd.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ kd.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setLongForm(true));
+ }
+
+ // Phonetic name
+ final DataKind kp =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ true,
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ null,
+ R.string.name_phonetic,
+ Weight.NONE,
+ new SimpleInflater(R.string.nameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+ kp.typeOverallMax = 1;
+ kinds.add(kp);
+
+ // We may want to change the order depending on displayOrderPrimary too.
+ kp.fieldList.add(
+ new EditField(
+ DataKind.PSEUDO_COLUMN_PHONETIC_NAME, R.string.name_phonetic, FLAGS_PHONETIC)
+ .setShortForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME,
+ R.string.name_phonetic_family,
+ FLAGS_PHONETIC)
+ .setLongForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_MIDDLE_NAME,
+ R.string.name_phonetic_middle,
+ FLAGS_PHONETIC)
+ .setLongForm(true));
+ kp.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC)
+ .setLongForm(true));
+ return kinds;
+ }
+ }
+
+ private static class NicknameKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "nickname";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Nickname.CONTENT_ITEM_TYPE,
+ null,
+ R.string.nicknameLabelsGroup,
+ Weight.NICKNAME,
+ new SimpleInflater(R.string.nicknameLabelsGroup),
+ new SimpleInflater(Nickname.NAME));
+
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Nickname.TYPE, Nickname.TYPE_DEFAULT);
+
+ throwIfList(kind);
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class PhoneKindBuilder extends KindBuilder {
+
+ /** Just to avoid line-wrapping... */
+ protected static EditType build(int type, boolean secondary) {
+ return new EditType(type, Phone.getTypeLabelResource(type)).setSecondary(secondary);
+ }
+
+ @Override
+ public String getTagName() {
+ return "phone";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Phone.CONTENT_ITEM_TYPE,
+ Phone.TYPE,
+ R.string.phoneLabelsGroup,
+ Weight.PHONE,
+ new PhoneActionInflater(),
+ new SimpleInflater(Phone.NUMBER));
+
+ kind.iconAltRes = R.drawable.ic_message_24dp;
+ kind.iconAltDescriptionRes = R.string.sms;
+ kind.actionAltHeader = new PhoneActionAltInflater();
+
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("home".equals(type)) {
+ return build(Phone.TYPE_HOME, false);
+ }
+ if ("mobile".equals(type)) {
+ return build(Phone.TYPE_MOBILE, false);
+ }
+ if ("work".equals(type)) {
+ return build(Phone.TYPE_WORK, false);
+ }
+ if ("fax_work".equals(type)) {
+ return build(Phone.TYPE_FAX_WORK, true);
+ }
+ if ("fax_home".equals(type)) {
+ return build(Phone.TYPE_FAX_HOME, true);
+ }
+ if ("pager".equals(type)) {
+ return build(Phone.TYPE_PAGER, true);
+ }
+ if ("other".equals(type)) {
+ return build(Phone.TYPE_OTHER, false);
+ }
+ if ("callback".equals(type)) {
+ return build(Phone.TYPE_CALLBACK, true);
+ }
+ if ("car".equals(type)) {
+ return build(Phone.TYPE_CAR, true);
+ }
+ if ("company_main".equals(type)) {
+ return build(Phone.TYPE_COMPANY_MAIN, true);
+ }
+ if ("isdn".equals(type)) {
+ return build(Phone.TYPE_ISDN, true);
+ }
+ if ("main".equals(type)) {
+ return build(Phone.TYPE_MAIN, true);
+ }
+ if ("other_fax".equals(type)) {
+ return build(Phone.TYPE_OTHER_FAX, true);
+ }
+ if ("radio".equals(type)) {
+ return build(Phone.TYPE_RADIO, true);
+ }
+ if ("telex".equals(type)) {
+ return build(Phone.TYPE_TELEX, true);
+ }
+ if ("tty_tdd".equals(type)) {
+ return build(Phone.TYPE_TTY_TDD, true);
+ }
+ if ("work_mobile".equals(type)) {
+ return build(Phone.TYPE_WORK_MOBILE, true);
+ }
+ if ("work_pager".equals(type)) {
+ return build(Phone.TYPE_WORK_PAGER, true);
+ }
+
+ // Note "assistant" used to be a custom column for the fallback type, but not anymore.
+ if ("assistant".equals(type)) {
+ return build(Phone.TYPE_ASSISTANT, true);
+ }
+ if ("mms".equals(type)) {
+ return build(Phone.TYPE_MMS, true);
+ }
+ if ("custom".equals(type)) {
+ return build(Phone.TYPE_CUSTOM, true).setCustomColumn(Phone.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class EmailKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "email";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Email.CONTENT_ITEM_TYPE,
+ Email.TYPE,
+ R.string.emailLabelsGroup,
+ Weight.EMAIL,
+ new EmailActionInflater(),
+ new SimpleInflater(Email.DATA));
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) {
+ return buildEmailType(Email.TYPE_HOME);
+ }
+ if ("work".equals(type)) {
+ return buildEmailType(Email.TYPE_WORK);
+ }
+ if ("other".equals(type)) {
+ return buildEmailType(Email.TYPE_OTHER);
+ }
+ if ("mobile".equals(type)) {
+ return buildEmailType(Email.TYPE_MOBILE);
+ }
+ if ("custom".equals(type)) {
+ return buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class StructuredPostalKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "postal";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ StructuredPostal.CONTENT_ITEM_TYPE,
+ StructuredPostal.TYPE,
+ R.string.postalLabelsGroup,
+ Weight.STRUCTURED_POSTAL,
+ new PostalActionInflater(),
+ new SimpleInflater(StructuredPostal.FORMATTED_ADDRESS));
+
+ if (getAttr(attrs, "needsStructured", false)) {
+ if (Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage())) {
+ // Japanese order
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ // Generic order
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+ } else {
+ kind.maxLinesForDisplay = MAX_LINES_FOR_POSTAL_ADDRESS;
+ kind.fieldList.add(
+ new EditField(
+ StructuredPostal.FORMATTED_ADDRESS, R.string.postal_address, FLAGS_POSTAL));
+ }
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("home".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_HOME);
+ }
+ if ("work".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_WORK);
+ }
+ if ("other".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_OTHER);
+ }
+ if ("custom".equals(type)) {
+ return buildPostalType(StructuredPostal.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Email.LABEL);
+ }
+ return null;
+ }
+ }
+
+ private static class ImKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "im";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+
+ // IM is special:
+ // - It uses "protocol" as the custom label field
+ // - Its TYPE is fixed to TYPE_OTHER
+
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Im.CONTENT_ITEM_TYPE,
+ Im.PROTOCOL,
+ R.string.imLabelsGroup,
+ Weight.IM,
+ new ImActionInflater(),
+ new SimpleInflater(Im.DATA) // header / action
+ );
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ if ("aim".equals(type)) {
+ return buildImType(Im.PROTOCOL_AIM);
+ }
+ if ("msn".equals(type)) {
+ return buildImType(Im.PROTOCOL_MSN);
+ }
+ if ("yahoo".equals(type)) {
+ return buildImType(Im.PROTOCOL_YAHOO);
+ }
+ if ("skype".equals(type)) {
+ return buildImType(Im.PROTOCOL_SKYPE);
+ }
+ if ("qq".equals(type)) {
+ return buildImType(Im.PROTOCOL_QQ);
+ }
+ if ("google_talk".equals(type)) {
+ return buildImType(Im.PROTOCOL_GOOGLE_TALK);
+ }
+ if ("icq".equals(type)) {
+ return buildImType(Im.PROTOCOL_ICQ);
+ }
+ if ("jabber".equals(type)) {
+ return buildImType(Im.PROTOCOL_JABBER);
+ }
+ if ("custom".equals(type)) {
+ return buildImType(Im.PROTOCOL_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Im.CUSTOM_PROTOCOL);
+ }
+ return null;
+ }
+ }
+
+ private static class OrganizationKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "organization";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Organization.CONTENT_ITEM_TYPE,
+ null,
+ R.string.organizationLabelsGroup,
+ Weight.ORGANIZATION,
+ new SimpleInflater(R.string.organizationLabelsGroup),
+ ORGANIZATION_BODY_INFLATER);
+
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class PhotoKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "photo";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Photo.CONTENT_ITEM_TYPE,
+ null /* no type */,
+ Weight.NONE,
+ -1,
+ null,
+ null // no header, no body
+ );
+
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class NoteKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "note";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Note.CONTENT_ITEM_TYPE,
+ null,
+ R.string.label_notes,
+ Weight.NOTE,
+ new SimpleInflater(R.string.label_notes),
+ new SimpleInflater(Note.NOTE));
+
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+ kind.maxLinesForDisplay = MAX_LINES_FOR_NOTE;
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class WebsiteKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "website";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Website.CONTENT_ITEM_TYPE,
+ null,
+ R.string.websiteLabelsGroup,
+ Weight.WEBSITE,
+ new SimpleInflater(R.string.websiteLabelsGroup),
+ new SimpleInflater(Website.URL));
+
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Website.TYPE, Website.TYPE_OTHER);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class SipAddressKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "sip_address";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ SipAddress.CONTENT_ITEM_TYPE,
+ null,
+ R.string.label_sip_address,
+ Weight.SIP_ADDRESS,
+ new SimpleInflater(R.string.label_sip_address),
+ new SimpleInflater(SipAddress.SIP_ADDRESS));
+
+ kind.fieldList.add(
+ new EditField(SipAddress.SIP_ADDRESS, R.string.label_sip_address, FLAGS_SIP_ADDRESS));
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ private static class GroupMembershipKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "group_membership";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ GroupMembership.CONTENT_ITEM_TYPE,
+ null,
+ R.string.groupsLabel,
+ Weight.GROUP_MEMBERSHIP,
+ null,
+ null);
+
+ kind.fieldList.add(new EditField(GroupMembership.GROUP_ROW_ID, -1, -1));
+ kind.maxLinesForDisplay = MAX_LINES_FOR_GROUP;
+
+ throwIfList(kind);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+ }
+
+ /**
+ * Event DataKind parser.
+ *
+ * <p>Event DataKind is used only for Google/Exchange types, so this parser is not used for now.
+ */
+ private static class EventKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "event";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Event.CONTENT_ITEM_TYPE,
+ Event.TYPE,
+ R.string.eventLabelsGroup,
+ Weight.EVENT,
+ new EventActionInflater(),
+ new SimpleInflater(Event.START_DATE));
+
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ if (getAttr(attrs, Attr.DATE_WITH_TIME, false)) {
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_AND_TIME_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+ } else {
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ }
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ final boolean yo = getAttr(attrs, Attr.YEAR_OPTIONAL, false);
+
+ if ("birthday".equals(type)) {
+ return buildEventType(Event.TYPE_BIRTHDAY, yo).setSpecificMax(1);
+ }
+ if ("anniversary".equals(type)) {
+ return buildEventType(Event.TYPE_ANNIVERSARY, yo);
+ }
+ if ("other".equals(type)) {
+ return buildEventType(Event.TYPE_OTHER, yo);
+ }
+ if ("custom".equals(type)) {
+ return buildEventType(Event.TYPE_CUSTOM, yo)
+ .setSecondary(true)
+ .setCustomColumn(Event.LABEL);
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Relationship DataKind parser.
+ *
+ * <p>Relationship DataKind is used only for Google/Exchange types, so this parser is not used for
+ * now.
+ */
+ private static class RelationshipKindBuilder extends KindBuilder {
+
+ @Override
+ public String getTagName() {
+ return "relationship";
+ }
+
+ @Override
+ public List<DataKind> parseDataKind(Context context, XmlPullParser parser, AttributeSet attrs)
+ throws DefinitionException, XmlPullParserException, IOException {
+ final DataKind kind =
+ newDataKind(
+ context,
+ parser,
+ attrs,
+ false,
+ Relation.CONTENT_ITEM_TYPE,
+ Relation.TYPE,
+ R.string.relationLabelsGroup,
+ Weight.RELATIONSHIP,
+ new RelationActionInflater(),
+ new SimpleInflater(Relation.NAME));
+
+ kind.fieldList.add(
+ new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ List<DataKind> result = new ArrayList<>();
+ result.add(kind);
+ return result;
+ }
+
+ @Override
+ protected EditType buildEditTypeForTypeTag(AttributeSet attrs, String type) {
+ // EditType is mutable, so we need to create a new instance every time.
+ if ("assistant".equals(type)) {
+ return buildRelationType(Relation.TYPE_ASSISTANT);
+ }
+ if ("brother".equals(type)) {
+ return buildRelationType(Relation.TYPE_BROTHER);
+ }
+ if ("child".equals(type)) {
+ return buildRelationType(Relation.TYPE_CHILD);
+ }
+ if ("domestic_partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_DOMESTIC_PARTNER);
+ }
+ if ("father".equals(type)) {
+ return buildRelationType(Relation.TYPE_FATHER);
+ }
+ if ("friend".equals(type)) {
+ return buildRelationType(Relation.TYPE_FRIEND);
+ }
+ if ("manager".equals(type)) {
+ return buildRelationType(Relation.TYPE_MANAGER);
+ }
+ if ("mother".equals(type)) {
+ return buildRelationType(Relation.TYPE_MOTHER);
+ }
+ if ("parent".equals(type)) {
+ return buildRelationType(Relation.TYPE_PARENT);
+ }
+ if ("partner".equals(type)) {
+ return buildRelationType(Relation.TYPE_PARTNER);
+ }
+ if ("referred_by".equals(type)) {
+ return buildRelationType(Relation.TYPE_REFERRED_BY);
+ }
+ if ("relative".equals(type)) {
+ return buildRelationType(Relation.TYPE_RELATIVE);
+ }
+ if ("sister".equals(type)) {
+ return buildRelationType(Relation.TYPE_SISTER);
+ }
+ if ("spouse".equals(type)) {
+ return buildRelationType(Relation.TYPE_SPOUSE);
+ }
+ if ("custom".equals(type)) {
+ return buildRelationType(Relation.TYPE_CUSTOM)
+ .setSecondary(true)
+ .setCustomColumn(Relation.LABEL);
+ }
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/ExchangeAccountType.java b/java/com/android/contacts/common/model/account/ExchangeAccountType.java
new file mode 100644
index 000000000..a27028e80
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/ExchangeAccountType.java
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+public class ExchangeAccountType extends BaseAccountType {
+
+ private static final String TAG = "ExchangeAccountType";
+
+ private static final String ACCOUNT_TYPE_AOSP = "com.android.exchange";
+ private static final String ACCOUNT_TYPE_GOOGLE_1 = "com.google.android.exchange";
+ private static final String ACCOUNT_TYPE_GOOGLE_2 = "com.google.android.gm.exchange";
+
+ public ExchangeAccountType(Context context, String authenticatorPackageName, String type) {
+ this.accountType = type;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindEvent(context);
+ addDataKindWebsite(context);
+ addDataKindGroupMembership(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ public static boolean isExchangeType(String type) {
+ return ACCOUNT_TYPE_AOSP.equals(type)
+ || ACCOUNT_TYPE_GOOGLE_1.equals(type)
+ || ACCOUNT_TYPE_GOOGLE_2.equals(type);
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ StructuredName.CONTENT_ITEM_TYPE, R.string.nameLabelsGroup, Weight.NONE, true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME));
+
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindDisplayName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME,
+ R.string.nameLabelsGroup,
+ Weight.NONE,
+ true));
+
+ boolean displayOrderPrimary =
+ context.getResources().getBoolean(R.bool.config_editor_field_order_primary);
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(StructuredName.PREFIX, R.string.name_prefix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ if (!displayOrderPrimary) {
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredName.GIVEN_NAME, R.string.name_given, FLAGS_PERSON_NAME));
+ kind.fieldList.add(
+ new EditField(StructuredName.MIDDLE_NAME, R.string.name_middle, FLAGS_PERSON_NAME)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredName.FAMILY_NAME, R.string.name_family, FLAGS_PERSON_NAME));
+ }
+ kind.fieldList.add(
+ new EditField(StructuredName.SUFFIX, R.string.name_suffix, FLAGS_PERSON_NAME)
+ .setOptional(true));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhoneticName(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME,
+ R.string.name_phonetic,
+ Weight.NONE,
+ true));
+ kind.actionHeader = new SimpleInflater(R.string.nameLabelsGroup);
+ kind.actionBody = new SimpleInflater(Nickname.NAME);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_FAMILY_NAME, R.string.name_phonetic_family, FLAGS_PHONETIC));
+ kind.fieldList.add(
+ new EditField(
+ StructuredName.PHONETIC_GIVEN_NAME, R.string.name_phonetic_given, FLAGS_PHONETIC));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindNickname(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindNickname(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Nickname.NAME, R.string.nicknameLabelsGroup, FLAGS_PERSON_NAME));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME).setSpecificMax(2));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK).setSpecificMax(2));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_CAR).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_COMPANY_MAIN).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MMS).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true).setSpecificMax(1));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_ASSISTANT).setSecondary(true).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeOverallMax = 3;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindStructuredPostal(context);
+
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ if (useJapaneseOrder) {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindIm(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindIm(context);
+
+ // Types are not supported for IM. There can be 3 IMs, but OWA only shows only the first
+ kind.typeOverallMax = 3;
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Im.TYPE, Im.TYPE_OTHER);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Im.DATA, R.string.imLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindOrganization(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindOrganization(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(
+ new EditField(Organization.COMPANY, R.string.ghostData_company, FLAGS_GENERIC_NAME));
+ kind.fieldList.add(
+ new EditField(Organization.TITLE, R.string.ghostData_title, FLAGS_GENERIC_NAME));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhoto(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhoto(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Photo.PHOTO, -1, -1));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindNote(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindNote(context);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Note.NOTE, R.string.label_notes, FLAGS_NOTE));
+
+ return kind;
+ }
+
+ protected DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeOverallMax = 1;
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, false).setSpecificMax(1));
+
+ kind.dateFormatWithYear = CommonDateUtils.DATE_AND_TIME_FORMAT;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindWebsite(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindWebsite(context);
+
+ kind.typeOverallMax = 1;
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Website.URL, R.string.websiteLabelsGroup, FLAGS_WEBSITE));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/ExternalAccountType.java b/java/com/android/contacts/common/model/account/ExternalAccountType.java
new file mode 100644
index 000000000..aca1f70d2
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/ExternalAccountType.java
@@ -0,0 +1,443 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Xml;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/** A general contacts account type descriptor. */
+public class ExternalAccountType extends BaseAccountType {
+
+ private static final String TAG = "ExternalAccountType";
+
+ private static final String SYNC_META_DATA = "android.content.SyncAdapter";
+
+ /**
+ * The metadata name for so-called "contacts.xml".
+ *
+ * <p>On LMP and later, we also accept the "alternate" name. This is to allow sync adapters to
+ * have a contacts.xml without making it visible on older platforms. If you modify this also
+ * update the corresponding list in ContactsProvider/PhotoPriorityResolver
+ */
+ private static final String[] METADATA_CONTACTS_NAMES =
+ new String[] {
+ "android.provider.ALTERNATE_CONTACTS_STRUCTURE", "android.provider.CONTACTS_STRUCTURE"
+ };
+
+ private static final String TAG_CONTACTS_SOURCE_LEGACY = "ContactsSource";
+ private static final String TAG_CONTACTS_ACCOUNT_TYPE = "ContactsAccountType";
+ private static final String TAG_CONTACTS_DATA_KIND = "ContactsDataKind";
+ private static final String TAG_EDIT_SCHEMA = "EditSchema";
+
+ private static final String ATTR_EDIT_CONTACT_ACTIVITY = "editContactActivity";
+ private static final String ATTR_CREATE_CONTACT_ACTIVITY = "createContactActivity";
+ private static final String ATTR_INVITE_CONTACT_ACTIVITY = "inviteContactActivity";
+ private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
+ private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
+ private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
+ private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
+ private static final String ATTR_DATA_SET = "dataSet";
+ private static final String ATTR_EXTENSION_PACKAGE_NAMES = "extensionPackageNames";
+
+ // The following attributes should only be set in non-sync-adapter account types. They allow
+ // for the account type and resource IDs to be specified without an associated authenticator.
+ private static final String ATTR_ACCOUNT_TYPE = "accountType";
+ private static final String ATTR_ACCOUNT_LABEL = "accountTypeLabel";
+ private static final String ATTR_ACCOUNT_ICON = "accountTypeIcon";
+
+ private final boolean mIsExtension;
+
+ private String mEditContactActivityClassName;
+ private String mCreateContactActivityClassName;
+ private String mInviteContactActivity;
+ private String mInviteActionLabelAttribute;
+ private int mInviteActionLabelResId;
+ private String mViewContactNotifyService;
+ private String mViewGroupActivity;
+ private String mViewGroupLabelAttribute;
+ private int mViewGroupLabelResId;
+ private List<String> mExtensionPackageNames;
+ private String mAccountTypeLabelAttribute;
+ private String mAccountTypeIconAttribute;
+ private boolean mHasContactsMetadata;
+ private boolean mHasEditSchema;
+
+ public ExternalAccountType(Context context, String resPackageName, boolean isExtension) {
+ this(context, resPackageName, isExtension, null);
+ }
+
+ /**
+ * Constructor used for testing to initialize with any arbitrary XML.
+ *
+ * @param injectedMetadata If non-null, it'll be used to initialize the type. Only set by tests.
+ * If null, the metadata is loaded from the specified package.
+ */
+ ExternalAccountType(
+ Context context,
+ String packageName,
+ boolean isExtension,
+ XmlResourceParser injectedMetadata) {
+ this.mIsExtension = isExtension;
+ this.resourcePackageName = packageName;
+ this.syncAdapterPackageName = packageName;
+
+ final XmlResourceParser parser;
+ if (injectedMetadata == null) {
+ parser = loadContactsXml(context, packageName);
+ } else {
+ parser = injectedMetadata;
+ }
+ boolean needLineNumberInErrorLog = true;
+ try {
+ if (parser != null) {
+ inflate(context, parser);
+ }
+
+ // Done parsing; line number no longer needed in error log.
+ needLineNumberInErrorLog = false;
+ if (mHasEditSchema) {
+ checkKindExists(StructuredName.CONTENT_ITEM_TYPE);
+ checkKindExists(DataKind.PSEUDO_MIME_TYPE_DISPLAY_NAME);
+ checkKindExists(DataKind.PSEUDO_MIME_TYPE_PHONETIC_NAME);
+ checkKindExists(Photo.CONTENT_ITEM_TYPE);
+ } else {
+ // Bring in name and photo from fallback source, which are non-optional
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindPhoto(context);
+ }
+ } catch (DefinitionException e) {
+ final StringBuilder error = new StringBuilder();
+ error.append("Problem reading XML");
+ if (needLineNumberInErrorLog && (parser != null)) {
+ error.append(" in line ");
+ error.append(parser.getLineNumber());
+ }
+ error.append(" for external package ");
+ error.append(packageName);
+
+ Log.e(TAG, error.toString(), e);
+ return;
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+
+ mExtensionPackageNames = new ArrayList<String>();
+ mInviteActionLabelResId =
+ resolveExternalResId(
+ context,
+ mInviteActionLabelAttribute,
+ syncAdapterPackageName,
+ ATTR_INVITE_CONTACT_ACTION_LABEL);
+ mViewGroupLabelResId =
+ resolveExternalResId(
+ context,
+ mViewGroupLabelAttribute,
+ syncAdapterPackageName,
+ ATTR_VIEW_GROUP_ACTION_LABEL);
+ titleRes =
+ resolveExternalResId(
+ context, mAccountTypeLabelAttribute, syncAdapterPackageName, ATTR_ACCOUNT_LABEL);
+ iconRes =
+ resolveExternalResId(
+ context, mAccountTypeIconAttribute, syncAdapterPackageName, ATTR_ACCOUNT_ICON);
+
+ // If we reach this point, the account type has been successfully initialized.
+ mIsInitialized = true;
+ }
+
+ /**
+ * Returns the CONTACTS_STRUCTURE metadata (aka "contacts.xml") in the given apk package.
+ *
+ * <p>This method looks through all services in the package that handle sync adapter intents for
+ * the first one that contains CONTACTS_STRUCTURE metadata. We have to look through all sync
+ * adapters in the package in case there are contacts and other sync adapters (eg, calendar) in
+ * the same package.
+ *
+ * <p>Returns {@code null} if the package has no CONTACTS_STRUCTURE metadata. In this case the
+ * account type *will* be initialized with minimal configuration.
+ */
+ public static XmlResourceParser loadContactsXml(Context context, String resPackageName) {
+ final PackageManager pm = context.getPackageManager();
+ final Intent intent = new Intent(SYNC_META_DATA).setPackage(resPackageName);
+ final List<ResolveInfo> intentServices =
+ pm.queryIntentServices(intent, PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+
+ if (intentServices != null) {
+ for (final ResolveInfo resolveInfo : intentServices) {
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo == null) {
+ continue;
+ }
+ for (String metadataName : METADATA_CONTACTS_NAMES) {
+ final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, metadataName);
+ if (parser != null) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(
+ TAG,
+ String.format(
+ "Metadata loaded from: %s, %s, %s",
+ serviceInfo.packageName, serviceInfo.name, metadataName));
+ }
+ return parser;
+ }
+ }
+ }
+ }
+
+ // Package was found, but that doesn't contain the CONTACTS_STRUCTURE metadata.
+ return null;
+ }
+
+ /** Returns {@code TRUE} if the package contains CONTACTS_STRUCTURE metadata. */
+ public static boolean hasContactsXml(Context context, String resPackageName) {
+ return loadContactsXml(context, resPackageName) != null;
+ }
+
+ /**
+ * Takes a string in the "@xxx/yyy" format and return the resource ID for the resource in the
+ * resource package.
+ *
+ * <p>If the argument is in the invalid format or isn't a resource name, it returns -1.
+ *
+ * @param context context
+ * @param resourceName Resource name in the "@xxx/yyy" format, e.g. "@string/invite_lavbel"
+ * @param packageName name of the package containing the resource.
+ * @param xmlAttributeName attribute name which the resource came from. Used for logging.
+ */
+ @VisibleForTesting
+ static int resolveExternalResId(
+ Context context, String resourceName, String packageName, String xmlAttributeName) {
+ if (TextUtils.isEmpty(resourceName)) {
+ return -1; // Empty text is okay.
+ }
+ if (resourceName.charAt(0) != '@') {
+ Log.e(TAG, xmlAttributeName + " must be a resource name beginnig with '@'");
+ return -1;
+ }
+ final String name = resourceName.substring(1);
+ final Resources res;
+ try {
+ res = context.getPackageManager().getResourcesForApplication(packageName);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Unable to load package " + packageName);
+ return -1;
+ }
+ final int resId = res.getIdentifier(name, null, packageName);
+ if (resId == 0) {
+ Log.e(TAG, "Unable to load " + resourceName + " from package " + packageName);
+ return -1;
+ }
+ return resId;
+ }
+
+ private void checkKindExists(String mimeType) throws DefinitionException {
+ if (getKindForMimetype(mimeType) == null) {
+ throw new DefinitionException(mimeType + " must be supported");
+ }
+ }
+
+ @Override
+ public boolean isEmbedded() {
+ return false;
+ }
+
+ @Override
+ public boolean isExtension() {
+ return mIsExtension;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return mHasEditSchema;
+ }
+
+ /** Whether this account type has the android.provider.CONTACTS_STRUCTURE metadata xml. */
+ public boolean hasContactsMetadata() {
+ return mHasContactsMetadata;
+ }
+
+ @Override
+ public String getEditContactActivityClassName() {
+ return mEditContactActivityClassName;
+ }
+
+ @Override
+ public String getCreateContactActivityClassName() {
+ return mCreateContactActivityClassName;
+ }
+
+ @Override
+ public String getInviteContactActivityClassName() {
+ return mInviteContactActivity;
+ }
+
+ @Override
+ protected int getInviteContactActionResId() {
+ return mInviteActionLabelResId;
+ }
+
+ @Override
+ public String getViewContactNotifyServiceClassName() {
+ return mViewContactNotifyService;
+ }
+
+ @Override
+ public String getViewGroupActivity() {
+ return mViewGroupActivity;
+ }
+
+ @Override
+ protected int getViewGroupLabelResId() {
+ return mViewGroupLabelResId;
+ }
+
+ @Override
+ public List<String> getExtensionPackageNames() {
+ return mExtensionPackageNames;
+ }
+
+ /**
+ * Inflate this {@link AccountType} from the given parser. This may only load details matching the
+ * publicly-defined schema.
+ */
+ protected void inflate(Context context, XmlPullParser parser) throws DefinitionException {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+
+ try {
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // Drain comments and whitespace
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new IllegalStateException("No start tag found");
+ }
+
+ String rootTag = parser.getName();
+ if (!TAG_CONTACTS_ACCOUNT_TYPE.equals(rootTag)
+ && !TAG_CONTACTS_SOURCE_LEGACY.equals(rootTag)) {
+ throw new IllegalStateException(
+ "Top level element must be " + TAG_CONTACTS_ACCOUNT_TYPE + ", not " + rootTag);
+ }
+
+ mHasContactsMetadata = true;
+
+ int attributeCount = parser.getAttributeCount();
+ for (int i = 0; i < attributeCount; i++) {
+ String attr = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, attr + "=" + value);
+ }
+ if (ATTR_EDIT_CONTACT_ACTIVITY.equals(attr)) {
+ mEditContactActivityClassName = value;
+ } else if (ATTR_CREATE_CONTACT_ACTIVITY.equals(attr)) {
+ mCreateContactActivityClassName = value;
+ } else if (ATTR_INVITE_CONTACT_ACTIVITY.equals(attr)) {
+ mInviteContactActivity = value;
+ } else if (ATTR_INVITE_CONTACT_ACTION_LABEL.equals(attr)) {
+ mInviteActionLabelAttribute = value;
+ } else if (ATTR_VIEW_CONTACT_NOTIFY_SERVICE.equals(attr)) {
+ mViewContactNotifyService = value;
+ } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
+ mViewGroupActivity = value;
+ } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
+ mViewGroupLabelAttribute = value;
+ } else if (ATTR_DATA_SET.equals(attr)) {
+ dataSet = value;
+ } else if (ATTR_EXTENSION_PACKAGE_NAMES.equals(attr)) {
+ mExtensionPackageNames.add(value);
+ } else if (ATTR_ACCOUNT_TYPE.equals(attr)) {
+ accountType = value;
+ } else if (ATTR_ACCOUNT_LABEL.equals(attr)) {
+ mAccountTypeLabelAttribute = value;
+ } else if (ATTR_ACCOUNT_ICON.equals(attr)) {
+ mAccountTypeIconAttribute = value;
+ } else {
+ Log.e(TAG, "Unsupported attribute " + attr);
+ }
+ }
+
+ // Parse all children kinds
+ final int startDepth = parser.getDepth();
+ while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > startDepth)
+ && type != XmlPullParser.END_DOCUMENT) {
+
+ if (type != XmlPullParser.START_TAG || parser.getDepth() != startDepth + 1) {
+ continue; // Not a direct child tag
+ }
+
+ String tag = parser.getName();
+ if (TAG_EDIT_SCHEMA.equals(tag)) {
+ mHasEditSchema = true;
+ parseEditSchema(context, parser, attrs);
+ } else if (TAG_CONTACTS_DATA_KIND.equals(tag)) {
+ final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactsDataKind);
+ final DataKind kind = new DataKind();
+
+ kind.mimeType = a.getString(R.styleable.ContactsDataKind_android_mimeType);
+ final String summaryColumn =
+ a.getString(R.styleable.ContactsDataKind_android_summaryColumn);
+ if (summaryColumn != null) {
+ // Inflate a specific column as summary when requested
+ kind.actionHeader = new SimpleInflater(summaryColumn);
+ }
+ final String detailColumn =
+ a.getString(R.styleable.ContactsDataKind_android_detailColumn);
+ if (detailColumn != null) {
+ // Inflate specific column as summary
+ kind.actionBody = new SimpleInflater(detailColumn);
+ }
+
+ a.recycle();
+
+ addKind(kind);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ throw new DefinitionException("Problem reading XML", e);
+ } catch (IOException e) {
+ throw new DefinitionException("Problem reading XML", e);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/FallbackAccountType.java b/java/com/android/contacts/common/model/account/FallbackAccountType.java
new file mode 100644
index 000000000..976a7b892
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/FallbackAccountType.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.Context;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+
+public class FallbackAccountType extends BaseAccountType {
+
+ private static final String TAG = "FallbackAccountType";
+
+ private FallbackAccountType(Context context, String resPackageName) {
+ this.accountType = null;
+ this.dataSet = null;
+ this.titleRes = R.string.account_phone;
+ this.iconRes = R.mipmap.ic_contacts_launcher;
+
+ // Note those are only set for unit tests.
+ this.resourcePackageName = resPackageName;
+ this.syncAdapterPackageName = resPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindSipAddress(context);
+ addDataKindGroupMembership(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ public FallbackAccountType(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Used to compare with an {@link ExternalAccountType} built from a test contacts.xml. In order to
+ * build {@link DataKind}s with the same resource package name, {@code resPackageName} is
+ * injectable.
+ */
+ static AccountType createWithPackageNameForTest(Context context, String resPackageName) {
+ return new FallbackAccountType(context, resPackageName);
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/GoogleAccountType.java b/java/com/android/contacts/common/model/account/GoogleAccountType.java
new file mode 100644
index 000000000..2f1fe0ed6
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/GoogleAccountType.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class GoogleAccountType extends BaseAccountType {
+
+ /**
+ * The package name that we should load contacts.xml from and rely on to handle G+ account
+ * actions. Even though this points to gms, in some cases gms will still hand off responsibility
+ * to the G+ app.
+ */
+ public static final String PLUS_EXTENSION_PACKAGE_NAME = "com.google.android.gms";
+
+ public static final String ACCOUNT_TYPE = "com.google";
+ private static final String TAG = "GoogleAccountType";
+ private static final List<String> mExtensionPackages =
+ new ArrayList<>(Collections.singletonList(PLUS_EXTENSION_PACKAGE_NAME));
+
+ public GoogleAccountType(Context context, String authenticatorPackageName) {
+ this.accountType = ACCOUNT_TYPE;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindSipAddress(context);
+ addDataKindGroupMembership(context);
+ addDataKindRelation(context);
+ addDataKindEvent(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ @Override
+ public List<String> getExtensionPackageNames() {
+ return mExtensionPackages;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ private DataKind addDataKindRelation(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(
+ Relation.CONTENT_ITEM_TYPE,
+ R.string.relationLabelsGroup,
+ Weight.RELATIONSHIP,
+ true));
+ kind.actionHeader = new RelationActionInflater();
+ kind.actionBody = new SimpleInflater(Relation.NAME);
+
+ kind.typeColumn = Relation.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+ kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+ kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+ kind.typeList.add(
+ buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ return kind;
+ }
+
+ private DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(
+ new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, Weight.EVENT, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+ kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+ kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+ kind.typeList.add(
+ buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+
+ @Override
+ public String getViewContactNotifyServiceClassName() {
+ return "com.google.android.syncadapters.contacts." + "SyncHighResPhotoIntentService";
+ }
+
+ @Override
+ public String getViewContactNotifyServicePackageName() {
+ return "com.google.android.syncadapters.contacts";
+ }
+}
diff --git a/java/com/android/contacts/common/model/account/SamsungAccountType.java b/java/com/android/contacts/common/model/account/SamsungAccountType.java
new file mode 100644
index 000000000..45406bc2b
--- /dev/null
+++ b/java/com/android/contacts/common/model/account/SamsungAccountType.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.account;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.dataitem.DataKind;
+import com.android.contacts.common.util.CommonDateUtils;
+import java.util.ArrayList;
+import java.util.Locale;
+
+/**
+ * A writable account type that can be used to support samsung contacts. This may not perfectly
+ * match Samsung's latest intended account schema.
+ *
+ * <p>This is only used to partially support Samsung accounts. The DataKind labels & fields are
+ * setup to support the values used by Samsung. But, not everything in the Samsung account type is
+ * supported. The Samsung account type includes a "Message Type" mimetype that we have no intention
+ * of showing inside the Contact editor. Similarly, we don't handle the "Ringtone" mimetype here
+ * since managing ringtones is handled in a different flow.
+ */
+public class SamsungAccountType extends BaseAccountType {
+
+ private static final String TAG = "KnownExternalAccountType";
+ private static final String ACCOUNT_TYPE_SAMSUNG = "com.osp.app.signin";
+
+ public SamsungAccountType(Context context, String authenticatorPackageName, String type) {
+ this.accountType = type;
+ this.resourcePackageName = null;
+ this.syncAdapterPackageName = authenticatorPackageName;
+
+ try {
+ addDataKindStructuredName(context);
+ addDataKindDisplayName(context);
+ addDataKindPhoneticName(context);
+ addDataKindNickname(context);
+ addDataKindPhone(context);
+ addDataKindEmail(context);
+ addDataKindStructuredPostal(context);
+ addDataKindIm(context);
+ addDataKindOrganization(context);
+ addDataKindPhoto(context);
+ addDataKindNote(context);
+ addDataKindWebsite(context);
+ addDataKindGroupMembership(context);
+ addDataKindRelation(context);
+ addDataKindEvent(context);
+
+ mIsInitialized = true;
+ } catch (DefinitionException e) {
+ Log.e(TAG, "Problem building account type", e);
+ }
+ }
+
+ /**
+ * Returns {@code TRUE} if this is samsung's account type and Samsung hasn't bothered to define a
+ * contacts.xml to provide a more accurate definition than ours.
+ */
+ public static boolean isSamsungAccountType(Context context, String type, String packageName) {
+ return ACCOUNT_TYPE_SAMSUNG.equals(type)
+ && !ExternalAccountType.hasContactsXml(context, packageName);
+ }
+
+ @Override
+ protected DataKind addDataKindStructuredPostal(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindStructuredPostal(context);
+
+ final boolean useJapaneseOrder =
+ Locale.JAPANESE.getLanguage().equals(Locale.getDefault().getLanguage());
+ kind.typeColumn = StructuredPostal.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_WORK).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_HOME).setSpecificMax(1));
+ kind.typeList.add(buildPostalType(StructuredPostal.TYPE_OTHER).setSpecificMax(1));
+
+ kind.fieldList = new ArrayList<>();
+ if (useJapaneseOrder) {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ } else {
+ kind.fieldList.add(
+ new EditField(StructuredPostal.STREET, R.string.postal_street, FLAGS_POSTAL));
+ kind.fieldList.add(new EditField(StructuredPostal.CITY, R.string.postal_city, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.REGION, R.string.postal_region, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.POSTCODE, R.string.postal_postcode, FLAGS_POSTAL));
+ kind.fieldList.add(
+ new EditField(StructuredPostal.COUNTRY, R.string.postal_country, FLAGS_POSTAL)
+ .setOptional(true));
+ }
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindPhone(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindPhone(context);
+
+ kind.typeColumn = Phone.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MOBILE));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_HOME));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_WORK));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_MAIN));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_WORK).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_FAX_HOME).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_PAGER).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_RADIO).setSecondary(true));
+ kind.typeList.add(buildPhoneType(Phone.TYPE_OTHER));
+ kind.typeList.add(
+ buildPhoneType(Phone.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Phone.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Phone.NUMBER, R.string.phoneLabelsGroup, FLAGS_PHONE));
+
+ return kind;
+ }
+
+ @Override
+ protected DataKind addDataKindEmail(Context context) throws DefinitionException {
+ final DataKind kind = super.addDataKindEmail(context);
+
+ kind.typeColumn = Email.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildEmailType(Email.TYPE_HOME));
+ kind.typeList.add(buildEmailType(Email.TYPE_WORK));
+ kind.typeList.add(buildEmailType(Email.TYPE_OTHER));
+ kind.typeList.add(
+ buildEmailType(Email.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Email.LABEL));
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Email.DATA, R.string.emailLabelsGroup, FLAGS_EMAIL));
+
+ return kind;
+ }
+
+ private DataKind addDataKindRelation(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Relation.CONTENT_ITEM_TYPE, R.string.relationLabelsGroup, 160, true));
+ kind.actionHeader = new RelationActionInflater();
+ kind.actionBody = new SimpleInflater(Relation.NAME);
+
+ kind.typeColumn = Relation.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.typeList.add(buildRelationType(Relation.TYPE_ASSISTANT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_BROTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_CHILD));
+ kind.typeList.add(buildRelationType(Relation.TYPE_DOMESTIC_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FATHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_FRIEND));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MANAGER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_MOTHER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARENT));
+ kind.typeList.add(buildRelationType(Relation.TYPE_PARTNER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_REFERRED_BY));
+ kind.typeList.add(buildRelationType(Relation.TYPE_RELATIVE));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SISTER));
+ kind.typeList.add(buildRelationType(Relation.TYPE_SPOUSE));
+ kind.typeList.add(
+ buildRelationType(Relation.TYPE_CUSTOM).setSecondary(true).setCustomColumn(Relation.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Relation.TYPE, Relation.TYPE_SPOUSE);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Relation.DATA, R.string.relationLabelsGroup, FLAGS_RELATION));
+
+ return kind;
+ }
+
+ private DataKind addDataKindEvent(Context context) throws DefinitionException {
+ DataKind kind =
+ addKind(new DataKind(Event.CONTENT_ITEM_TYPE, R.string.eventLabelsGroup, 150, true));
+ kind.actionHeader = new EventActionInflater();
+ kind.actionBody = new SimpleInflater(Event.START_DATE);
+
+ kind.typeColumn = Event.TYPE;
+ kind.typeList = new ArrayList<>();
+ kind.dateFormatWithoutYear = CommonDateUtils.NO_YEAR_DATE_FORMAT;
+ kind.dateFormatWithYear = CommonDateUtils.FULL_DATE_FORMAT;
+ kind.typeList.add(buildEventType(Event.TYPE_BIRTHDAY, true).setSpecificMax(1));
+ kind.typeList.add(buildEventType(Event.TYPE_ANNIVERSARY, false));
+ kind.typeList.add(buildEventType(Event.TYPE_OTHER, false));
+ kind.typeList.add(
+ buildEventType(Event.TYPE_CUSTOM, false).setSecondary(true).setCustomColumn(Event.LABEL));
+
+ kind.defaultValues = new ContentValues();
+ kind.defaultValues.put(Event.TYPE, Event.TYPE_BIRTHDAY);
+
+ kind.fieldList = new ArrayList<>();
+ kind.fieldList.add(new EditField(Event.DATA, R.string.eventLabelsGroup, FLAGS_EVENT));
+
+ return kind;
+ }
+
+ @Override
+ public boolean isGroupMembershipEditable() {
+ return true;
+ }
+
+ @Override
+ public boolean areContactsWritable() {
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/DataItem.java b/java/com/android/contacts/common/model/dataitem/DataItem.java
new file mode 100644
index 000000000..dc746055b
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/DataItem.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Photo;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts.Data;
+import android.provider.ContactsContract.Contacts.Entity;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.model.account.AccountType.EditType;
+
+/** This is the base class for data items, which represents a row from the Data table. */
+public class DataItem implements Collapser.Collapsible<DataItem> {
+
+ private final ContentValues mContentValues;
+ protected DataKind mKind;
+
+ protected DataItem(ContentValues values) {
+ mContentValues = values;
+ }
+
+ /**
+ * Factory for creating subclasses of DataItem objects based on the mimetype in the content
+ * values. Raw contact is the raw contact that this data item is associated with.
+ */
+ public static DataItem createFrom(ContentValues values) {
+ final String mimeType = values.getAsString(Data.MIMETYPE);
+ if (GroupMembership.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new GroupMembershipDataItem(values);
+ } else if (StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredNameDataItem(values);
+ } else if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhoneDataItem(values);
+ } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EmailDataItem(values);
+ } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new StructuredPostalDataItem(values);
+ } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new ImDataItem(values);
+ } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new OrganizationDataItem(values);
+ } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NicknameDataItem(values);
+ } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new NoteDataItem(values);
+ } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new WebsiteDataItem(values);
+ } else if (SipAddress.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new SipAddressDataItem(values);
+ } else if (Event.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new EventDataItem(values);
+ } else if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new RelationDataItem(values);
+ } else if (Identity.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new IdentityDataItem(values);
+ } else if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
+ return new PhotoDataItem(values);
+ }
+
+ // generic
+ return new DataItem(values);
+ }
+
+ public ContentValues getContentValues() {
+ return mContentValues;
+ }
+
+ public Long getRawContactId() {
+ return mContentValues.getAsLong(Data.RAW_CONTACT_ID);
+ }
+
+ public void setRawContactId(long rawContactId) {
+ mContentValues.put(Data.RAW_CONTACT_ID, rawContactId);
+ }
+
+ /** Returns the data id. */
+ public long getId() {
+ return mContentValues.getAsLong(Data._ID);
+ }
+
+ /** Returns the mimetype of the data. */
+ public String getMimeType() {
+ return mContentValues.getAsString(Data.MIMETYPE);
+ }
+
+ public void setMimeType(String mimeType) {
+ mContentValues.put(Data.MIMETYPE, mimeType);
+ }
+
+ public boolean isPrimary() {
+ Integer primary = mContentValues.getAsInteger(Data.IS_PRIMARY);
+ return primary != null && primary != 0;
+ }
+
+ public boolean isSuperPrimary() {
+ Integer superPrimary = mContentValues.getAsInteger(Data.IS_SUPER_PRIMARY);
+ return superPrimary != null && superPrimary != 0;
+ }
+
+ public boolean hasKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return key != null
+ && mContentValues.containsKey(key)
+ && mContentValues.getAsInteger(key) != null;
+ }
+
+ public int getKindTypeColumn(DataKind kind) {
+ final String key = kind.typeColumn;
+ return mContentValues.getAsInteger(key);
+ }
+
+ /**
+ * Indicates the carrier presence value for the current {@link DataItem}.
+ *
+ * @return {@link Data#CARRIER_PRESENCE_VT_CAPABLE} if the {@link DataItem} supports carrier video
+ * calling, {@code 0} otherwise.
+ */
+ public int getCarrierPresence() {
+ return mContentValues.getAsInteger(Data.CARRIER_PRESENCE);
+ }
+
+ /**
+ * This builds the data string depending on the type of data item by using the generic DataKind
+ * object underneath.
+ */
+ public String buildDataString(Context context, DataKind kind) {
+ if (kind.actionBody == null) {
+ return null;
+ }
+ CharSequence actionBody = kind.actionBody.inflateUsing(context, mContentValues);
+ return actionBody == null ? null : actionBody.toString();
+ }
+
+ /**
+ * This builds the data string(intended for display) depending on the type of data item. It
+ * returns the same value as {@link #buildDataString} by default, but certain data items can
+ * override it to provide their version of formatted data strings.
+ *
+ * @return Data string representing the data item, possibly formatted for display
+ */
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ return buildDataString(context, kind);
+ }
+
+ public DataKind getDataKind() {
+ return mKind;
+ }
+
+ public void setDataKind(DataKind kind) {
+ mKind = kind;
+ }
+
+ public Integer getTimesUsed() {
+ return mContentValues.getAsInteger(Entity.TIMES_USED);
+ }
+
+ public Long getLastTimeUsed() {
+ return mContentValues.getAsLong(Entity.LAST_TIME_USED);
+ }
+
+ @Override
+ public void collapseWith(DataItem that) {
+ DataKind thisKind = getDataKind();
+ DataKind thatKind = that.getDataKind();
+ // If this does not have a type and that does, or if that's type is higher precedence,
+ // use that's type
+ if ((!hasKindTypeColumn(thisKind) && that.hasKindTypeColumn(thatKind))
+ || (that.hasKindTypeColumn(thatKind)
+ && getTypePrecedence(thisKind, getKindTypeColumn(thisKind))
+ > getTypePrecedence(thatKind, that.getKindTypeColumn(thatKind)))) {
+ mContentValues.put(thatKind.typeColumn, that.getKindTypeColumn(thatKind));
+ mKind = thatKind;
+ }
+
+ // Choose the max of the maxLines and maxLabelLines values.
+ mKind.maxLinesForDisplay = Math.max(thisKind.maxLinesForDisplay, thatKind.maxLinesForDisplay);
+
+ // If any of the collapsed entries are super primary make the whole thing super primary.
+ if (isSuperPrimary() || that.isSuperPrimary()) {
+ mContentValues.put(Data.IS_SUPER_PRIMARY, 1);
+ mContentValues.put(Data.IS_PRIMARY, 1);
+ }
+
+ // If any of the collapsed entries are primary make the whole thing primary.
+ if (isPrimary() || that.isPrimary()) {
+ mContentValues.put(Data.IS_PRIMARY, 1);
+ }
+
+ // Add up the times used
+ mContentValues.put(
+ Entity.TIMES_USED,
+ (getTimesUsed() == null ? 0 : getTimesUsed())
+ + (that.getTimesUsed() == null ? 0 : that.getTimesUsed()));
+
+ // Use the most recent time
+ mContentValues.put(
+ Entity.LAST_TIME_USED,
+ Math.max(
+ getLastTimeUsed() == null ? 0 : getLastTimeUsed(),
+ that.getLastTimeUsed() == null ? 0 : that.getLastTimeUsed()));
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ return MoreContactUtils.shouldCollapse(
+ getMimeType(),
+ buildDataString(context, mKind),
+ t.getMimeType(),
+ t.buildDataString(context, t.getDataKind()));
+ }
+
+ /**
+ * Return the precedence for the the given {@link EditType#rawValue}, where lower numbers are
+ * higher precedence.
+ */
+ private static int getTypePrecedence(DataKind kind, int rawValue) {
+ for (int i = 0; i < kind.typeList.size(); i++) {
+ final EditType type = kind.typeList.get(i);
+ if (type.rawValue == rawValue) {
+ return i;
+ }
+ }
+ return Integer.MAX_VALUE;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/DataKind.java b/java/com/android/contacts/common/model/dataitem/DataKind.java
new file mode 100644
index 000000000..3b470a2ae
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/DataKind.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.Data;
+import com.android.contacts.common.model.account.AccountType.EditField;
+import com.android.contacts.common.model.account.AccountType.EditType;
+import com.android.contacts.common.model.account.AccountType.StringInflater;
+import com.google.common.collect.Iterators;
+import java.text.SimpleDateFormat;
+import java.util.List;
+
+/**
+ * Description of a specific data type, usually marked by a unique {@link Data#MIMETYPE}. Includes
+ * details about how to view and edit {@link Data} rows of this kind, including the possible {@link
+ * EditType} labels and editable {@link EditField}.
+ */
+public final class DataKind {
+
+ public static final String PSEUDO_MIME_TYPE_DISPLAY_NAME = "#displayName";
+ public static final String PSEUDO_MIME_TYPE_PHONETIC_NAME = "#phoneticName";
+ public static final String PSEUDO_COLUMN_PHONETIC_NAME = "#phoneticName";
+
+ public String resourcePackageName;
+ public String mimeType;
+ public int titleRes;
+ public int iconAltRes;
+ public int iconAltDescriptionRes;
+ public int weight;
+ public boolean editable;
+
+ public StringInflater actionHeader;
+ public StringInflater actionAltHeader;
+ public StringInflater actionBody;
+
+ public String typeColumn;
+
+ /** Maximum number of values allowed in the list. -1 represents infinity. */
+ public int typeOverallMax;
+
+ public List<EditType> typeList;
+ public List<EditField> fieldList;
+
+ public ContentValues defaultValues;
+
+ /**
+ * If this is a date field, this specifies the format of the date when saving. The date includes
+ * year, month and day. If this is not a date field or the date field is not editable, this value
+ * should be ignored.
+ */
+ public SimpleDateFormat dateFormatWithoutYear;
+
+ /**
+ * If this is a date field, this specifies the format of the date when saving. The date includes
+ * month and day. If this is not a date field, the field is not editable or dates without year are
+ * not supported, this value should be ignored.
+ */
+ public SimpleDateFormat dateFormatWithYear;
+
+ /** The number of lines available for displaying this kind of data. Defaults to 1. */
+ public int maxLinesForDisplay;
+
+ public DataKind() {
+ maxLinesForDisplay = 1;
+ }
+
+ public DataKind(String mimeType, int titleRes, int weight, boolean editable) {
+ this.mimeType = mimeType;
+ this.titleRes = titleRes;
+ this.weight = weight;
+ this.editable = editable;
+ this.typeOverallMax = -1;
+ maxLinesForDisplay = 1;
+ }
+
+ public static String toString(SimpleDateFormat format) {
+ return format == null ? "(null)" : format.toPattern();
+ }
+
+ public static String toString(Iterable<?> list) {
+ if (list == null) {
+ return "(null)";
+ } else {
+ return Iterators.toString(list.iterator());
+ }
+ }
+
+ public String getKindString(Context context) {
+ return (titleRes == -1 || titleRes == 0) ? "" : context.getString(titleRes);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("DataKind:");
+ sb.append(" resPackageName=").append(resourcePackageName);
+ sb.append(" mimeType=").append(mimeType);
+ sb.append(" titleRes=").append(titleRes);
+ sb.append(" iconAltRes=").append(iconAltRes);
+ sb.append(" iconAltDescriptionRes=").append(iconAltDescriptionRes);
+ sb.append(" weight=").append(weight);
+ sb.append(" editable=").append(editable);
+ sb.append(" actionHeader=").append(actionHeader);
+ sb.append(" actionAltHeader=").append(actionAltHeader);
+ sb.append(" actionBody=").append(actionBody);
+ sb.append(" typeColumn=").append(typeColumn);
+ sb.append(" typeOverallMax=").append(typeOverallMax);
+ sb.append(" typeList=").append(toString(typeList));
+ sb.append(" fieldList=").append(toString(fieldList));
+ sb.append(" defaultValues=").append(defaultValues);
+ sb.append(" dateFormatWithoutYear=").append(toString(dateFormatWithoutYear));
+ sb.append(" dateFormatWithYear=").append(toString(dateFormatWithYear));
+
+ return sb.toString();
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/EmailDataItem.java b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java
new file mode 100644
index 000000000..2fe297816
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/EmailDataItem.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+
+/**
+ * Represents an email data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Email}.
+ */
+public class EmailDataItem extends DataItem {
+
+ /* package */ EmailDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getAddress() {
+ return getContentValues().getAsString(Email.ADDRESS);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(Email.DISPLAY_NAME);
+ }
+
+ public String getData() {
+ return getContentValues().getAsString(Email.DATA);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Email.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/EventDataItem.java b/java/com/android/contacts/common/model/dataitem/EventDataItem.java
new file mode 100644
index 000000000..15d9880b1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/EventDataItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.text.TextUtils;
+
+/**
+ * Represents an event data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Event}.
+ */
+public class EventDataItem extends DataItem {
+
+ /* package */ EventDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getStartDate() {
+ return getContentValues().getAsString(Event.START_DATE);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Event.LABEL);
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof EventDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final EventDataItem that = (EventDataItem) t;
+ // Events can be different (anniversary, birthday) but have the same start date
+ if (!TextUtils.equals(getStartDate(), that.getStartDate())) {
+ return false;
+ } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+ return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+ } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+ return false;
+ } else if (getKindTypeColumn(mKind) == Event.TYPE_CUSTOM
+ && !TextUtils.equals(getLabel(), that.getLabel())) {
+ // Check if custom types are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
new file mode 100644
index 000000000..f921b3c9d
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/GroupMembershipDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+
+/**
+ * Represents a group memebership data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.GroupMembership}.
+ */
+public class GroupMembershipDataItem extends DataItem {
+
+ /* package */ GroupMembershipDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getGroupRowId() {
+ return getContentValues().getAsLong(GroupMembership.GROUP_ROW_ID);
+ }
+
+ public String getGroupSourceId() {
+ return getContentValues().getAsString(GroupMembership.GROUP_SOURCE_ID);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java
new file mode 100644
index 000000000..2badf92f7
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/IdentityDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+
+/**
+ * Represents an identity data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Identity}.
+ */
+public class IdentityDataItem extends DataItem {
+
+ /* package */ IdentityDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getIdentity() {
+ return getContentValues().getAsString(Identity.IDENTITY);
+ }
+
+ public String getNamespace() {
+ return getContentValues().getAsString(Identity.NAMESPACE);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/ImDataItem.java b/java/com/android/contacts/common/model/dataitem/ImDataItem.java
new file mode 100644
index 000000000..16b9fd094
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/ImDataItem.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.text.TextUtils;
+
+/**
+ * Represents an IM data item, wrapping the columns in {@link ContactsContract.CommonDataKinds.Im}.
+ */
+public class ImDataItem extends DataItem {
+
+ private final boolean mCreatedFromEmail;
+
+ /* package */ ImDataItem(ContentValues values) {
+ super(values);
+ mCreatedFromEmail = false;
+ }
+
+ private ImDataItem(ContentValues values, boolean createdFromEmail) {
+ super(values);
+ mCreatedFromEmail = createdFromEmail;
+ }
+
+ public static ImDataItem createFromEmail(EmailDataItem item) {
+ final ImDataItem im = new ImDataItem(new ContentValues(item.getContentValues()), true);
+ im.setMimeType(Im.CONTENT_ITEM_TYPE);
+ return im;
+ }
+
+ public String getData() {
+ if (mCreatedFromEmail) {
+ return getContentValues().getAsString(Email.DATA);
+ } else {
+ return getContentValues().getAsString(Im.DATA);
+ }
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Im.LABEL);
+ }
+
+ /** Values are one of Im.PROTOCOL_ */
+ public Integer getProtocol() {
+ return getContentValues().getAsInteger(Im.PROTOCOL);
+ }
+
+ public boolean isProtocolValid() {
+ return getProtocol() != null;
+ }
+
+ public String getCustomProtocol() {
+ return getContentValues().getAsString(Im.CUSTOM_PROTOCOL);
+ }
+
+ public int getChatCapability() {
+ Integer result = getContentValues().getAsInteger(Im.CHAT_CAPABILITY);
+ return result == null ? 0 : result;
+ }
+
+ public boolean isCreatedFromEmail() {
+ return mCreatedFromEmail;
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof ImDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final ImDataItem that = (ImDataItem) t;
+ // IM can have the same data put different protocol. These should not collapse.
+ if (!getData().equals(that.getData())) {
+ return false;
+ } else if (!isProtocolValid() || !that.isProtocolValid()) {
+ // Deal with invalid protocol as if it was custom. If either has a non valid
+ // protocol, check to see if the other has a valid that is not custom
+ if (isProtocolValid()) {
+ return getProtocol() == Im.PROTOCOL_CUSTOM;
+ } else if (that.isProtocolValid()) {
+ return that.getProtocol() == Im.PROTOCOL_CUSTOM;
+ }
+ return true;
+ } else if (getProtocol() != that.getProtocol()) {
+ return false;
+ } else if (getProtocol() == Im.PROTOCOL_CUSTOM
+ && !TextUtils.equals(getCustomProtocol(), that.getCustomProtocol())) {
+ // Check if custom protocols are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java
new file mode 100644
index 000000000..a448be786
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/NicknameDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+
+/**
+ * Represents a nickname data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Nickname}.
+ */
+public class NicknameDataItem extends DataItem {
+
+ public NicknameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Nickname.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Nickname.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/NoteDataItem.java b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java
new file mode 100644
index 000000000..b55ecc3e5
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/NoteDataItem.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+
+/**
+ * Represents a note data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Note}.
+ */
+public class NoteDataItem extends DataItem {
+
+ /* package */ NoteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNote() {
+ return getContentValues().getAsString(Note.NOTE);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
new file mode 100644
index 000000000..b33124838
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/OrganizationDataItem.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+
+/**
+ * Represents an organization data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Organization}.
+ */
+public class OrganizationDataItem extends DataItem {
+
+ /* package */ OrganizationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getCompany() {
+ return getContentValues().getAsString(Organization.COMPANY);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Organization.LABEL);
+ }
+
+ public String getTitle() {
+ return getContentValues().getAsString(Organization.TITLE);
+ }
+
+ public String getDepartment() {
+ return getContentValues().getAsString(Organization.DEPARTMENT);
+ }
+
+ public String getJobDescription() {
+ return getContentValues().getAsString(Organization.JOB_DESCRIPTION);
+ }
+
+ public String getSymbol() {
+ return getContentValues().getAsString(Organization.SYMBOL);
+ }
+
+ public String getPhoneticName() {
+ return getContentValues().getAsString(Organization.PHONETIC_NAME);
+ }
+
+ public String getOfficeLocation() {
+ return getContentValues().getAsString(Organization.OFFICE_LOCATION);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java
new file mode 100644
index 000000000..e1f56456a
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/PhoneDataItem.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+
+/**
+ * Represents a phone data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Phone}.
+ */
+public class PhoneDataItem extends DataItem {
+
+ public static final String KEY_FORMATTED_PHONE_NUMBER = "formattedPhoneNumber";
+
+ /* package */ PhoneDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getNumber() {
+ return getContentValues().getAsString(Phone.NUMBER);
+ }
+
+ /** Returns the normalized phone number in E164 format. */
+ public String getNormalizedNumber() {
+ return getContentValues().getAsString(Phone.NORMALIZED_NUMBER);
+ }
+
+ public String getFormattedPhoneNumber() {
+ return getContentValues().getAsString(KEY_FORMATTED_PHONE_NUMBER);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Phone.LABEL);
+ }
+
+ public void computeFormattedPhoneNumber(String defaultCountryIso) {
+ final String phoneNumber = getNumber();
+ if (phoneNumber != null) {
+ final String formattedPhoneNumber =
+ PhoneNumberUtilsCompat.formatNumber(
+ phoneNumber, getNormalizedNumber(), defaultCountryIso);
+ getContentValues().put(KEY_FORMATTED_PHONE_NUMBER, formattedPhoneNumber);
+ }
+ }
+
+ /**
+ * Returns the formatted phone number (if already computed using {@link
+ * #computeFormattedPhoneNumber}). Otherwise this method returns the unformatted phone number.
+ */
+ @Override
+ public String buildDataStringForDisplay(Context context, DataKind kind) {
+ final String formatted = getFormattedPhoneNumber();
+ if (formatted != null) {
+ return formatted;
+ } else {
+ return getNumber();
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java
new file mode 100644
index 000000000..0bf7a318b
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/PhotoDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Contacts.Photo;
+
+/**
+ * Represents a photo data item, wrapping the columns in {@link ContactsContract.Contacts.Photo}.
+ */
+public class PhotoDataItem extends DataItem {
+
+ /* package */ PhotoDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public Long getPhotoFileId() {
+ return getContentValues().getAsLong(Photo.PHOTO_FILE_ID);
+ }
+
+ public byte[] getPhoto() {
+ return getContentValues().getAsByteArray(Photo.PHOTO);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/RelationDataItem.java b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java
new file mode 100644
index 000000000..fdbcbb313
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/RelationDataItem.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
+import android.text.TextUtils;
+
+/**
+ * Represents a relation data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Relation}.
+ */
+public class RelationDataItem extends DataItem {
+
+ /* package */ RelationDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getName() {
+ return getContentValues().getAsString(Relation.NAME);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Relation.LABEL);
+ }
+
+ @Override
+ public boolean shouldCollapseWith(DataItem t, Context context) {
+ if (!(t instanceof RelationDataItem) || mKind == null || t.getDataKind() == null) {
+ return false;
+ }
+ final RelationDataItem that = (RelationDataItem) t;
+ // Relations can have different types (assistant, father) but have the same name
+ if (!TextUtils.equals(getName(), that.getName())) {
+ return false;
+ } else if (!hasKindTypeColumn(mKind) || !that.hasKindTypeColumn(that.getDataKind())) {
+ return hasKindTypeColumn(mKind) == that.hasKindTypeColumn(that.getDataKind());
+ } else if (getKindTypeColumn(mKind) != that.getKindTypeColumn(that.getDataKind())) {
+ return false;
+ } else if (getKindTypeColumn(mKind) == Relation.TYPE_CUSTOM
+ && !TextUtils.equals(getLabel(), that.getLabel())) {
+ // Check if custom types are not the same
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
new file mode 100644
index 000000000..0ca9eae6d
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/SipAddressDataItem.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+
+/**
+ * Represents a sip address data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.SipAddress}.
+ */
+public class SipAddressDataItem extends DataItem {
+
+ /* package */ SipAddressDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getSipAddress() {
+ return getContentValues().getAsString(SipAddress.SIP_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(SipAddress.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
new file mode 100644
index 000000000..22bf037f1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/StructuredNameDataItem.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Contacts.Data;
+
+/**
+ * Represents a structured name data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.StructuredName}.
+ */
+public class StructuredNameDataItem extends DataItem {
+
+ public StructuredNameDataItem() {
+ super(new ContentValues());
+ getContentValues().put(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ }
+
+ /* package */ StructuredNameDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getDisplayName() {
+ return getContentValues().getAsString(StructuredName.DISPLAY_NAME);
+ }
+
+ public void setDisplayName(String name) {
+ getContentValues().put(StructuredName.DISPLAY_NAME, name);
+ }
+
+ public String getGivenName() {
+ return getContentValues().getAsString(StructuredName.GIVEN_NAME);
+ }
+
+ public String getFamilyName() {
+ return getContentValues().getAsString(StructuredName.FAMILY_NAME);
+ }
+
+ public String getPrefix() {
+ return getContentValues().getAsString(StructuredName.PREFIX);
+ }
+
+ public String getMiddleName() {
+ return getContentValues().getAsString(StructuredName.MIDDLE_NAME);
+ }
+
+ public String getSuffix() {
+ return getContentValues().getAsString(StructuredName.SUFFIX);
+ }
+
+ public String getPhoneticGivenName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_GIVEN_NAME);
+ }
+
+ public void setPhoneticGivenName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_GIVEN_NAME, name);
+ }
+
+ public String getPhoneticMiddleName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
+ }
+
+ public void setPhoneticMiddleName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_MIDDLE_NAME, name);
+ }
+
+ public String getPhoneticFamilyName() {
+ return getContentValues().getAsString(StructuredName.PHONETIC_FAMILY_NAME);
+ }
+
+ public void setPhoneticFamilyName(String name) {
+ getContentValues().put(StructuredName.PHONETIC_FAMILY_NAME, name);
+ }
+
+ public String getFullNameStyle() {
+ return getContentValues().getAsString(StructuredName.FULL_NAME_STYLE);
+ }
+
+ public boolean isSuperPrimary() {
+ final ContentValues contentValues = getContentValues();
+ return contentValues == null || !contentValues.containsKey(StructuredName.IS_SUPER_PRIMARY)
+ ? false
+ : contentValues.getAsBoolean(StructuredName.IS_SUPER_PRIMARY);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
new file mode 100644
index 000000000..18aae282c
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/StructuredPostalDataItem.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+
+/**
+ * Represents a structured postal data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.StructuredPostal}.
+ */
+public class StructuredPostalDataItem extends DataItem {
+
+ /* package */ StructuredPostalDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getFormattedAddress() {
+ return getContentValues().getAsString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(StructuredPostal.LABEL);
+ }
+
+ public String getStreet() {
+ return getContentValues().getAsString(StructuredPostal.STREET);
+ }
+
+ public String getPOBox() {
+ return getContentValues().getAsString(StructuredPostal.POBOX);
+ }
+
+ public String getNeighborhood() {
+ return getContentValues().getAsString(StructuredPostal.NEIGHBORHOOD);
+ }
+
+ public String getCity() {
+ return getContentValues().getAsString(StructuredPostal.CITY);
+ }
+
+ public String getRegion() {
+ return getContentValues().getAsString(StructuredPostal.REGION);
+ }
+
+ public String getPostcode() {
+ return getContentValues().getAsString(StructuredPostal.POSTCODE);
+ }
+
+ public String getCountry() {
+ return getContentValues().getAsString(StructuredPostal.COUNTRY);
+ }
+}
diff --git a/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
new file mode 100644
index 000000000..b8400ecd1
--- /dev/null
+++ b/java/com/android/contacts/common/model/dataitem/WebsiteDataItem.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.model.dataitem;
+
+import android.content.ContentValues;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+/**
+ * Represents a website data item, wrapping the columns in {@link
+ * ContactsContract.CommonDataKinds.Website}.
+ */
+public class WebsiteDataItem extends DataItem {
+
+ /* package */ WebsiteDataItem(ContentValues values) {
+ super(values);
+ }
+
+ public String getUrl() {
+ return getContentValues().getAsString(Website.URL);
+ }
+
+ public String getLabel() {
+ return getContentValues().getAsString(Website.LABEL);
+ }
+}
diff --git a/java/com/android/contacts/common/preference/ContactsPreferences.java b/java/com/android/contacts/common/preference/ContactsPreferences.java
new file mode 100644
index 000000000..7f0d99acd
--- /dev/null
+++ b/java/com/android/contacts/common/preference/ContactsPreferences.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.model.account.AccountWithDataSet;
+
+/** Manages user preferences for contacts. */
+public class ContactsPreferences implements OnSharedPreferenceChangeListener {
+
+ /** The value for the DISPLAY_ORDER key to show the given name first. */
+ public static final int DISPLAY_ORDER_PRIMARY = 1;
+
+ /** The value for the DISPLAY_ORDER key to show the family name first. */
+ public static final int DISPLAY_ORDER_ALTERNATIVE = 2;
+
+ public static final String DISPLAY_ORDER_KEY = "android.contacts.DISPLAY_ORDER";
+
+ /** The value for the SORT_ORDER key corresponding to sort by given name first. */
+ public static final int SORT_ORDER_PRIMARY = 1;
+
+ public static final String SORT_ORDER_KEY = "android.contacts.SORT_ORDER";
+
+ /** The value for the SORT_ORDER key corresponding to sort by family name first. */
+ public static final int SORT_ORDER_ALTERNATIVE = 2;
+
+ public static final String PREF_DISPLAY_ONLY_PHONES = "only_phones";
+
+ public static final boolean PREF_DISPLAY_ONLY_PHONES_DEFAULT = false;
+
+ /**
+ * Value to use when a preference is unassigned and needs to be read from the shared preferences
+ */
+ private static final int PREFERENCE_UNASSIGNED = -1;
+
+ private final Context mContext;
+ private final SharedPreferences mPreferences;
+ private int mSortOrder = PREFERENCE_UNASSIGNED;
+ private int mDisplayOrder = PREFERENCE_UNASSIGNED;
+ private String mDefaultAccount = null;
+ private ChangeListener mListener = null;
+ private Handler mHandler;
+ private String mDefaultAccountKey;
+ private String mDefaultAccountSavedKey;
+
+ public ContactsPreferences(Context context) {
+ mContext = context;
+ mHandler = new Handler();
+ mPreferences = mContext.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
+ mDefaultAccountKey =
+ mContext.getResources().getString(R.string.contact_editor_default_account_key);
+ mDefaultAccountSavedKey =
+ mContext.getResources().getString(R.string.contact_editor_anything_saved_key);
+ maybeMigrateSystemSettings();
+ }
+
+ public boolean isSortOrderUserChangeable() {
+ return mContext.getResources().getBoolean(R.bool.config_sort_order_user_changeable);
+ }
+
+ public int getDefaultSortOrder() {
+ if (mContext.getResources().getBoolean(R.bool.config_default_sort_order_primary)) {
+ return SORT_ORDER_PRIMARY;
+ } else {
+ return SORT_ORDER_ALTERNATIVE;
+ }
+ }
+
+ public int getSortOrder() {
+ if (!isSortOrderUserChangeable()) {
+ return getDefaultSortOrder();
+ }
+ if (mSortOrder == PREFERENCE_UNASSIGNED) {
+ mSortOrder = mPreferences.getInt(SORT_ORDER_KEY, getDefaultSortOrder());
+ }
+ return mSortOrder;
+ }
+
+ public void setSortOrder(int sortOrder) {
+ mSortOrder = sortOrder;
+ final Editor editor = mPreferences.edit();
+ editor.putInt(SORT_ORDER_KEY, sortOrder);
+ editor.commit();
+ }
+
+ public boolean isDisplayOrderUserChangeable() {
+ return mContext.getResources().getBoolean(R.bool.config_display_order_user_changeable);
+ }
+
+ public int getDefaultDisplayOrder() {
+ if (mContext.getResources().getBoolean(R.bool.config_default_display_order_primary)) {
+ return DISPLAY_ORDER_PRIMARY;
+ } else {
+ return DISPLAY_ORDER_ALTERNATIVE;
+ }
+ }
+
+ public int getDisplayOrder() {
+ if (!isDisplayOrderUserChangeable()) {
+ return getDefaultDisplayOrder();
+ }
+ if (mDisplayOrder == PREFERENCE_UNASSIGNED) {
+ mDisplayOrder = mPreferences.getInt(DISPLAY_ORDER_KEY, getDefaultDisplayOrder());
+ }
+ return mDisplayOrder;
+ }
+
+ public void setDisplayOrder(int displayOrder) {
+ mDisplayOrder = displayOrder;
+ final Editor editor = mPreferences.edit();
+ editor.putInt(DISPLAY_ORDER_KEY, displayOrder);
+ editor.commit();
+ }
+
+ public boolean isDefaultAccountUserChangeable() {
+ return mContext.getResources().getBoolean(R.bool.config_default_account_user_changeable);
+ }
+
+ public String getDefaultAccount() {
+ if (!isDefaultAccountUserChangeable()) {
+ return mDefaultAccount;
+ }
+ if (TextUtils.isEmpty(mDefaultAccount)) {
+ final String accountString = mPreferences.getString(mDefaultAccountKey, mDefaultAccount);
+ if (!TextUtils.isEmpty(accountString)) {
+ final AccountWithDataSet accountWithDataSet = AccountWithDataSet.unstringify(accountString);
+ mDefaultAccount = accountWithDataSet.name;
+ }
+ }
+ return mDefaultAccount;
+ }
+
+ public void setDefaultAccount(AccountWithDataSet accountWithDataSet) {
+ mDefaultAccount = accountWithDataSet == null ? null : accountWithDataSet.name;
+ final Editor editor = mPreferences.edit();
+ if (TextUtils.isEmpty(mDefaultAccount)) {
+ editor.remove(mDefaultAccountKey);
+ } else {
+ editor.putString(mDefaultAccountKey, accountWithDataSet.stringify());
+ }
+ editor.putBoolean(mDefaultAccountSavedKey, true);
+ editor.commit();
+ }
+
+ public void registerChangeListener(ChangeListener listener) {
+ if (mListener != null) {
+ unregisterChangeListener();
+ }
+
+ mListener = listener;
+
+ // Reset preferences to "unknown" because they may have changed while the
+ // listener was unregistered.
+ mDisplayOrder = PREFERENCE_UNASSIGNED;
+ mSortOrder = PREFERENCE_UNASSIGNED;
+ mDefaultAccount = null;
+
+ mPreferences.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ public void unregisterChangeListener() {
+ if (mListener != null) {
+ mListener = null;
+ }
+
+ mPreferences.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, final String key) {
+ // This notification is not sent on the Ui thread. Use the previously created Handler
+ // to switch to the Ui thread
+ mHandler.post(
+ new Runnable() {
+ @Override
+ public void run() {
+ refreshValue(key);
+ }
+ });
+ }
+
+ /**
+ * Forces the value for the given key to be looked up from shared preferences and notifies the
+ * registered {@link ChangeListener}
+ *
+ * @param key the {@link SharedPreferences} key to look up
+ */
+ public void refreshValue(String key) {
+ if (DISPLAY_ORDER_KEY.equals(key)) {
+ mDisplayOrder = PREFERENCE_UNASSIGNED;
+ mDisplayOrder = getDisplayOrder();
+ } else if (SORT_ORDER_KEY.equals(key)) {
+ mSortOrder = PREFERENCE_UNASSIGNED;
+ mSortOrder = getSortOrder();
+ } else if (mDefaultAccountKey.equals(key)) {
+ mDefaultAccount = null;
+ mDefaultAccount = getDefaultAccount();
+ }
+ if (mListener != null) {
+ mListener.onChange();
+ }
+ }
+
+ /**
+ * If there are currently no preferences (which means this is the first time we are run), For sort
+ * order and display order, check to see if there are any preferences stored in system settings
+ * (pre-L) which can be copied into our own SharedPreferences. For default account setting, check
+ * to see if there are any preferences stored in the previous SharedPreferences which can be
+ * copied into current SharedPreferences.
+ */
+ private void maybeMigrateSystemSettings() {
+ if (!mPreferences.contains(SORT_ORDER_KEY)) {
+ int sortOrder = getDefaultSortOrder();
+ try {
+ sortOrder = Settings.System.getInt(mContext.getContentResolver(), SORT_ORDER_KEY);
+ } catch (SettingNotFoundException e) {
+ }
+ setSortOrder(sortOrder);
+ }
+
+ if (!mPreferences.contains(DISPLAY_ORDER_KEY)) {
+ int displayOrder = getDefaultDisplayOrder();
+ try {
+ displayOrder = Settings.System.getInt(mContext.getContentResolver(), DISPLAY_ORDER_KEY);
+ } catch (SettingNotFoundException e) {
+ }
+ setDisplayOrder(displayOrder);
+ }
+
+ if (!mPreferences.contains(mDefaultAccountKey)) {
+ final SharedPreferences previousPrefs =
+ PreferenceManager.getDefaultSharedPreferences(mContext);
+ final String defaultAccount = previousPrefs.getString(mDefaultAccountKey, null);
+ if (!TextUtils.isEmpty(defaultAccount)) {
+ final AccountWithDataSet accountWithDataSet =
+ AccountWithDataSet.unstringify(defaultAccount);
+ setDefaultAccount(accountWithDataSet);
+ }
+ }
+ }
+
+ public interface ChangeListener {
+
+ void onChange();
+ }
+}
diff --git a/java/com/android/contacts/common/preference/DisplayOrderPreference.java b/java/com/android/contacts/common/preference/DisplayOrderPreference.java
new file mode 100644
index 000000000..8dda57f9f
--- /dev/null
+++ b/java/com/android/contacts/common/preference/DisplayOrderPreference.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import com.android.contacts.common.R;
+
+/** Custom preference: view-name-as (first name first or last name first). */
+public final class DisplayOrderPreference extends ListPreference {
+
+ private ContactsPreferences mPreferences;
+ private Context mContext;
+
+ public DisplayOrderPreference(Context context) {
+ super(context);
+ prepare();
+ }
+
+ public DisplayOrderPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ prepare();
+ }
+
+ private void prepare() {
+ mContext = getContext();
+ mPreferences = new ContactsPreferences(mContext);
+ setEntries(
+ new String[] {
+ mContext.getString(R.string.display_options_view_given_name_first),
+ mContext.getString(R.string.display_options_view_family_name_first),
+ });
+ setEntryValues(
+ new String[] {
+ String.valueOf(ContactsPreferences.DISPLAY_ORDER_PRIMARY),
+ String.valueOf(ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE),
+ });
+ setValue(String.valueOf(mPreferences.getDisplayOrder()));
+ }
+
+ @Override
+ protected boolean shouldPersist() {
+ return false; // This preference takes care of its own storage
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ switch (mPreferences.getDisplayOrder()) {
+ case ContactsPreferences.DISPLAY_ORDER_PRIMARY:
+ return mContext.getString(R.string.display_options_view_given_name_first);
+ case ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE:
+ return mContext.getString(R.string.display_options_view_family_name_first);
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ int newValue = Integer.parseInt(value);
+ if (newValue != mPreferences.getDisplayOrder()) {
+ mPreferences.setDisplayOrder(newValue);
+ notifyChanged();
+ }
+ return true;
+ }
+
+ @Override
+ // UX recommendation is not to show cancel button on such lists.
+ protected void onPrepareDialogBuilder(Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+ builder.setNegativeButton(null, null);
+ }
+}
diff --git a/java/com/android/contacts/common/preference/SortOrderPreference.java b/java/com/android/contacts/common/preference/SortOrderPreference.java
new file mode 100644
index 000000000..9b6f57860
--- /dev/null
+++ b/java/com/android/contacts/common/preference/SortOrderPreference.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.preference;
+
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import com.android.contacts.common.R;
+
+/** Custom preference: sort-by. */
+public final class SortOrderPreference extends ListPreference {
+
+ private ContactsPreferences mPreferences;
+ private Context mContext;
+
+ public SortOrderPreference(Context context) {
+ super(context);
+ prepare();
+ }
+
+ public SortOrderPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ prepare();
+ }
+
+ private void prepare() {
+ mContext = getContext();
+ mPreferences = new ContactsPreferences(mContext);
+ setEntries(
+ new String[] {
+ mContext.getString(R.string.display_options_sort_by_given_name),
+ mContext.getString(R.string.display_options_sort_by_family_name),
+ });
+ setEntryValues(
+ new String[] {
+ String.valueOf(ContactsPreferences.SORT_ORDER_PRIMARY),
+ String.valueOf(ContactsPreferences.SORT_ORDER_ALTERNATIVE),
+ });
+ setValue(String.valueOf(mPreferences.getSortOrder()));
+ }
+
+ @Override
+ protected boolean shouldPersist() {
+ return false; // This preference takes care of its own storage
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ switch (mPreferences.getSortOrder()) {
+ case ContactsPreferences.SORT_ORDER_PRIMARY:
+ return mContext.getString(R.string.display_options_sort_by_given_name);
+ case ContactsPreferences.SORT_ORDER_ALTERNATIVE:
+ return mContext.getString(R.string.display_options_sort_by_family_name);
+ }
+ return null;
+ }
+
+ @Override
+ protected boolean persistString(String value) {
+ int newValue = Integer.parseInt(value);
+ if (newValue != mPreferences.getSortOrder()) {
+ mPreferences.setSortOrder(newValue);
+ notifyChanged();
+ }
+ return true;
+ }
+
+ @Override
+ // UX recommendation is not to show cancel button on such lists.
+ protected void onPrepareDialogBuilder(Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+ builder.setNegativeButton(null, null);
+ }
+}
diff --git a/java/com/android/contacts/common/res/color/popup_menu_color.xml b/java/com/android/contacts/common/res/color/popup_menu_color.xml
new file mode 100644
index 000000000..c52bd5b50
--- /dev/null
+++ b/java/com/android/contacts/common/res/color/popup_menu_color.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:alpha="0.5" android:color="#ff000000" android:state_enabled="false"/>
+ <item android:color="#ff000000"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/color/tab_text_color.xml b/java/com/android/contacts/common/res/color/tab_text_color.xml
new file mode 100644
index 000000000..71ef3e903
--- /dev/null
+++ b/java/com/android/contacts/common/res/color/tab_text_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/actionbar_text_color" android:state_selected="true"/>
+ <item android:color="@color/actionbar_unselected_text_color"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png
new file mode 100644
index 000000000..d86b2195a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..ddbb2c459
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..d5942dcad
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png
new file mode 100644
index 000000000..4dc506515
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..503e58e22
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png
new file mode 100644
index 000000000..969552935
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png
new file mode 100644
index 000000000..540ab4dee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..017e4bbf7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..703d30b92
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..c7b1113cf
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png
new file mode 100644
index 000000000..deb3a6dc1
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..06bd18fbb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..d829d11e2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..1ba12950c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..5ff3ac574
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..b4ebfc7b2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..03fd2fb10
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..e8cb0f5fe
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png
new file mode 100644
index 000000000..45137967c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..1c9bb81fa
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png
new file mode 100644
index 000000000..57177b7c6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png
new file mode 100644
index 000000000..56708b0ba
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..10ae5a70c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png
new file mode 100644
index 000000000..84b1227bd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..ccdda6701
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..3aa29b852
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..603ddc895
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png
new file mode 100644
index 000000000..97905c9f5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..c74bfab13
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..4ea7afa00
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png
new file mode 100644
index 000000000..cddf9be75
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..86578be45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..e9afcc924
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..2054530ed
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..a0f17568e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png
new file mode 100644
index 000000000..ae937176e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-hdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png
new file mode 100644
index 000000000..0d80482a9
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..4139942d6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..569d28f54
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png
new file mode 100644
index 000000000..5ec4c96a7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-hdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png
new file mode 100644
index 000000000..d86d61164
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..4139942d6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..065ff62ce
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png
new file mode 100644
index 000000000..013d5e711
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-mdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..947f03cec
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..6d09d7278
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..63c7456f0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-sw600dp-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png
new file mode 100644
index 000000000..f709f2ce4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..4139942d6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..af5855420
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png
new file mode 100644
index 000000000..cb801ac1b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-ldrtl-xhdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png
new file mode 100644
index 000000000..2b23b1ec5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..1a21fb400
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..3dddca516
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png
new file mode 100644
index 000000000..77f9de5e3
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..9d359db9f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png
new file mode 100644
index 000000000..590a728ad
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png
new file mode 100644
index 000000000..8a2df3992
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..ad268bf2f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..b3000d31e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..353e06495
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png
new file mode 100644
index 000000000..201ad40cb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..9aac6d79b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..39c16ed2d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..841509682
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..b8fc39aee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..736dfd6fa
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..8c44e7015
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..c16c6c5de
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..c67170e31
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png
new file mode 100644
index 000000000..3072b7569
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png
new file mode 100644
index 000000000..f0b1c725d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..38e0a2882
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png
new file mode 100644
index 000000000..fc4ddd32c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..1b43a07d0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..af75db4b4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..64995d147
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png
new file mode 100644
index 000000000..dc9655b6d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..a5a30213d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..3bf8e0362
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png
new file mode 100644
index 000000000..7d5d66de3
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..86578be45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..3226ab760
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..061904c42
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..1d9371de0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png
new file mode 100644
index 000000000..64bd6912c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-mdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..046b24a96
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-sw600dp-hdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..1ff337370
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-sw600dp-mdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..2eb7c7ebc
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-sw600dp-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png
new file mode 100644
index 000000000..71f782701
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..bb7327251
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..6256300b4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png
new file mode 100644
index 000000000..ef45e933a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..40eed1d12
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png
new file mode 100644
index 000000000..5769f1178
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png
new file mode 100644
index 000000000..48e75beee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..09c0e3efd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..e188d4a37
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..c571b2e3e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png
new file mode 100644
index 000000000..d2f709942
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..ce5f704ec
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..3d0580f93
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..f91b71847
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..2fbd458e9
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..2cdb2d7a1
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..65a6b7bbb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..48483a0b6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png
new file mode 100644
index 000000000..906791177
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..e053c757a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png
new file mode 100644
index 000000000..763767b4f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png
new file mode 100644
index 000000000..aea15f0be
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..7e7c289d4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png
new file mode 100644
index 000000000..fdfafed9a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..43319dc92
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..2d43c4d5b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..d2671edf7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png
new file mode 100644
index 000000000..c1783de67
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..ca9d7d66b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..eda10e612
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png
new file mode 100644
index 000000000..b65272542
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_background_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..86578be45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..5532e88c2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..f4af92657
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png
new file mode 100644
index 000000000..8fb0636cf
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_section_divider_holo_custom.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png
new file mode 100644
index 000000000..f4f00ca0f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xhdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png
new file mode 100644
index 000000000..142c5457d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..72c51b0d5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..8d67e448f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png
new file mode 100644
index 000000000..90ead2e45
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..2656cad18
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png
new file mode 100644
index 000000000..670bf796c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png
new file mode 100644
index 000000000..24142c729
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png
new file mode 100644
index 000000000..03cad4c90
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_group_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..f44df1afd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..c41a5fcff
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png
new file mode 100644
index 000000000..436a82da6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_back.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png
new file mode 100644
index 000000000..a70c60c03
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png
new file mode 100644
index 000000000..c64b9defe
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_group_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png
new file mode 100644
index 000000000..ff1759b8f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_overflow_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png
new file mode 100644
index 000000000..878e694ad
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png
new file mode 100644
index 000000000..ed4138f15
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_person_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png
new file mode 100644
index 000000000..0fec2f2b5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_remove_field_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png
new file mode 100644
index 000000000..fa682b11b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png
new file mode 100644
index 000000000..6c45bc8e6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_holo_light.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png
new file mode 100644
index 000000000..955f7383b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_menu_star_lt.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png
new file mode 100644
index 000000000..0a79824b8
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png
new file mode 100644
index 000000000..184f7418d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..8f744f039
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png
new file mode 100644
index 000000000..6a6cdeeaa
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png
new file mode 100644
index 000000000..89d29b7f5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_rx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..55f1d1369
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png
new file mode 100644
index 000000000..8d897ba5a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_tx_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png
new file mode 100644
index 000000000..4ab5ad0ee
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..d0979e9eb
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png
new file mode 100644
index 000000000..52c00ddcd
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_activated_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png
new file mode 100644
index 000000000..3e4ca684e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_focused_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
new file mode 100644
index 000000000..230d649bf
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_longpressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png
new file mode 100644
index 000000000..1352a1702
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_pressed_holo_light.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png
new file mode 100644
index 000000000..7ddf14a0d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxhdpi/list_title_holo.9.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png
new file mode 100644
index 000000000..2ffb2ecae
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_ab_search.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png
new file mode 100644
index 000000000..ae01a04ae
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_arrow_back_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png
new file mode 100644
index 000000000..1741675de
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_business_white_120dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png
new file mode 100644
index 000000000..b0e020573
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png
new file mode 100644
index 000000000..903c1623d
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_call_note_white_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png
new file mode 100644
index 000000000..3a5540ff6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_close_dk.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png
new file mode 100644
index 000000000..d3ff0ecb6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_create_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png
new file mode 100644
index 000000000..5b96af5b7
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_history_white_drawable_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png
new file mode 100644
index 000000000..3a82cab3b
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_info_outline_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png
new file mode 100644
index 000000000..fa7c17ac4
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_message_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png
new file mode 100644
index 000000000..33d40d8b6
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png
new file mode 100644
index 000000000..2fa2cca80
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_person_add_24dp.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png
new file mode 100644
index 000000000..b072ad11f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_phone_attach.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png
new file mode 100644
index 000000000..d90782a32
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_scroll_handle.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png
new file mode 100644
index 000000000..0643ea55f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_videocam.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png
new file mode 100644
index 000000000..1d6e1aa0f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable-xxxhdpi/ic_voicemail_avatar.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/drawable/dialog_background_material.xml b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml
new file mode 100644
index 000000000..1b71cd63a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/dialog_background_material.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<inset xmlns:android="http://schemas.android.com/apk/res/android"
+ android:inset="16dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="2dp"/>
+ <solid android:color="@color/call_subject_history_background"/>
+ </shape>
+</inset>
diff --git a/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml
new file mode 100644
index 000000000..67645ff91
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/fastscroll_thumb.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_scroll_handle_pressed" android:state_pressed="true"/>
+ <item android:drawable="@drawable/ic_scroll_handle_default"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml
new file mode 100644
index 000000000..56fab8f6f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_back_arrow.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_arrow_back_24dp"
+ android:tint="@color/actionbar_icon_color"/> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/ic_call.xml b/java/com/android/contacts/common/res/drawable/ic_call.xml
new file mode 100644
index 000000000..0fedd452f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_call.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_call_24dp"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml
new file mode 100644
index 000000000..3c6c8b534
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_message_24dp.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_message_24dp"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_more_vert.xml b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml
new file mode 100644
index 000000000..fcc3d9e4f
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_more_vert.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0"
+ android:width="24dp">
+ <path
+ android:fillColor="#FF000000"
+ android:pathData="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>
+</vector>
diff --git a/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml
new file mode 100644
index 000000000..0af90edb3
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_person_add_tinted_24dp.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_person_add_24dp"
+ android:tint="@color/actionbar_icon_color"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml
new file mode 100644
index 000000000..ac932f87c
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_default.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_scroll_handle"
+ android:tint="@color/dialer_secondary_text_color"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml
new file mode 100644
index 000000000..4838de58a
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_scroll_handle_pressed.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_scroll_handle"
+ android:tint="@color/dialtacts_theme_color"/> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml
new file mode 100644
index 000000000..801806044
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_search_add_contact.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_person_add_24dp"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml
new file mode 100644
index 000000000..f1b5cba43
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_search_video_call.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_videocam"
+ android:tint="@color/search_video_call_icon_tint"/>
diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_all.xml b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml
new file mode 100644
index 000000000..9cc6fbc96
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_tab_all.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_menu_person_lt" android:state_selected="false"/>
+ <item android:drawable="@drawable/ic_menu_person_dk" android:state_selected="true"/>
+</selector>
+
diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml
new file mode 100644
index 000000000..6b3e7a415
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_tab_groups.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_menu_group_lt" android:state_selected="false"/>
+ <item android:drawable="@drawable/ic_menu_group_dk" android:state_selected="true"/>
+</selector>
+
diff --git a/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml
new file mode 100644
index 000000000..a12e0993e
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_tab_starred.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_menu_star_lt" android:state_selected="false"/>
+ <item android:drawable="@drawable/ic_menu_star_dk" android:state_selected="true"/>
+</selector>
+
diff --git a/java/com/android/contacts/common/res/drawable/ic_work_profile.xml b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml
new file mode 100644
index 000000000..fc21100c0
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/ic_work_profile.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="16dp"
+ android:viewportHeight="48"
+ android:viewportWidth="48"
+ android:width="16dp">
+
+
+ <path
+ android:fillColor="#757575"
+ android:pathData="M28 33h-8v-3H6v8c0 2.2 1.8 4 4 4h28c2.2 0 4-1.8
+4-4v-8H28v3zm12-21h-7V9l-3-3H18l-3 3.1V12H8c-2.2 0-4 1.8-4 4v8c0 2.2 1.8 4 4
+4h12v-3h8v3h12c2.2 0 4-1.8 4-4v-8c0-2.2-1.8-4-4-4zm-10 0H18V9h12v3z"/>
+ <path
+ android:pathData="M0 0h48v48H0z"/>
+</vector>
diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml
new file mode 100644
index 000000000..94e309507
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/item_background_material_borderless_dark.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackgroundBorderless -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_material_dark"/> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml
new file mode 100644
index 000000000..91ab763a5
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/item_background_material_dark.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackground -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_material_dark">
+ <item android:id="@android:id/mask">
+ <color android:color="@android:color/white"/>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/item_background_material_light.xml b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml
new file mode 100644
index 000000000..d41accb02
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/item_background_material_light.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Based on the Theme.Material's default selectableItemBackground -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_material_light">
+ <item android:id="@android:id/mask">
+ <color android:color="@android:color/white"/>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml
new file mode 100644
index 000000000..5b774fd20
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/list_item_activated_background.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/list_activated_holo" android:state_activated="true"/>
+ <item android:drawable="@drawable/list_background_holo"/>
+</selector>
diff --git a/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml
new file mode 100644
index 000000000..35fff99c2
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/list_selector_background_transition_holo_light.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<transition xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/list_pressed_holo_light"/>
+ <item android:drawable="@drawable/list_longpressed_holo_light"/>
+</transition>
diff --git a/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml
new file mode 100644
index 000000000..27614a1ac
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/searchedittext_custom_cursor.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2014 Google Inc. All Rights Reserved. -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <size android:width="2dp"/>
+ <solid android:color="@color/dialtacts_theme_color"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/drawable/unread_count_background.xml b/java/com/android/contacts/common/res/drawable/unread_count_background.xml
new file mode 100644
index 000000000..4fc6b9b60
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/unread_count_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/tab_unread_count_background_radius"/>
+ <solid android:color="@color/tab_unread_count_background_color"/>
+</shape>
diff --git a/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml
new file mode 100644
index 000000000..bef30a434
--- /dev/null
+++ b/java/com/android/contacts/common/res/drawable/view_pager_tab_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/tab_ripple_color">
+ <item android:id="@android:id/mask">
+ <color android:color="@android:color/white"/>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
new file mode 100644
index 000000000..2aa97722d
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout-ldrtl/unread_count_tab.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<!-- layoutDirection set to ltr as a workaround to a framework bug (b/22010411) causing view with
+ layout_centerInParent inside a RelativeLayout to expand to screen width when RTL is active -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/view_pager_tab_background"
+ android:layoutDirection="ltr">
+ <!-- The tab icon -->
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"/>
+ <TextView
+ android:id="@+id/count"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/tab_unread_count_background_size"
+ android:layout_marginTop="@dimen/tab_unread_count_margin_top"
+ android:layout_marginStart="@dimen/tab_unread_count_margin_left"
+ android:layout_toStartOf="@id/icon"
+ android:paddingLeft="@dimen/tab_unread_count_text_padding"
+ android:paddingRight="@dimen/tab_unread_count_text_padding"
+ android:background="@drawable/unread_count_background"
+ android:fontFamily="sans-serif-medium"
+ android:importantForAccessibility="no"
+ android:layoutDirection="locale"
+ android:minWidth="@dimen/tab_unread_count_background_size"
+ android:textAlignment="center"
+ android:textColor="@color/tab_accent_color"
+ android:textSize="@dimen/tab_unread_count_text_size"/>
+</RelativeLayout>
+
diff --git a/java/com/android/contacts/common/res/layout/account_filter_header.xml b/java/com/android/contacts/common/res/layout/account_filter_header.xml
new file mode 100644
index 000000000..a12ab08fd
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/account_filter_header.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout showing the type of account filter
+ (e.g. All contacts filter, custom filter, etc.),
+ which is the header of all contact lists. -->
+
+<!-- Solely used to set a background color -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/background_primary">
+ <!-- Used to show the touch feedback -->
+ <FrameLayout
+ android:id="@+id/account_filter_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/contact_browser_list_header_left_margin"
+ android:layout_marginEnd="@dimen/contact_browser_list_header_right_margin"
+ android:paddingTop="@dimen/list_header_extra_top_padding"
+ android:background="?android:attr/selectableItemBackground"
+ android:visibility="gone">
+ <!-- Shows the text and underlining -->
+ <TextView
+ android:id="@+id/account_filter_header"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:paddingStart="@dimen/contact_browser_list_item_text_indent"
+ android:paddingLeft="@dimen/contact_browser_list_item_text_indent"/>
+ </FrameLayout>
+</FrameLayout>
diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml
new file mode 100644
index 000000000..587626e8d
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/account_selector_list_item.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/detail_network_icon_size"
+ android:layout_height="@dimen/detail_network_icon_size"
+ android:layout_margin="16dip"
+ android:layout_gravity="center_vertical"/>
+
+ <LinearLayout
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginLeft="8dp"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml
new file mode 100644
index 000000000..33821166e
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/account_selector_list_item_condensed.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeight"
+ android:orientation="horizontal">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/detail_network_icon_size"
+ android:layout_height="@dimen/detail_network_icon_size"
+ android:layout_margin="24dip"
+ android:layout_gravity="center_vertical"/>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dip"
+ android:layout_marginRight="8dip"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorSecondary"/>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/call_subject_history.xml b/java/com/android/contacts/common/res/layout/call_subject_history.xml
new file mode 100644
index 000000000..733f1d8b6
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/call_subject_history.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent">
+
+ <ListView
+ android:id="@+id/subject_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:background="@color/call_subject_history_background"
+ android:divider="@null"
+ android:elevation="8dp"/>
+
+</RelativeLayout> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml
new file mode 100644
index 000000000..c378f24b2
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/call_subject_history_list_item.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_subject_history_item_padding"
+ android:paddingBottom="@dimen/call_subject_history_item_padding"
+ android:paddingStart="@dimen/call_subject_dialog_margin"
+ android:paddingEnd="@dimen/call_subject_dialog_margin"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textSize="@dimen/call_subject_dialog_primary_text_size"/>
diff --git a/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml
new file mode 100644
index 000000000..02a5c809c
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/contact_detail_list_padding.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- The actual padding is embedded in a FrameLayout since we cannot change the
+ visibility of a header view in a ListView without having a parent view. -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <View
+ android:id="@+id/contact_detail_list_padding"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/list_header_extra_top_padding"/>
+</FrameLayout>
diff --git a/java/com/android/contacts/common/res/layout/contact_list_card.xml b/java/com/android/contacts/common/res/layout/contact_list_card.xml
new file mode 100644
index 000000000..a04f4cad9
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/contact_list_card.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/list_card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal"
+ android:visibility="invisible">
+ <View
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/contact_list_space_layout_weight"
+ android:background="@color/background_primary"/>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/contact_list_card_layout_weight"
+ android:background="@color/contact_all_list_background_color"
+ android:elevation="@dimen/contact_list_card_elevation"/>
+ <View
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="@integer/contact_list_space_layout_weight"
+ android:background="@color/background_primary"/>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/contact_list_content.xml b/java/com/android/contacts/common/res/layout/contact_list_content.xml
new file mode 100644
index 000000000..3ee27a0ad
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/contact_list_content.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- android:paddingTop is used instead of android:layout_marginTop. It looks
+ android:layout_marginTop is ignored when used with <fragment></fragment>, which
+ only happens in Tablet UI since we rely on ViewPager in Phone UI.
+ Instead, android:layout_marginTop inside <fragment /> is effective. -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?attr/contact_browser_background"
+ android:orientation="vertical">
+
+ <!-- Shown only when an Account filter is set.
+ - paddingTop should be here to show "shade" effect correctly. -->
+ <include layout="@layout/account_filter_header"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+ <include layout="@layout/contact_list_card"/>
+ <view
+ android:id="@android:id/list"
+ class="com.android.contacts.common.list.PinnedHeaderListView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="?attr/contact_browser_list_padding_left"
+ android:layout_marginEnd="?attr/contact_browser_list_padding_right"
+ android:layout_marginLeft="?attr/contact_browser_list_padding_left"
+ android:layout_marginRight="?attr/contact_browser_list_padding_right"
+ android:paddingTop="?attr/list_item_padding_top"
+ android:clipToPadding="false"
+ android:fadingEdge="none"
+ android:fastScrollEnabled="true"/>
+ <ProgressBar
+ android:id="@+id/search_progress"
+ style="?android:attr/progressBarStyleLarge"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+ </FrameLayout>
+
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/default_account_checkbox.xml b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml
new file mode 100644
index 000000000..b7c0cf644
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/default_account_checkbox.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/default_account_checkbox_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="4dp"
+ android:orientation="vertical">
+ <CheckBox
+ android:id="@+id/default_account_checkbox_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dip"
+ android:layout_marginLeft="13dip"
+ android:paddingStart="15dip"
+ android:gravity="center"
+ android:text="@string/set_default_account"
+ android:textAlignment="viewStart"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="@color/dialer_secondary_text_color"
+ />
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/dialog_call_subject.xml b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml
new file mode 100644
index 000000000..709bb50cb
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/dialog_call_subject.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_subject_dialog"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:orientation="vertical">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1">
+
+ <!-- The call subject dialog will be centered in the space above the subject list. -->
+ <LinearLayout
+ android:id="@+id/dialog_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:background="@drawable/dialog_background_material"
+ android:clickable="true"
+ android:elevation="16dp"
+ android:orientation="vertical"
+ android:theme="@android:style/Theme.Material.Light.Dialog">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_margin"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:orientation="horizontal">
+
+ <QuickContactBadge
+ android:id="@+id/contact_photo"
+ android:layout_width="@dimen/call_subject_dialog_contact_photo_size"
+ android:layout_height="@dimen/call_subject_dialog_contact_photo_size"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:layout_gravity="top"
+ android:focusable="true"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_between_line_margin"
+ android:layout_gravity="center_vertical"
+ android:singleLine="true"
+ android:textColor="@color/dialer_secondary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+ </LinearLayout>
+ </LinearLayout>
+
+ <EditText
+ android:id="@+id/call_subject"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_marginTop="@dimen/call_subject_dialog_edit_spacing"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:layout_gravity="top"
+ android:background="@null"
+ android:gravity="top"
+ android:hint="@string/call_subject_hint"
+ android:textColor="@color/dialer_secondary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"
+ />
+
+ <TextView
+ android:id="@+id/character_limit"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_margin"
+ android:layout_marginBottom="@dimen/call_subject_dialog_margin"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin"
+ android:singleLine="true"
+ android:textColor="@color/dialer_secondary_text_color"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+
+ <View
+ android:layout_width="fill_parent"
+ android:layout_height="1dp"
+ android:background="@color/call_subject_divider"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_subject_dialog_margin"
+ android:layout_marginBottom="@dimen/call_subject_dialog_margin"
+ android:layout_marginStart="@dimen/call_subject_dialog_margin"
+ android:layout_marginEnd="@dimen/call_subject_dialog_margin">
+
+ <ImageView
+ android:id="@+id/history_button"
+ android:layout_width="25dp"
+ android:layout_height="25dp"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:src="@drawable/ic_history_white_drawable_24dp"
+ android:tint="@color/call_subject_history_icon"/>
+
+ <TextView
+ android:id="@+id/send_and_call_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:singleLine="true"
+ android:text="@string/send_and_call_button"
+ android:textColor="@color/call_subject_button"
+ android:textSize="@dimen/call_subject_dialog_secondary_text_size"/>
+
+ </RelativeLayout>
+ </LinearLayout>
+ </RelativeLayout>
+ <!-- The subject list is pinned to the bottom of the screen. -->
+ <ListView
+ android:id="@+id/subject_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/dialog_view"
+ android:background="@color/call_subject_history_background"
+ android:divider="@null"
+ android:elevation="8dp"/>
+
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/directory_header.xml b/java/com/android/contacts/common/res/layout/directory_header.xml
new file mode 100644
index 000000000..b8f5163c0
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/directory_header.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout used for list section separators. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/directory_header"
+ style="@style/DirectoryHeader"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/directory_header_extra_top_padding"
+ android:paddingBottom="@dimen/directory_header_extra_bottom_padding"
+ android:paddingStart="?attr/list_item_padding_left"
+ android:paddingEnd="?attr/list_item_padding_right"
+ android:paddingLeft="?attr/list_item_padding_left"
+ android:paddingRight="?attr/list_item_padding_right"
+ android:background="?attr/contact_browser_background"
+ android:minHeight="@dimen/list_section_divider_min_height">
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/DirectoryHeaderStyle"/>
+ <TextView
+ android:id="@+id/display_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="8dp"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textAppearance="@style/DirectoryHeaderStyle"/>
+ <TextView
+ android:id="@+id/count"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:paddingTop="1dip"
+ android:gravity="end"
+ android:singleLine="true"
+ android:textAppearance="@style/DirectoryHeaderStyle"/>
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/list_separator.xml b/java/com/android/contacts/common/res/layout/list_separator.xml
new file mode 100644
index 000000000..ab60605c5
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/list_separator.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/title"
+ android:textStyle="bold"
+ android:paddingTop="16dip"
+ android:paddingBottom="15dip"
+ android:paddingStart="16dip"
+ android:paddingEnd="16dip"
+ android:paddingLeft="16dip"
+ android:paddingRight="16dip"
+ android:textColor="@color/frequently_contacted_title_color"
+ android:textSize="@dimen/frequently_contacted_title_text_size"/>
diff --git a/java/com/android/contacts/common/res/layout/search_bar_expanded.xml b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
new file mode 100644
index 000000000..8a3bd6088
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/search_bar_expanded.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_box_expanded"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ImageButton
+ android:id="@+id/search_back_button"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="@dimen/search_box_icon_size"
+ android:layout_margin="@dimen/search_box_navigation_icon_margin"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/action_menu_back_from_search"
+ android:src="@drawable/ic_back_arrow"
+ android:tint="@color/contactscommon_actionbar_background_color"/>
+
+ <EditText
+ android:id="@+id/search_view"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/search_box_icon_size"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/search_box_text_left_margin"
+ android:background="@null"
+ android:fontFamily="@string/search_font_family"
+ android:imeOptions="flagNoExtractUi"
+ android:inputType="textFilter"
+ android:singleLine="true"
+ android:textColor="@color/searchbox_text_color"
+ android:textColorHint="@color/searchbox_hint_text_color"
+ android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+ android:textSize="@dimen/search_text_size"/>
+
+ <ImageView
+ android:id="@+id/search_close_button"
+ android:layout_width="@dimen/search_box_close_icon_size"
+ android:layout_height="@dimen/search_box_close_icon_size"
+ android:padding="@dimen/search_box_close_icon_padding"
+ android:background="?attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/description_clear_search"
+ android:src="@drawable/ic_close_dk"
+ android:tint="@color/searchbox_icon_tint"/>
+
+</LinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/select_account_list_item.xml b/java/com/android/contacts/common/res/layout/select_account_list_item.xml
new file mode 100644
index 000000000..fbd31e573
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/select_account_list_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout of a single item in the InCallUI Account Chooser Dialog. -->
+<com.android.contacts.common.widget.ActivityTouchLinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="8dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:scaleType="center"/>
+
+ <LinearLayout
+ android:id="@+id/text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="8dp"
+ android:gravity="start|center_vertical"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/dialer_primary_text_color"/>
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:maxLines="1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:visibility="gone"/>
+ </LinearLayout>
+
+</com.android.contacts.common.widget.ActivityTouchLinearLayout>
diff --git a/java/com/android/contacts/common/res/layout/unread_count_tab.xml b/java/com/android/contacts/common/res/layout/unread_count_tab.xml
new file mode 100644
index 000000000..83481ee2d
--- /dev/null
+++ b/java/com/android/contacts/common/res/layout/unread_count_tab.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/view_pager_tab_background">
+ <!-- The tab icon -->
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"/>
+ <TextView
+ android:id="@+id/count"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/tab_unread_count_background_size"
+ android:layout_marginTop="@dimen/tab_unread_count_margin_top"
+ android:layout_marginStart="@dimen/tab_unread_count_margin_left"
+ android:layout_toEndOf="@id/icon"
+ android:paddingLeft="@dimen/tab_unread_count_text_padding"
+ android:paddingRight="@dimen/tab_unread_count_text_padding"
+ android:background="@drawable/unread_count_background"
+ android:fontFamily="sans-serif-medium"
+ android:gravity="center"
+ android:importantForAccessibility="no"
+ android:minWidth="@dimen/tab_unread_count_background_size"
+ android:textAlignment="center"
+ android:textColor="@color/tab_accent_color"
+ android:textSize="@dimen/tab_unread_count_text_size"/>
+</RelativeLayout>
diff --git a/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..64eff002f
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-hdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..b4ee8215a
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-mdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..6feeadfbe
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-xhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..01a3fde9d
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-xxhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png
new file mode 100644
index 000000000..328e067ee
--- /dev/null
+++ b/java/com/android/contacts/common/res/mipmap-xxxhdpi/ic_contacts_launcher.png
Binary files differ
diff --git a/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml
new file mode 100644
index 000000000..e05c6d658
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-ja/donottranslate_config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+
+ <!-- If true, phonetic name is included in the contact editor by default -->
+ <bool name="config_editor_include_phonetic_name">true</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml
new file mode 100644
index 000000000..8def55498
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-ko/donottranslate_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">false</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">false</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values-land/integers.xml b/java/com/android/contacts/common/res/values-land/integers.xml
new file mode 100644
index 000000000..26bac6222
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-land/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">60</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml
new file mode 100644
index 000000000..be4eb0bb0
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw600dp-land/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">20</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw600dp/dimens.xml b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml
new file mode 100644
index 000000000..cf67a1e72
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw600dp/dimens.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <dimen name="detail_item_side_margin">0dip</dimen>
+
+ <dimen name="contact_browser_list_header_left_margin">@dimen/list_visible_scrollbar_padding
+ </dimen>
+ <dimen name="contact_browser_list_header_right_margin">24dip</dimen>
+ <dimen name="contact_browser_list_top_margin">16dip</dimen>
+
+ <!-- Right margin of the floating action button -->
+ <dimen name="floating_action_button_margin_right">32dp</dimen>
+ <!-- Bottom margin of the floating action button -->
+ <dimen name="floating_action_button_margin_bottom">32dp</dimen>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw600dp/integers.xml b/java/com/android/contacts/common/res/values-sw600dp/integers.xml
new file mode 100644
index 000000000..31aeee995
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw600dp/integers.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <!-- Yikes, there is less space on a tablet! This makes the search experience rather
+ poor. Another reason to get rid of the exist tablet layout. -->
+ <integer name="snippet_length_before_tokenize">15</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml
new file mode 100644
index 000000000..577716d24
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw720dp-land/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">4</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">30</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-sw720dp/integers.xml b/java/com/android/contacts/common/res/values-sw720dp/integers.xml
new file mode 100644
index 000000000..05e309351
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-sw720dp/integers.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <integer name="contact_tile_column_count_in_favorites">2</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">20</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml
new file mode 100644
index 000000000..08023992b
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-zh-rCN/donottranslate_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml
new file mode 100644
index 000000000..08023992b
--- /dev/null
+++ b/java/com/android/contacts/common/res/values-zh-rTW/donottranslate_config.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">false</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/com/android/contacts/common/res/values/animation_constants.xml b/java/com/android/contacts/common/res/values/animation_constants.xml
new file mode 100644
index 000000000..9eec7d6c8
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/animation_constants.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="floating_action_button_animation_duration">250</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/attrs.xml b/java/com/android/contacts/common/res/values/attrs.xml
new file mode 100644
index 000000000..44d04f025
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/attrs.xml
@@ -0,0 +1,83 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <declare-styleable name="Theme">
+ <attr name="android:textColorSecondary"/>
+ </declare-styleable>
+
+ <declare-styleable name="ContactsDataKind">
+ <!-- Mime-type handled by this mapping. -->
+ <attr name="android:mimeType"/>
+ <!-- Icon used to represent data of this kind. -->
+ <attr name="android:icon"/>
+ <!-- Column in data table that summarizes this data. -->
+ <attr name="android:summaryColumn"/>
+ <!-- Column in data table that contains details for this data. -->
+ <attr name="android:detailColumn"/>
+ <!-- Flag indicating that detail should be built from SocialProvider. -->
+ <attr name="android:detailSocialSummary"/>
+ <!-- Resource representing the term "All Contacts" (e.g. "All Friends" or
+ "All connections"). Optional (Default is "All Contacts"). -->
+ <attr name="android:allContactsName"/>
+ </declare-styleable>
+
+ <declare-styleable name="ContactListItemView">
+ <attr format="dimension" name="list_item_height"/>
+ <attr format="dimension" name="list_section_header_height"/>
+ <attr format="reference" name="activated_background"/>
+ <attr format="reference" name="section_header_background"/>
+ <attr format="dimension" name="list_item_padding_top"/>
+ <attr format="dimension" name="list_item_padding_right"/>
+ <attr format="dimension" name="list_item_padding_bottom"/>
+ <attr format="dimension" name="list_item_padding_left"/>
+ <attr format="dimension" name="list_item_gap_between_image_and_text"/>
+ <attr format="dimension" name="list_item_gap_between_label_and_data"/>
+ <attr format="dimension" name="list_item_presence_icon_margin"/>
+ <attr format="dimension" name="list_item_presence_icon_size"/>
+ <attr format="dimension" name="list_item_photo_size"/>
+ <attr format="dimension" name="list_item_profile_photo_size"/>
+ <attr format="color" name="list_item_prefix_highlight_color"/>
+ <attr format="color" name="list_item_background_color"/>
+ <attr format="dimension" name="list_item_header_text_indent"/>
+ <attr format="color" name="list_item_header_text_color"/>
+ <attr format="dimension" name="list_item_header_text_size"/>
+ <attr format="dimension" name="list_item_header_height"/>
+ <attr format="color" name="list_item_name_text_color"/>
+ <attr format="dimension" name="list_item_name_text_size"/>
+ <attr format="dimension" name="list_item_text_indent"/>
+ <attr format="dimension" name="list_item_text_offset_top"/>
+ <attr format="integer" name="list_item_data_width_weight"/>
+ <attr format="integer" name="list_item_label_width_weight"/>
+ <attr format="dimension" name="list_item_video_call_icon_size"/>
+ <attr format="dimension" name="list_item_video_call_icon_margin"/>
+ </declare-styleable>
+
+ <declare-styleable name="ContactBrowser">
+ <attr format="dimension" name="contact_browser_list_padding_left"/>
+ <attr format="dimension" name="contact_browser_list_padding_right"/>
+ <attr format="reference" name="contact_browser_background"/>
+ </declare-styleable>
+
+ <declare-styleable name="ProportionalLayout">
+ <attr format="string" name="direction"/>
+ <attr format="float" name="ratio"/>
+ </declare-styleable>
+
+ <declare-styleable name="Favorites">
+ <attr format="dimension" name="favorites_padding_bottom"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/colors.xml b/java/com/android/contacts/common/res/values/colors.xml
new file mode 100644
index 000000000..7524eff58
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/colors.xml
@@ -0,0 +1,158 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <!-- Background color corresponding to the holo list 9-patch. -->
+ <color name="holo_list_background_color">#eeeeee</color>
+
+ <color name="focus_color">#44ff0000</color>
+
+ <!-- Color of ripples used for views with dark backgrounds -->
+ <color name="dialer_ripple_material_dark">#a0ffffff</color>
+
+ <!-- Color of ripples used for views with light backgrounds -->
+ <color name="dialer_ripple_material_light">#30000000</color>
+
+ <!-- Divider color for header separator -->
+ <color name="primary_text_color">#363636</color>
+
+ <color name="secondary_text_color">@color/dialer_secondary_text_color</color>
+
+ <!-- Text color for section header. -->
+ <color name="section_header_text_color">#2A56C6</color>
+
+ <!-- Divider color for header separator -->
+ <color name="main_header_separator_color">#AAAAAA</color>
+
+ <!-- Divider color for header separator -->
+ <color name="secondary_header_separator_color">#D0D0D0</color>
+
+ <!-- Color of the theme of the People app -->
+ <color name="people_app_theme_color">#363636</color>
+
+ <!-- Color of image view placeholder. -->
+ <color name="image_placeholder">#DDDDDD</color>
+
+ <!-- Color of the semi-transparent shadow box on contact tiles -->
+ <color name="contact_tile_shadow_box_color">#7F000000</color>
+
+ <!-- Color of the status message for starred contacts in the People app -->
+ <color name="people_contact_tile_status_color">#CCCCCC</color>
+
+ <color name="shortcut_overlay_text_background">#7f000000</color>
+
+ <color name="textColorIconOverlay">#fff</color>
+ <color name="textColorIconOverlayShadow">#000</color>
+
+
+ <array name="letter_tile_colors">
+ <item>#DB4437</item>
+ <item>#E91E63</item>
+ <item>#9C27B0</item>
+ <item>#673AB7</item>
+ <item>#3F51B5</item>
+ <item>#4285F4</item>
+ <item>#039BE5</item>
+ <item>#0097A7</item>
+ <item>#009688</item>
+ <item>#0F9D58</item>
+ <item>#689F38</item>
+ <item>#EF6C00</item>
+ <item>#FF5722</item>
+ <item>#757575</item>
+ </array>
+
+ <!-- Darker versions of letter_tile_colors, two shades darker. These colors are used
+ for settings secondary activity colors. -->
+ <array name="letter_tile_colors_dark">
+ <item>#C53929</item>
+ <item>#C2185B</item>
+ <item>#7B1FA2</item>
+ <item>#512DA8</item>
+ <item>#303F9F</item>
+ <item>#3367D6</item>
+ <item>#0277BD</item>
+ <item>#006064</item>
+ <item>#00796B</item>
+ <item>#0B8043</item>
+ <item>#33691E</item>
+ <item>#E65100</item>
+ <item>#E64A19</item>
+ <item>#424242</item>
+ </array>
+
+ <!-- The default color used for tinting photos when no color can be extracted via Palette,
+ this is Blue Grey 500 -->
+ <color name="quickcontact_default_photo_tint_color">#607D8B</color>
+ <!-- The default secondary color when no color can be extracted via Palette,
+ this is Blue Grey 700 -->
+ <color name="quickcontact_default_photo_tint_color_dark">#455A64</color>
+
+
+ <color name="letter_tile_default_color">#cccccc</color>
+
+ <color name="letter_tile_font_color">#ffffff</color>
+
+ <color name="contactscommon_actionbar_background_color">@color/dialer_theme_color</color>
+ <!-- Color for icons in the actionbar -->
+ <color name="actionbar_icon_color">#ffffff</color>
+ <!-- Darker version of the actionbar color. Used for the status bar and navigation bar colors. -->
+ <color name="actionbar_background_color_dark">#008aa1</color>
+
+ <color name="tab_ripple_color">#ffffff</color>
+ <color name="tab_accent_color">@color/tab_ripple_color</color>
+ <color name="tab_selected_underline_color">#f50057</color>
+ <color name="tab_unread_count_background_color">#1C3AA9</color>
+
+ <!-- Color of the title to the Frequently Contacted section -->
+ <color name="frequently_contacted_title_color">@color/contactscommon_actionbar_background_color
+ </color>
+
+ <!-- Color of action bar text. Ensure this stays in sync with packages/Telephony
+ phone_settings_actionbar_text_color-->
+ <color name="actionbar_text_color">#ffffff</color>
+ <color name="actionbar_unselected_text_color">#a6ffffff</color>
+
+ <!-- Text color of the search box text as entered by user -->
+ <color name="searchbox_text_color">#000000</color>
+ <!-- Background color of the search box -->
+ <color name="searchbox_background_color">#ffffff</color>
+
+ <color name="searchbox_hint_text_color">#737373</color>
+ <color name="searchbox_icon_tint">@color/searchbox_hint_text_color</color>
+
+ <color name="search_shortcut_icon_color">@color/dialtacts_theme_color</color>
+
+ <!-- Color of the background of the contact detail and editor pages -->
+ <color name="background_primary">#f9f9f9</color>
+ <color name="contact_all_list_background_color">#FFFFFF</color>
+
+ <!-- Text color used for character counter when the max limit has been exceeded -->
+ <color name="call_subject_limit_exceeded">#d1041c</color>
+
+ <!-- Tint color for the call subject history icon. -->
+ <color name="call_subject_history_icon">#000000</color>
+
+ <!-- Divider line on the call subject dialog. -->
+ <color name="call_subject_divider">#d8d8d8</color>
+
+ <!-- Text color for the SEND & CALL button on the call subject dialog. -->
+ <color name="call_subject_button">#00c853</color>
+
+ <!-- Background color for the call subject history view. -->
+ <color name="call_subject_history_background">#ffffff</color>
+ <color name="search_video_call_icon_tint">@color/searchbox_hint_text_color</color>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/dimens.xml b/java/com/android/contacts/common/res/values/dimens.xml
new file mode 100644
index 000000000..642eb31a4
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/dimens.xml
@@ -0,0 +1,161 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <!-- Padding between the action bar's bottom edge and the first header
+ in contacts/group lists. -->
+ <dimen name="list_header_extra_top_padding">0dip</dimen>
+
+ <!-- Minimum height used with @drawable/list_section_divider_holo_custom.
+ Right now the drawable has implicit 32dip minimal height, which is confusing.
+ This value is for making the hidden configuration explicit in xml. -->
+ <dimen name="list_section_divider_min_height">32dip</dimen>
+
+ <dimen name="directory_header_extra_top_padding">18dp</dimen>
+ <dimen name="directory_header_extra_bottom_padding">8dp</dimen>
+
+ <!-- Horizontal padding in between contact tiles -->
+ <dimen name="contact_tile_divider_padding">23dip</dimen>
+ <!-- Horizontal whitespace (both padding and margin) before the first tile and after the last tile -->
+ <dimen name="contact_tile_start_end_whitespace">16dip</dimen>
+
+ <!-- Left and right padding for a contact detail item -->
+ <dimen name="detail_item_side_margin">16dip</dimen>
+
+ <!-- ContactTile Layouts -->
+ <!--
+ Use sp instead of dip so that the shadowbox heights can all scale uniformly
+ when the font size is scaled for accessibility purposes
+ -->
+ <dimen name="contact_tile_shadowbox_height">48sp</dimen>
+
+ <!-- Top padding of the ListView in the contact tile list -->
+ <dimen name="contact_tile_list_padding_top">0dip</dimen>
+
+ <!-- Padding to be used between a visible scrollbar and the contact list -->
+ <dimen name="list_visible_scrollbar_padding">32dip</dimen>
+
+ <dimen name="contact_browser_list_header_left_margin">16dip</dimen>
+ <dimen name="contact_browser_list_header_right_margin">@dimen/list_visible_scrollbar_padding
+ </dimen>
+ <dimen name="contact_browser_list_item_text_indent">8dip</dimen>
+ <!-- Width of a contact list item section header. -->
+ <dimen name="contact_list_section_header_width">56dp</dimen>
+
+ <!-- Size of the shortcut icon. 0dip means: use the system default -->
+ <dimen name="shortcut_icon_size">0dip</dimen>
+
+ <!-- Text size of shortcut icon overlay text -->
+ <dimen name="shortcut_overlay_text_size">12dp</dimen>
+
+ <!-- Extra vertical padding for darkened background behind shortcut icon overlay text -->
+ <dimen name="shortcut_overlay_text_background_padding">1dp</dimen>
+
+ <!-- Width of height of an icon from a third-party app in the networks section of the contact card. -->
+ <dimen name="detail_network_icon_size">32dip</dimen>
+
+ <!-- Empty message margins -->
+ <dimen name="empty_message_top_margin">48dip</dimen>
+
+ <!-- contact browser list margins -->
+ <dimen name="contact_browser_list_item_text_size">16sp</dimen>
+ <dimen name="contact_browser_list_item_photo_size">40dp</dimen>
+ <dimen name="contact_browser_list_item_gap_between_image_and_text">15dp</dimen>
+ <dimen name="contact_browser_list_top_margin">12dp</dimen>
+
+ <!-- Dimensions for "No contacts" string in PhoneFavoriteFragment for the All contacts
+ with phone numbers section
+ -->
+ <dimen name="contact_phone_list_empty_description_size">20sp</dimen>
+ <dimen name="contact_phone_list_empty_description_padding">10dip</dimen>
+
+ <!-- Dimensions for contact letter tiles -->
+ <dimen name="tile_letter_font_size">40dp</dimen>
+ <dimen name="tile_letter_font_size_small">20dp</dimen>
+ <dimen name="tile_divider_width">1dp</dimen>
+ <item name="letter_to_tile_ratio" type="dimen">67%</item>
+
+ <!-- Height of the floating action button -->
+ <dimen name="floating_action_button_height">56dp</dimen>
+ <!-- Width of the floating action button -->
+ <dimen name="floating_action_button_width">56dp</dimen>
+ <!-- Z translation of the floating action button -->
+ <dimen name="floating_action_button_translation_z">8dp</dimen>
+ <!-- Padding to be applied to the bottom of lists to make space for the floating action
+ button -->
+ <dimen name="floating_action_button_list_bottom_padding">88dp</dimen>
+ <!-- Right margin of the floating action button -->
+ <dimen name="floating_action_button_margin_right">16dp</dimen>
+ <!-- Bottom margin of the floating action button -->
+ <dimen name="floating_action_button_margin_bottom">16dp</dimen>
+
+ <!-- Height of the selection indicator of a tab. -->
+ <dimen name="tab_selected_underline_height">2dp</dimen>
+ <!-- Size of text in tabs. -->
+ <dimen name="tab_text_size">14sp</dimen>
+ <dimen name="tab_elevation">2dp</dimen>
+ <dimen name="tab_unread_count_background_size">16dp</dimen>
+ <dimen name="tab_unread_count_background_radius">2dp</dimen>
+ <dimen name="tab_unread_count_margin_left">0dp</dimen>
+ <dimen name="tab_unread_count_margin_top">2dp</dimen>
+ <dimen name="tab_unread_count_text_size">12sp</dimen>
+ <dimen name="tab_unread_count_text_padding">2dp</dimen>
+
+ <!-- Padding around the icon in the search box. -->
+ <dimen name="search_box_icon_margin">4dp</dimen>
+ <!-- Size of the icon (voice search, back arrow) in the search box. -->
+ <dimen name="search_box_icon_size">48dp</dimen>
+ <!-- Size of the close icon.-->
+ <dimen name="search_box_close_icon_size">56dp</dimen>
+ <!-- Padding around the close button. It's visible size without padding is 24dp. -->
+ <dimen name="search_box_close_icon_padding">16dp</dimen>
+ <!-- Padding around back arrow icon in the search box -->
+ <dimen name="search_box_navigation_icon_margin">14dp</dimen>
+ <!-- Left margin of the text field in the search box. -->
+ <dimen name="search_box_text_left_margin">15dp</dimen>
+ <!-- Search box text size -->
+ <dimen name="search_text_size">20sp</dimen>
+
+ <!-- Top margin for the Frequently Contacted section title -->
+ <dimen name="frequently_contacted_title_top_margin_when_first_row">16dp</dimen>
+ <!-- Top margin for the Frequently Contacted section title, when the title is the first
+ item in the list -->
+ <dimen name="frequently_contacted_title_top_margin">57dp</dimen>
+
+ <dimen name="frequently_contacted_title_text_size">24sp</dimen>
+
+ <!-- Size of icon for contacts number shortcuts -->
+ <dimen name="search_shortcut_radius">40dp</dimen>
+
+ <dimen name="contact_list_card_elevation">2dp</dimen>
+
+ <!-- Padding used around the periphery of the call subject dialog, as well as in between the
+ items. -->
+ <dimen name="call_subject_dialog_margin">20dp</dimen>
+ <!-- Padding used between lines of text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_between_line_margin">8dp</dimen>
+ <!-- Size of the contact photo in the call subject dialog. -->
+ <dimen name="call_subject_dialog_contact_photo_size">50dp</dimen>
+ <!-- Margin above the edit text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_edit_spacing">60dp</dimen>
+ <!-- Size of primary text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_primary_text_size">16sp</dimen>
+ <!-- Size of secondary text in the call subject dialog. -->
+ <dimen name="call_subject_dialog_secondary_text_size">14sp</dimen>
+ <!-- Row padding for call subject history items. -->
+ <dimen name="call_subject_history_item_padding">15dp</dimen>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/donottranslate_config.xml b/java/com/android/contacts/common/res/values/donottranslate_config.xml
new file mode 100644
index 000000000..324437928
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/donottranslate_config.xml
@@ -0,0 +1,95 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <!-- Flag indicating whether Contacts app is allowed to import contacts -->
+ <bool name="config_allow_import_from_vcf_file">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a sort order -->
+ <bool name="config_sort_order_user_changeable">true</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_sort_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a name display order -->
+ <bool name="config_display_order_user_changeable">true</bool>
+
+ <!-- If true, the default sort order is primary (i.e. by given name) -->
+ <bool name="config_default_display_order_primary">true</bool>
+
+ <!-- If true, the order of name fields in the editor is primary (i.e. given name first) -->
+ <bool name="config_editor_field_order_primary">true</bool>
+
+ <!-- If true, an option is shown in Display Options UI to choose a default account -->
+ <bool name="config_default_account_user_changeable">true</bool>
+
+ <!-- Contacts preferences key for contact editor default account -->
+ <string name="contact_editor_default_account_key">ContactEditorUtils_default_account</string>
+
+ <!-- Contacts preferences key for contact editor anything saved -->
+ <string name="contact_editor_anything_saved_key">ContactEditorUtils_anything_saved</string>
+
+ <!-- The type of VCard for export. If you want to let the app emit vCard which is
+ specific to some vendor (like DoCoMo), specify this type (e.g. "docomo") -->
+ <string name="config_export_vcard_type" translatable="false">default</string>
+
+ <!-- The type of vcard for improt. If the vcard importer cannot guess the exact type
+ of a vCard type, the improter uses this type. -->
+ <string name="config_import_vcard_type" translatable="false">default</string>
+
+ <!-- Prefix of exported VCard file -->
+ <string name="config_export_file_prefix" translatable="false"></string>
+
+ <!-- Suffix of exported VCard file. Attached before an extension -->
+ <string name="config_export_file_suffix" translatable="false"></string>
+
+ <!-- Extension for exported VCard files -->
+ <string name="config_export_file_extension">vcf</string>
+
+ <!-- The filename that is suggested that users use when exporting vCards. Should include the .vcf extension. -->
+ <string name="exporting_vcard_filename" translatable="false">contacts.vcf</string>
+
+ <!-- Minimum number of exported VCard file index -->
+ <integer name="config_export_file_min_index">1</integer>
+
+ <!-- Maximum number of exported VCard file index -->
+ <integer name="config_export_file_max_index">99999</integer>
+
+ <!-- The list (separated by ',') of extensions should be checked in addition to
+ config_export_extension. e.g. If "aaa" is added to here and 00001.vcf and 00002.aaa
+ exist in a target directory, 00003.vcf becomes a next file name candidate.
+ Without this configuration, 00002.vcf becomes the candidate.-->
+ <string name="config_export_extensions_to_consider" translatable="false"></string>
+
+ <!-- If true, enable the "import contacts from SIM" feature if the device
+ has an appropriate SIM or ICC card.
+ Setting this flag to false in a resource overlay allows you to
+ entirely disable SIM import on a per-product basis. -->
+ <bool name="config_allow_sim_import">true</bool>
+
+ <!-- Flag indicating whether Contacts app is allowed to export contacts -->
+ <bool name="config_allow_export">true</bool>
+
+ <!-- Flag indicating whether Contacts app is allowed to share contacts with devices outside -->
+ <bool name="config_allow_share_contacts">true</bool>
+
+ <string name="pref_build_version_key">pref_build_version</string>
+ <string name="pref_open_source_licenses_key">pref_open_source_licenses</string>
+ <string name="pref_privacy_policy_key">pref_privacy_policy</string>
+ <string name="pref_terms_of_service_key">pref_terms_of_service</string>
+
+ <string name="star_sign">★</string>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/ids.xml b/java/com/android/contacts/common/res/values/ids.xml
new file mode 100644
index 000000000..871f5a636
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/ids.xml
@@ -0,0 +1,30 @@
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <!-- For Debug Purpose -->
+ <item name="cliv_name_textview" type="id"/>
+ <item name="cliv_label_textview" type="id"/>
+ <item name="cliv_data_view" type="id"/>
+
+ <!-- For tag ids used by ContactPhotoManager to tag views with contact details -->
+ <item name="tag_display_name" type="id"/>
+ <item name="tag_identifier" type="id"/>
+ <item name="tag_contact_type" type="id"/>
+
+ <item name="contact_tile_image" type="id"/>
+ <item name="contact_tile_name" type="id"/>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/integers.xml b/java/com/android/contacts/common/res/values/integers.xml
new file mode 100644
index 000000000..d38ad1da0
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/integers.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+
+ <!-- Determines the number of columns in a ContactTileRow in the favorites tab -->
+ <integer name="contact_tile_column_count_in_favorites">2</integer>
+ <integer name="contact_tile_column_count_in_favorites_new">3</integer>
+
+ <!-- The number of characters in the snippet before we need to tokenize and ellipse. -->
+ <integer name="snippet_length_before_tokenize">30</integer>
+
+ <!-- Layout weight of space elements in contact list view.
+ Default to 0 to indicate no padding-->
+ <integer name="contact_list_space_layout_weight">0</integer>
+ <!-- Layout weight of card in contact list view.
+ Default to 0 to indicate no padding -->
+ <integer name="contact_list_card_layout_weight">0</integer>
+
+ <!-- Duration of the animations on the call subject dialog. -->
+ <integer name="call_subject_animation_duration">250</integer>
+
+ <!-- A big number to make sure "About contacts" always showing at the bottom of Settings.-->
+ <integer name="about_contacts_order_number">100</integer>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/strings.xml b/java/com/android/contacts/common/res/values/strings.xml
new file mode 100644
index 000000000..15e1f15d9
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/strings.xml
@@ -0,0 +1,798 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Toast shown when text is copied to the clipboard [CHAR LIMIT=64] -->
+ <string name="toast_text_copied">Text copied</string>
+ <!-- Option displayed in context menu to copy long pressed item to clipboard [CHAR LIMIT=64] -->
+ <string name="copy_text">Copy to clipboard</string>
+
+ <!-- Action string for calling a custom phone number -->
+ <string name="call_custom">Call
+ <xliff:g id="custom">%s</xliff:g>
+ </string>
+ <!-- Action string for calling a home phone number -->
+ <string name="call_home">Call home</string>
+ <!-- Action string for calling a mobile phone number -->
+ <string name="call_mobile">Call mobile</string>
+ <!-- Action string for calling a work phone number -->
+ <string name="call_work">Call work</string>
+ <!-- Action string for calling a work fax phone number -->
+ <string name="call_fax_work">Call work fax</string>
+ <!-- Action string for calling a home fax phone number -->
+ <string name="call_fax_home">Call home fax</string>
+ <!-- Action string for calling a pager phone number -->
+ <string name="call_pager">Call pager</string>
+ <!-- Action string for calling an other phone number -->
+ <string name="call_other">Call</string>
+ <!-- Action string for calling a callback number -->
+ <string name="call_callback">Call callback</string>
+ <!-- Action string for calling a car phone number -->
+ <string name="call_car">Call car</string>
+ <!-- Action string for calling a company main phone number -->
+ <string name="call_company_main">Call company main</string>
+ <!-- Action string for calling a ISDN phone number -->
+ <string name="call_isdn">Call ISDN</string>
+ <!-- Action string for calling a main phone number -->
+ <string name="call_main">Call main</string>
+ <!-- Action string for calling an other fax phone number -->
+ <string name="call_other_fax">Call fax</string>
+ <!-- Action string for calling a radio phone number -->
+ <string name="call_radio">Call radio</string>
+ <!-- Action string for calling a Telex phone number -->
+ <string name="call_telex">Call telex</string>
+ <!-- Action string for calling a TTY/TDD phone number -->
+ <string name="call_tty_tdd">Call TTY/TDD</string>
+ <!-- Action string for calling a work mobile phone number -->
+ <string name="call_work_mobile">Call work mobile</string>
+ <!-- Action string for calling a work pager phone number -->
+ <string name="call_work_pager">Call work pager</string>
+ <!-- Action string for calling an assistant phone number -->
+ <string name="call_assistant">Call
+ <xliff:g id="assistant">%s</xliff:g>
+ </string>
+ <!-- Action string for calling a MMS phone number -->
+ <string name="call_mms">Call MMS</string>
+ <!-- Action string for calling a contact by shortcut -->
+ <string name="call_by_shortcut"><xliff:g id="contact_name">%s</xliff:g> (Call)</string>
+
+ <!-- Action string for sending an SMS to a custom phone number -->
+ <string name="sms_custom">Text
+ <xliff:g id="custom">%s</xliff:g>
+ </string>
+ <!-- Action string for sending an SMS to a home phone number -->
+ <string name="sms_home">Text home</string>
+ <!-- Action string for sending an SMS to a mobile phone number -->
+ <string name="sms_mobile">Text mobile</string>
+ <!-- Action string for sending an SMS to a work phone number -->
+ <string name="sms_work">Text work</string>
+ <!-- Action string for sending an SMS to a work fax phone number -->
+ <string name="sms_fax_work">Text work fax</string>
+ <!-- Action string for sending an SMS to a home fax phone number -->
+ <string name="sms_fax_home">Text home fax</string>
+ <!-- Action string for sending an SMS to a pager phone number -->
+ <string name="sms_pager">Text pager</string>
+ <!-- Action string for sending an SMS to an other phone number -->
+ <string name="sms_other">Text</string>
+ <!-- Action string for sending an SMS to a callback number -->
+ <string name="sms_callback">Text callback</string>
+ <!-- Action string for sending an SMS to a car phone number -->
+ <string name="sms_car">Text car</string>
+ <!-- Action string for sending an SMS to a company main phone number -->
+ <string name="sms_company_main">Text company main</string>
+ <!-- Action string for sending an SMS to a ISDN phone number -->
+ <string name="sms_isdn">Text ISDN</string>
+ <!-- Action string for sending an SMS to a main phone number -->
+ <string name="sms_main">Text main</string>
+ <!-- Action string for sending an SMS to an other fax phone number -->
+ <string name="sms_other_fax">Text fax</string>
+ <!-- Action string for sending an SMS to a radio phone number -->
+ <string name="sms_radio">Text radio</string>
+ <!-- Action string for sending an SMS to a Telex phone number -->
+ <string name="sms_telex">Text telex</string>
+ <!-- Action string for sending an SMS to a TTY/TDD phone number -->
+ <string name="sms_tty_tdd">Text TTY/TDD</string>
+ <!-- Action string for sending an SMS to a work mobile phone number -->
+ <string name="sms_work_mobile">Text work mobile</string>
+ <!-- Action string for sending an SMS to a work pager phone number -->
+ <string name="sms_work_pager">Text work pager</string>
+ <!-- Action string for sending an SMS to an assistant phone number -->
+ <string name="sms_assistant">Text
+ <xliff:g id="assistant">%s</xliff:g>
+ </string>
+ <!-- Action string for sending an SMS to a MMS phone number -->
+ <string name="sms_mms">Text MMS</string>
+ <!-- Action string for sending an SMS to a contact by shortcut -->
+ <string name="sms_by_shortcut"><xliff:g id="contact_name">%s</xliff:g> (Message)</string>
+
+ <!-- Title of the confirmation dialog for clearing frequents. [CHAR LIMIT=37] -->
+ <string name="clearFrequentsConfirmation_title">Clear frequently contacted?</string>
+
+ <!-- Confirmation dialog for clearing frequents. [CHAR LIMIT=NONE] -->
+ <string name="clearFrequentsConfirmation">You\'ll clear the frequently contacted list in the
+ Contacts and Phone apps, and force email apps to learn your addressing preferences from
+ scratch.
+ </string>
+
+ <!-- Title of the "Clearing frequently contacted" progress-dialog [CHAR LIMIT=35] -->
+ <string name="clearFrequentsProgress_title">Clearing frequently contacted\u2026</string>
+
+ <!-- Used to display as default status when the contact is available for chat [CHAR LIMIT=19] -->
+ <string name="status_available">Available</string>
+
+ <!-- Used to display as default status when the contact is away or idle for chat [CHAR LIMIT=19] -->
+ <string name="status_away">Away</string>
+
+ <!-- Used to display as default status when the contact is busy or Do not disturb for chat [CHAR LIMIT=19] -->
+ <string name="status_busy">Busy</string>
+
+ <!-- Directory partition name (also exists in contacts) -->
+ <string name="contactsList">Contacts</string>
+
+ <!-- The name of the invisible local contact directory -->
+ <string name="local_invisible_directory">Other</string>
+
+ <!-- The label in section header in the contact list for a contact directory [CHAR LIMIT=128] -->
+ <string name="directory_search_label">Directory</string>
+
+ <!-- The label in section header in the contact list for a work contact directory [CHAR LIMIT=128] -->
+ <string name="directory_search_label_work">Work directory</string>
+
+ <!-- The label in section header in the contact list for a local contacts [CHAR LIMIT=128] -->
+ <string name="local_search_label">All contacts</string>
+
+ <!-- String describing the text on the header of the profile contact in the contacts list
+ This may be programatically capitalized. [CHAR LIMIT=20] -->
+ <string msgid="9154761216179882405" name="user_profile_contacts_list_header">Me</string>
+
+ <!-- Title shown in the search result activity of contacts app while searching. [CHAR LIMIT=20]
+ (also in contacts) -->
+ <string name="search_results_searching">Searching\u2026</string>
+
+ <!-- Displayed at the top of search results indicating that more contacts were found than shown [CHAR LIMIT=64] -->
+ <string name="foundTooManyContacts">More than <xliff:g id="count">%d</xliff:g> found.</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts found when "Only contacts with phones" not selected. [CHAR LIMIT=30]
+ (also in contacts) -->
+ <string name="listFoundAllContactsZero">No contacts</string>
+
+ <!-- Displayed at the top of the contacts showing the total number of contacts found when typing search query -->
+ <plurals name="searchFoundContacts">
+ <item quantity="one">1 found</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> found</item>
+ </plurals>
+
+ <!-- String describing the text for photo of a contact in a contacts list.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_quick_contact_for">Quick contact for <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- Shown as the display name for a person when the name is missing or unknown. [CHAR LIMIT=18]-->
+ <string name="missing_name">(No name)</string>
+
+ <!-- The text displayed on the divider for the Favorites tab in Phone app indicating that items below it are frequently called as opposed to starred contacts [CHAR LIMIT = 39] -->
+ <string name="favoritesFrequentCalled">Frequently called</string>
+
+ <!-- The text displayed on the divider for the Favorites tab in People app indicating that items below it are frequently contacted [CHAR LIMIT = 39] -->
+ <string name="favoritesFrequentContacted">Frequently contacted</string>
+
+ <!-- String describing a contact picture that introduces users to the contact detail screen.
+
+ Used by AccessibilityService to announce the purpose of the button.
+
+ [CHAR LIMIT=NONE]
+ -->
+ <string msgid="2795575601596468581" name="description_view_contact_detail">View contact</string>
+
+ <!-- Contact list filter selection indicating that the list shows all contacts with phone numbers [CHAR LIMIT=64] -->
+ <string name="list_filter_phones">All contacts with phone numbers</string>
+
+ <!-- Contact list filter selection indicating that the list shows all work contacts with phone numbers [CHAR LIMIT=64] -->
+ <string name="list_filter_phones_work">Work profile contacts</string>
+
+ <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=25] -->
+ <string name="view_updates_from_group">View updates</string>
+
+ <!-- Title for data source when creating or editing a contact that doesn't
+ belong to a specific account. This contact will only exist on the device
+ and will not be synced. -->
+ <string name="account_phone">Device-only, unsynced</string>
+
+ <!-- Header that expands to list all name types when editing a structured name of a contact
+ [CHAR LIMIT=20] -->
+ <string name="nameLabelsGroup">Name</string>
+
+ <!-- Header that expands to list all nickname types when editing a nickname of a contact
+ [CHAR LIMIT=20] -->
+ <string name="nicknameLabelsGroup">Nickname</string>
+
+ <!-- Field title for the full name of a contact [CHAR LIMIT=64]-->
+ <string name="full_name">Name</string>
+ <!-- Field title for the given name of a contact -->
+ <string name="name_given">First name</string>
+ <!-- Field title for the family name of a contact -->
+ <string name="name_family">Last name</string>
+ <!-- Field title for the prefix name of a contact -->
+ <string name="name_prefix">Name prefix</string>
+ <!-- Field title for the middle name of a contact -->
+ <string name="name_middle">Middle name</string>
+ <!-- Field title for the suffix name of a contact -->
+ <string name="name_suffix">Name suffix</string>
+
+ <!-- Field title for the phonetic name of a contact [CHAR LIMIT=64]-->
+ <string name="name_phonetic">Phonetic name</string>
+
+ <!-- Field title for the phonetic given name of a contact -->
+ <string name="name_phonetic_given">Phonetic first name</string>
+ <!-- Field title for the phonetic middle name of a contact -->
+ <string name="name_phonetic_middle">Phonetic middle name</string>
+ <!-- Field title for the phonetic family name of a contact -->
+ <string name="name_phonetic_family">Phonetic last name</string>
+
+ <!-- Header that expands to list all of the types of phone numbers when editing or creating a
+ phone number for a contact [CHAR LIMIT=20] -->
+ <string name="phoneLabelsGroup">Phone</string>
+
+ <!-- Header that expands to list all of the types of email addresses when editing or creating
+ an email address for a contact [CHAR LIMIT=20] -->
+ <string name="emailLabelsGroup">Email</string>
+
+ <!-- Header that expands to list all of the types of postal addresses when editing or creating
+ an postal address for a contact [CHAR LIMIT=20] -->
+ <string name="postalLabelsGroup">Address</string>
+
+ <!-- Header that expands to list all of the types of IM account when editing or creating an IM
+ account for a contact [CHAR LIMIT=20] -->
+ <string name="imLabelsGroup">IM</string>
+
+ <!-- Header that expands to list all organization types when editing an organization of a
+ contact [CHAR LIMIT=20] -->
+ <string name="organizationLabelsGroup">Organization</string>
+
+ <!-- Header for the list of all relationships for a contact [CHAR LIMIT=20] -->
+ <string name="relationLabelsGroup">Relationship</string>
+
+ <!-- Header that expands to list all event types when editing an event of a contact
+ [CHAR LIMIT=20] -->
+ <string name="eventLabelsGroup">Special date</string>
+
+ <!-- Generic action string for text messaging a contact. Used by AccessibilityService to
+ announce the purpose of the view. [CHAR LIMIT=NONE] -->
+ <string name="sms">Text message</string>
+
+ <!-- Field title for the full postal address of a contact [CHAR LIMIT=64]-->
+ <string name="postal_address">Address</string>
+
+ <!-- Hint text for the organization name when editing -->
+ <string name="ghostData_company">Company</string>
+
+ <!-- Hint text for the organization title when editing -->
+ <string name="ghostData_title">Title</string>
+
+ <!-- The label describing the Notes field of a contact. This field allows free form text entry
+ about a contact -->
+ <string name="label_notes">Notes</string>
+
+ <!-- The label describing the SIP address field of a contact. [CHAR LIMIT=20] -->
+ <string name="label_sip_address">SIP</string>
+
+ <!-- Header that expands to list all website types when editing a website of a contact
+ [CHAR LIMIT=20] -->
+ <string name="websiteLabelsGroup">Website</string>
+
+ <!-- Header for the list of all groups for a contact [CHAR LIMIT=20] -->
+ <string name="groupsLabel">Groups</string>
+
+ <!-- Action string for sending an email to a home email address -->
+ <string name="email_home">Email home</string>
+ <!-- Action string for sending an email to a mobile email address -->
+ <string name="email_mobile">Email mobile</string>
+ <!-- Action string for sending an email to a work email address -->
+ <string name="email_work">Email work</string>
+ <!-- Action string for sending an email to an other email address -->
+ <string name="email_other">Email</string>
+ <!-- Action string for sending an email to a custom email address -->
+ <string name="email_custom">Email <xliff:g id="custom">%s</xliff:g></string>
+
+ <!-- Generic action string for sending an email -->
+ <string name="email">Email</string>
+
+ <!-- Field title for the street of a structured postal address of a contact -->
+ <string name="postal_street">Street</string>
+ <!-- Field title for the PO box of a structured postal address of a contact -->
+ <string name="postal_pobox">PO box</string>
+ <!-- Field title for the neighborhood of a structured postal address of a contact -->
+ <string name="postal_neighborhood">Neighborhood</string>
+ <!-- Field title for the city of a structured postal address of a contact -->
+ <string name="postal_city">City</string>
+ <!-- Field title for the region, or state, of a structured postal address of a contact -->
+ <string name="postal_region">State</string>
+ <!-- Field title for the postal code of a structured postal address of a contact -->
+ <string name="postal_postcode">ZIP code</string>
+ <!-- Field title for the country of a structured postal address of a contact -->
+ <string name="postal_country">Country</string>
+
+ <!-- Action string for viewing a home postal address -->
+ <string name="map_home">View home address</string>
+ <!-- Action string for viewing a work postal address -->
+ <string name="map_work">View work address</string>
+ <!-- Action string for viewing an other postal address -->
+ <string name="map_other">View address</string>
+ <!-- Action string for viewing a custom postal address -->
+ <string name="map_custom">View <xliff:g id="custom">%s</xliff:g> address</string>
+
+ <!-- Action string for starting an IM chat with the AIM protocol -->
+ <string name="chat_aim">Chat using AIM</string>
+ <!-- Action string for starting an IM chat with the MSN or Windows Live protocol -->
+ <string name="chat_msn">Chat using Windows Live</string>
+ <!-- Action string for starting an IM chat with the Yahoo protocol -->
+ <string name="chat_yahoo">Chat using Yahoo</string>
+ <!-- Action string for starting an IM chat with the Skype protocol -->
+ <string name="chat_skype">Chat using Skype</string>
+ <!-- Action string for starting an IM chat with the QQ protocol -->
+ <string name="chat_qq">Chat using QQ</string>
+ <!-- Action string for starting an IM chat with the Google Talk protocol -->
+ <string name="chat_gtalk">Chat using Google Talk</string>
+ <!-- Action string for starting an IM chat with the ICQ protocol -->
+ <string name="chat_icq">Chat using ICQ</string>
+ <!-- Action string for starting an IM chat with the Jabber protocol -->
+ <string name="chat_jabber">Chat using Jabber</string>
+
+ <!-- Generic action string for starting an IM chat -->
+ <string name="chat">Chat</string>
+
+ <!-- String describing the Contact Editor Minus button
+
+ Used by AccessibilityService to announce the purpose of the button.
+
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_minus_button">delete</string>
+
+ <!-- Content description for the expand or collapse name fields button.
+ Clicking this button causes the name editor to toggle between showing
+ a single field where the entire name is edited at once, or multiple
+ fields corresponding to each part of the name (Name Prefix, First Name,
+ Middle Name, Last Name, Name Suffix).
+ [CHAR LIMIT=NONE] -->
+ <string name="expand_collapse_name_fields_description">Expand or collapse name fields</string>
+
+ <!-- Content description for the expand or collapse phonetic name fields button. [CHAR LIMIT=100] -->
+ <string name="expand_collapse_phonetic_name_fields_description">Expand or collapse phonetic
+ name fields</string>
+
+ <!-- Contact list filter label indicating that the list is showing all available accounts [CHAR LIMIT=64] -->
+ <string name="list_filter_all_accounts">All contacts</string>
+
+ <!-- Menu item to indicate you are done editing a contact and want to save the changes you've made -->
+ <string name="menu_done">Done</string>
+
+ <!-- Menu item to indicate you want to cancel the current editing process and NOT save the changes you've made [CHAR LIMIT=12] -->
+ <string name="menu_doNotSave">Cancel</string>
+
+ <!-- Displayed at the top of the contacts showing the account filter selected [CHAR LIMIT=64] -->
+ <string name="listAllContactsInAccount">Contacts in <xliff:g example="abc@gmail.com" id="name">%s</xliff:g></string>
+
+ <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=64] -->
+ <string name="listCustomView">Contacts in custom view</string>
+
+ <!-- Displayed at the top of the contacts showing single contact. [CHAR LIMIT=64] -->
+ <string name="listSingleContact">Single contact</string>
+
+ <!-- Message asking user to select an account to save contacts imported from vcard or SIM card [CHAR LIMIT=64] -->
+ <string name="dialog_new_contact_account">Save imported contacts to:</string>
+
+ <!-- Action string for selecting SIM for importing contacts -->
+ <string name="import_from_sim">Import from SIM card</string>
+
+ <!-- Action string for selecting a SIM subscription for importing contacts -->
+ <string name="import_from_sim_summary">Import from SIM <xliff:g id="sim_name">^1</xliff:g> - <xliff:g id="sim_number">^2</xliff:g></string>
+
+ <!-- Action string for selecting a SIM subscription for importing contacts, without a phone number -->
+ <string name="import_from_sim_summary_no_number">Import from SIM <xliff:g id="sim_name">%1$s</xliff:g></string>
+
+ <!-- Action string for selecting a .vcf file to import contacts from [CHAR LIMIT=30] -->
+ <string name="import_from_vcf_file" product="default">Import from .vcf file</string>
+
+ <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard import.
+ The argument is file name for the vCard import the user wants to cancel.
+ [CHAR LIMIT=128] -->
+ <string name="cancel_import_confirmation_message">Cancel import of <xliff:g example="import.vcf" id="filename">%s</xliff:g>?</string>
+
+ <!-- Message shown in a Dialog confirming a user's cancel request toward existing vCard export.
+ The argument is file name for the vCard export the user wants to cancel.
+ [CHAR LIMIT=128] -->
+ <string name="cancel_export_confirmation_message">Cancel export of <xliff:g example="export.vcf" id="filename">%s</xliff:g>?</string>
+
+ <!-- Title shown in a Dialog telling users cancel vCard import/export operation is failed. [CHAR LIMIT=40] -->
+ <string name="cancel_vcard_import_or_export_failed">Couldn\'t cancel vCard import/export</string>
+
+ <!-- The failed reason which should not be shown but it may in some buggy condition. [CHAR LIMIT=40] -->
+ <string name="fail_reason_unknown">Unknown error.</string>
+
+ <!-- The failed reason shown when vCard importer/exporter could not open the file
+ specified by a user. The file name should be in the message. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_could_not_open_file">Couldn\'t open \"<xliff:g id="file_name">%s</xliff:g>\": <xliff:g id="exact_reason">%s</xliff:g>.</string>
+
+ <!-- The failed reason shown when contacts exporter fails to be initialized.
+ Some exact reason must follow this. [CHAR LIMIT=NONE]-->
+ <string name="fail_reason_could_not_initialize_exporter">Couldn\'t start the exporter: \"<xliff:g id="exact_reason">%s</xliff:g>\".</string>
+
+ <!-- The failed reason shown when there's no contact which is allowed to be exported.
+ Note that user may have contacts data but all of them are probably not allowed to be
+ exported because of security/permission reasons. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_no_exportable_contact">There is no exportable contact.</string>
+
+ <!-- The user doesn't have all permissions required to use the current screen. So
+ close the current screen and show the user this message. -->
+ <string name="missing_required_permission">You have disabled a required permission.</string>
+
+ <!-- The failed reason shown when some error happend during contacts export.
+ Some exact reason must follow this. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_error_occurred_during_export">An error occurred during export: \"<xliff:g id="exact_reason">%s</xliff:g>\".</string>
+
+ <!-- The failed reason shown when the given file name is too long for the system.
+ The length limit of each file is different in each Android device, so we don't need to
+ mention it here. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_too_long_filename">Required filename is too long (\"<xliff:g id="filename">%s</xliff:g>\").</string>
+
+ <!-- The failed reason shown when Contacts app (especially vCard importer/exporter)
+ emitted some I/O error. Exact reason will be appended by the system. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_io_error">I/O error</string>
+
+ <!-- Failure reason show when Contacts app (especially vCard importer) encountered
+ low memory problem and could not proceed its import procedure. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_low_memory_during_import">Not enough memory. The file may be too large.</string>
+
+ <!-- The failed reason shown when vCard parser was not able to be parsed by the current vCard
+ implementation. This might happen even when the input vCard is completely valid, though
+ we believe it is rather rare in the actual world. [CHAR LIMIT=NONE] -->
+ <string name="fail_reason_vcard_parse_error">Couldn\'t parse vCard for an unexpected reason.</string>
+
+ <!-- The failed reason shown when vCard importer doesn't support the format.
+ This may be shown when the vCard is corrupted [CHAR LIMIT=40] -->
+ <string name="fail_reason_not_supported">The format isn\'t supported.</string>
+
+ <!-- Fail reason shown when vCard importer failed to look over meta information stored in vCard file(s). -->
+ <string name="fail_reason_failed_to_collect_vcard_meta_info">Couldn\'t collect meta information of given vCard file(s).</string>
+
+ <!-- The failed reason shown when the import of some of vCard files failed during multiple vCard
+ files import. It includes the case where all files were failed to be imported. -->
+ <string name="fail_reason_failed_to_read_files">One or more files couldn\'t be imported (%s).</string>
+
+ <!-- The title shown when exporting vCard is successfuly finished [CHAR LIMIT=40] -->
+ <string name="exporting_vcard_finished_title">Finished exporting <xliff:g example="export.vcf" id="filename">%s</xliff:g>.</string>
+
+ <!-- The title shown when exporting vCard has finished successfully but the destination filename could not be resolved. [CHAR LIMIT=NONE] -->
+ <string name="exporting_vcard_finished_title_fallback">Finished exporting contacts.</string>
+
+ <!-- The toast message shown when exporting vCard has finished and vCards are ready to be shared [CHAR LIMIT=150]-->
+ <string name="exporting_vcard_finished_toast">Finished exporting contacts, click the notification to share contacts.</string>
+
+ <!-- The message on notification shown when exporting vCard has finished and vCards are ready to be shared [CHAR LIMIT=60]-->
+ <string name="touch_to_share_contacts">Tap to share contacts.</string>
+
+ <!-- The title shown when exporting vCard is canceled (probably by a user)
+ The argument is file name the user canceled importing.
+ [CHAR LIMIT=40] -->
+ <string name="exporting_vcard_canceled_title">Exporting <xliff:g example="export.vcf" id="filename">%s</xliff:g> canceled.</string>
+
+ <!-- Dialog title shown when the application is exporting contact data outside. [CHAR LIMIT=NONE] -->
+ <string name="exporting_contact_list_title">Exporting contact data</string>
+
+ <!-- Message shown when the application is exporting contact data outside -->
+ <string name="exporting_contact_list_message">Contact data is being exported.</string>
+
+ <!-- The error reason the vCard composer "may" emit when database is corrupted or
+ something is going wrong. Usually users should not see this text. [CHAR LIMIT=NONE] -->
+ <string name="composer_failed_to_get_database_infomation">Couldn\'t get database information.</string>
+
+ <!-- This error message shown when the user actually have no contact
+ (e.g. just after data-wiping), or, data providers of the contact list prohibits their
+ contacts from being exported to outside world via vcard exporter, etc. [CHAR LIMIT=NONE] -->
+ <string name="composer_has_no_exportable_contact">There are no exportable contacts. If you do have contacts on your device, some data providers may not allow the contacts to be exported from the device.</string>
+
+ <!-- The error reason the vCard composer may emit when vCard composer is not initialized
+ even when needed.
+ Users should not usually see this error message. [CHAR LIMIT=NONE] -->
+ <string name="composer_not_initialized">The vCard composer didn\'t start properly.</string>
+
+ <!-- Dialog title shown when exporting Contact data failed. [CHAR LIMIT=20] -->
+ <string name="exporting_contact_failed_title">Couldn\'t export</string>
+
+ <!-- Dialog message shown when exporting Contact data failed. [CHAR LIMIT=NONE] -->
+ <string name="exporting_contact_failed_message">The contact data wasn\'t exported.\nReason: \"<xliff:g id="fail_reason">%s</xliff:g>\"</string>
+
+ <!-- Description shown when importing vCard data.
+ The argument is the name of a contact which is being read.
+ [CHAR LIMIT=20] -->
+ <string name="importing_vcard_description">Importing <xliff:g example="Joe Due" id="name">%s</xliff:g></string>
+
+ <!-- Dialog title shown when reading vCard data failed [CHAR LIMIT=40] -->
+ <string name="reading_vcard_failed_title">Couldn\'t read vCard data</string>
+
+ <!-- The title shown when reading vCard is canceled (probably by a user)
+ [CHAR LIMIT=40] -->
+ <string name="reading_vcard_canceled_title">Reading vCard data canceled</string>
+
+ <!-- The title shown when reading vCard finished
+ The argument is file name the user imported.
+ [CHAR LIMIT=40] -->
+ <string name="importing_vcard_finished_title">Finished importing vCard <xliff:g example="import.vcf" id="filename">%s</xliff:g></string>
+
+ <!-- The title shown when importing vCard is canceled (probably by a user)
+ The argument is file name the user canceled importing.
+ [CHAR LIMIT=40] -->
+ <string name="importing_vcard_canceled_title">Importing <xliff:g example="import.vcf" id="filename">%s</xliff:g> canceled</string>
+
+ <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later
+ when there are already other import/export requests.
+ The argument is file name the user imported.
+ [CHAR LIMIT=40] -->
+ <string name="vcard_import_will_start_message"><xliff:g example="import.vcf" id="filename">%s</xliff:g> will be imported shortly.</string>
+ <!-- The message shown when vCard import request is accepted. The system may start that work soon, or do it later when there are already other import/export requests.
+ "The file" is what a user selected for importing.
+ [CHAR LIMIT=40] -->
+ <string name="vcard_import_will_start_message_with_default_name">The file will be imported shortly.</string>
+ <!-- The message shown when a given vCard import request is rejected by the system. [CHAR LIMIT=NONE] -->
+ <string name="vcard_import_request_rejected_message">vCard import request was rejected. Try again later.</string>
+ <!-- The message shown when vCard export request is accepted. The system may start that work soon, or do it later
+ when there are already other import/export requests.
+ The argument is file name the user exported.
+ [CHAR LIMIT=40] -->
+ <string name="vcard_export_will_start_message"><xliff:g example="import.vcf" id="filename">%s</xliff:g> will be exported shortly.</string>
+
+ <!-- The message shown when a vCard export request is accepted but the destination filename could not be resolved. [CHAR LIMIT=NONE] -->
+ <string name="vcard_export_will_start_message_fallback">The file will be exported shortly.</string>
+
+ <!-- The message shown when a vCard export request is accepted and contacts will be exported shortly. [CHAR LIMIT=70]-->
+ <string name="contacts_export_will_start_message">Contacts will be exported shortly.</string>
+
+ <!-- The message shown when a given vCard export request is rejected by the system. [CHAR LIMIT=NONE] -->
+ <string name="vcard_export_request_rejected_message">vCard export request was rejected. Try again later.</string>
+ <!-- Used when file name is unknown in vCard processing. It typically happens
+ when the file is given outside the Contacts app. [CHAR LIMIT=30] -->
+ <string name="vcard_unknown_filename">contact</string>
+
+ <!-- The message shown when vCard importer is caching files to be imported into local temporary
+ data storage. [CHAR LIMIT=NONE] -->
+ <string name="caching_vcard_message">Caching vCard(s) to local temporary storage. The actual import will start soon.</string>
+
+ <!-- Message used when vCard import has failed. [CHAR LIMIT=40] -->
+ <string name="vcard_import_failed">Couldn\'t import vCard.</string>
+
+ <!-- The "file name" displayed for vCards received directly via NFC [CHAR LIMIT=16] -->
+ <string name="nfc_vcard_file_name">Contact received over NFC</string>
+
+ <!-- Dialog title shown when a user confirms whether he/she export Contact data. [CHAR LIMIT=32] -->
+ <string name="confirm_export_title">Export contacts?</string>
+
+ <!-- The title shown when vCard importer is caching files to be imported into local temporary
+ data storage. [CHAR LIMIT=40] -->
+ <string name="caching_vcard_title">Caching</string>
+
+ <!-- The message shown while importing vCard(s).
+ First argument is current index of contacts to be imported.
+ Second argument is the total number of contacts.
+ Third argument is the name of a contact which is being read.
+ [CHAR LIMIT=20] -->
+ <string name="progress_notifier_message">Importing <xliff:g id="current_number">%s</xliff:g>/<xliff:g id="total_number">%s</xliff:g>: <xliff:g example="Joe Due" id="name">%s</xliff:g></string>
+
+ <!-- Action that exports all contacts to a user selected destination. [CHAR LIMIT=25] -->
+ <string name="export_to_vcf_file" product="default">Export to .vcf file</string>
+
+ <!-- Contact preferences related strings -->
+
+ <!-- Label of the "sort by" display option -->
+ <string name="display_options_sort_list_by">Sort by</string>
+
+ <!-- An allowable value for the "sort list by" contact display option -->
+ <string name="display_options_sort_by_given_name">First name</string>
+
+ <!-- An allowable value for the "sort list by" contact display option -->
+ <string name="display_options_sort_by_family_name">Last name</string>
+
+ <!-- Label of the "name format" display option [CHAR LIMIT=64]-->
+ <string name="display_options_view_names_as">Name format</string>
+
+ <!-- An allowable value for the "view names as" contact display option -->
+ <string name="display_options_view_given_name_first">First name first</string>
+
+ <!-- An allowable value for the "view names as" contact display option -->
+ <string name="display_options_view_family_name_first">Last name first</string>
+
+ <!--Label of the "default account" setting option to set default editor account. [CHAR LIMIT=80]-->
+ <string name="default_editor_account">Default account for new contacts</string>
+
+ <!--Label of the "Sync contact metadata" setting option to set sync account for Lychee. [CHAR LIMIT=80]-->
+ <string name="sync_contact_metadata_title">Sync contact metadata [DOGFOOD]</string>
+
+ <!--Label of the "Sync contact metadata" setting dialog to set sync account for Lychee. [CHAR LIMIT=80]-->
+ <string name="sync_contact_metadata_dialog_title">Sync contact metadata</string>
+
+ <!-- Label of the "About" setting -->
+ <string name="setting_about">About Contacts</string>
+
+ <!-- Title of the settings activity [CHAR LIMIT=64] -->
+ <string name="activity_title_settings">Settings</string>
+
+ <!-- Action that shares visible contacts -->
+ <string name="share_visible_contacts">Share visible contacts</string>
+
+ <!-- A framework exception (ie, transaction too large) can be thrown while attempting to share all visible contacts. If so, show this toast. -->
+ <string name="share_visible_contacts_failure">Failed to share visible contacts.</string>
+
+ <!-- Action that shares favorite contacts [CHAR LIMIT=40]-->
+ <string name="share_favorite_contacts">Share favorite contacts</string>
+
+ <!-- Action that shares contacts [CHAR LIMIT=30]-->
+ <string name="share_contacts">Share all contacts</string>
+
+ <!-- A framework exception can be thrown while attempting to share all contacts. If so, show this toast. [CHAR LIMIT=40]-->
+ <string name="share_contacts_failure">Failed to share contacts.</string>
+
+ <!-- Dialog title when selecting the bulk operation to perform from a list. [CHAR LIMIT=36] -->
+ <string name="dialog_import_export">Import/export contacts</string>
+
+ <!-- Dialog title when importing contacts from an external source. [CHAR LIMIT=36] -->
+ <string name="dialog_import">Import contacts</string>
+
+ <!-- Toast indicating that sharing a contact has failed. [CHAR LIMIT=NONE] -->
+ <string name="share_error">This contact can\'t be shared.</string>
+
+ <!-- Toast indicating that no visible contact to share [CHAR LIMIT=NONE] -->
+ <string name="no_contact_to_share">There are no contacts to share.</string>
+
+ <!-- Menu item to search contacts -->
+ <string name="menu_search">Search</string>
+
+ <!-- Query hint displayed inside the search field [CHAR LIMIT=64] -->
+ <string name="hint_findContacts">Find contacts</string>
+
+ <!-- The description text for the favorites tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE] -->
+ <string name="contactsFavoritesLabel">Favorites</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when "All contacts" is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZero">No contacts.</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when "Custom" is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZeroCustom">No visible contacts.</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when starred contact list is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZeroStarred">No favorites</string>
+
+ <!-- Displayed at the top of the contacts showing the zero total number of contacts visible when a group or account is selected [CHAR LIMIT=64]-->
+ <string name="listTotalAllContactsZeroGroup">No contacts in <xliff:g example="Friends" id="name">%s</xliff:g></string>
+
+ <!-- The menu item to clear frequents [CHAR LIMIT=30] -->
+ <string name="menu_clear_frequents">Clear frequents</string>
+
+ <!-- Menu item to select SIM card -->
+ <string name="menu_select_sim">Select SIM card</string>
+
+ <!-- The menu item to open the list of accounts. [CHAR LIMIT=60]-->
+ <string name="menu_accounts">Manage accounts</string>
+
+ <!-- The menu item to bulk import or bulk export contacts from SIM card or SD card. [CHAR LIMIT=30]-->
+ <string name="menu_import_export">Import/export</string>
+
+ <!-- The font-family to use for tab text. -->
+ <string name="tab_font_family" translatable="false">sans-serif</string>
+
+ <!-- Attribution of a contact status update, when the time of update is unknown -->
+ <string name="contact_status_update_attribution">via <xliff:g example="Google Talk" id="source">%1$s</xliff:g></string>
+
+ <!-- Attribution of a contact status update, when the time of update is known -->
+ <string name="contact_status_update_attribution_with_date"><xliff:g example="3 hours ago" id="date">%1$s</xliff:g> via <xliff:g example="Google Talk" id="source">%2$s</xliff:g></string>
+
+ <!-- Font family used when drawing letters for letter tile avatars. -->
+ <string name="letter_tile_letter_font_family" translatable="false">sans-serif-medium</string>
+
+ <!-- Content description for the fake action menu up button as used
+ inside search. [CHAR LIMIT=NONE] -->
+ <string name="action_menu_back_from_search">stop searching</string>
+
+ <!-- String describing the icon used to clear the search field -->
+ <string name="description_clear_search">Clear search</string>
+
+ <!-- The font-family to use for the text inside the searchbox. -->
+ <string name="search_font_family" translatable="false">sans-serif</string>
+
+ <!-- The title of the preference section that allows users to configure how they want their
+ contacts to be displayed. [CHAR LIMIT=128] -->
+ <string name="settings_contact_display_options_title">Contact display options</string>
+
+ <!-- Title for Select Account Dialog [CHAR LIMIT=30] -->
+ <string name="select_account_dialog_title">Account</string>
+
+ <!-- Label for the check box to toggle default sim card setting [CHAR LIMIT=35]-->
+ <string name="set_default_account">Always use this for calls</string>
+
+ <!-- Title for dialog to select Phone Account for outgoing call. [CHAR LIMIT=40] -->
+ <string name="select_phone_account_for_calls">Call with</string>
+
+ <!-- String used for actions in the dialer call log and the quick contact card to initiate
+ a call to an individual. The user is prompted to enter a note which is sent along with
+ the call (e.g. a call subject). [CHAR LIMIT=40] -->
+ <string name="call_with_a_note">Call with a note</string>
+
+ <!-- Hint text shown in the call subject dialog. [CHAR LIMIT=255] -->
+ <string name="call_subject_hint">Type a note to send with call ...</string>
+
+ <!-- Button used to start a new call with the user entered subject. [CHAR LIMIT=32] -->
+ <string name="send_and_call_button">SEND &amp; CALL</string>
+
+ <!-- String used to represent the total number of characters entered for a call subject,
+ compared to the character limit. Example: 2 / 64 -->
+ <string name="call_subject_limit"><xliff:g example="4" id="count">%1$s</xliff:g> / <xliff:g example="64" id="limit">%2$s</xliff:g></string>
+
+ <!-- String used to build a phone number bype and phone number string.
+ Example: Mobile • 650-555-1212 -->
+ <string name="call_subject_type_and_number"><xliff:g example="Mobile" id="type">%1$s</xliff:g> • <xliff:g example="(650) 555-1212" id="number">%2$s</xliff:g></string>
+
+ <!-- String format to describe a tab e.g.call history tab. -->
+ <string name="tab_title"><xliff:g id="title">%1$s</xliff:g> tab.</string>
+
+ <!-- String format to describe the number of unread items in a tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <plurals name="tab_title_with_unread_items">
+ <item quantity="one">
+ <xliff:g id="title">%1$s</xliff:g> tab. <xliff:g id="count">%2$d</xliff:g> unread item.
+ </item>
+ <item quantity="other">
+ <xliff:g id="title">%1$s</xliff:g> tab. <xliff:g id="count">%2$d</xliff:g> unread items.
+ </item>
+ </plurals>
+
+ <!-- Build version title in About preference. [CHAR LIMIT=40]-->
+ <string name="about_build_version">Build version</string>
+
+ <!-- Open source licenses title in About preference. [CHAR LIMIT=60] -->
+ <string name="about_open_source_licenses">Open source licenses</string>
+
+ <!-- Open source licenses summary in About preference. [CHAR LIMIT=NONE] -->
+ <string name="about_open_source_licenses_summary">License details for open source software</string>
+
+ <!-- Privacy policy title in About preference. [CHAR LIMIT=40]-->
+ <string name="about_privacy_policy">Privacy policy</string>
+
+ <!-- Terms of service title in about preference. [CHAR LIMIT=60]-->
+ <string name="about_terms_of_service">Terms of service</string>
+
+ <!-- Title for the activity that displays licenses for open source libraries. [CHAR LIMIT=100]-->
+ <string name="activity_title_licenses">Open source licenses</string>
+
+ <!-- Toast message showing when failed to open the url. [CHAR LIMIT=100]-->
+ <string name="url_open_error_toast">Failed to open the url.</string>
+
+ <!-- Description string for an action button to initiate a video call from search results.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE]-->
+ <string name="description_search_video_call">Place video call</string>
+</resources>
diff --git a/java/com/android/contacts/common/res/values/styles.xml b/java/com/android/contacts/common/res/values/styles.xml
new file mode 100644
index 000000000..07d4a0225
--- /dev/null
+++ b/java/com/android/contacts/common/res/values/styles.xml
@@ -0,0 +1,97 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <style name="DirectoryHeader">
+ <item name="android:background">@android:color/transparent</item>
+ </style>
+
+ <style name="SectionHeaderStyle" parent="@android:style/TextAppearance.Large">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textColor">@color/section_header_text_color</item>
+ <item name="android:textStyle">bold</item>
+ </style>
+
+ <style name="DirectoryHeaderStyle" parent="@android:style/TextAppearance.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">@color/dialer_secondary_text_color</item>
+ <item name="android:fontFamily">sans-serif-medium</item>
+ </style>
+
+ <!-- TextView style used for headers.
+
+This is similar to ?android:attr/listSeparatorTextView but uses different
+background and text color. See also android:style/Widget.Holo.TextView.ListSeparator
+(which is private, so we cannot specify it as a parent style). -->
+ <style name="ContactListSeparatorTextViewStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <!-- See comments for @dimen/list_section_divider_min_height -->
+ <item name="android:minHeight">@dimen/list_section_divider_min_height</item>
+ <item name="android:background">@drawable/list_section_divider_holo_custom</item>
+ <item name="android:textAppearance">@style/DirectoryHeaderStyle</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingLeft">8dip</item>
+ <item name="android:paddingStart">8dip</item>
+ <item name="android:paddingTop">4dip</item>
+ <item name="android:paddingBottom">4dip</item>
+ <item name="android:ellipsize">end</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="TextAppearanceMedium" parent="@android:style/TextAppearance.Medium">
+ <item name="android:textSize">16sp</item>
+ <item name="android:textColor">#000000</item>
+ </style>
+
+ <style name="TextAppearanceSmall" parent="@android:style/TextAppearance.Small">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textColor">#737373</item>
+ </style>
+
+ <style name="ListViewStyle" parent="@android:style/Widget.Material.Light.ListView">
+ <item name="android:overScrollMode">always</item>
+ </style>
+
+ <style name="BackgroundOnlyTheme" parent="@android:style/Theme.Material.Light">
+ <item name="android:windowBackground">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowNoTitle">true</item>
+ <!-- Activities that use this theme are background activities without obvious displays.
+ However, some also have dialogs. Therefore, it doesn't make sense to set this true.-->
+ <item name="android:windowNoDisplay">false</item>
+ <item name="android:windowIsFloating">true</item>
+ </style>
+
+ <style name="Theme.CallSubjectDialogTheme" parent="@android:style/Theme.Material.Light.Dialog">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">match_parent</item>
+
+ <!-- No backgrounds, titles or window float -->
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowFullscreen">false</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowDrawsSystemBarBackgrounds">false</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowElevation">0dp</item>
+ </style>
+</resources>
diff --git a/java/com/android/contacts/common/testing/InjectedServices.java b/java/com/android/contacts/common/testing/InjectedServices.java
new file mode 100644
index 000000000..5ab5e5feb
--- /dev/null
+++ b/java/com/android/contacts/common/testing/InjectedServices.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.testing;
+
+import android.content.ContentResolver;
+import android.content.SharedPreferences;
+import android.util.ArrayMap;
+import java.util.Map;
+
+/**
+ * A mechanism for providing alternative (mock) services to the application while running tests.
+ * Activities, Services and the Application should check with this class to see if a particular
+ * service has been overridden.
+ */
+public class InjectedServices {
+
+ private ContentResolver mContentResolver;
+ private SharedPreferences mSharedPreferences;
+ private Map<String, Object> mSystemServices;
+
+ public ContentResolver getContentResolver() {
+ return mContentResolver;
+ }
+
+ public void setContentResolver(ContentResolver contentResolver) {
+ this.mContentResolver = contentResolver;
+ }
+
+ public SharedPreferences getSharedPreferences() {
+ return mSharedPreferences;
+ }
+
+ public void setSharedPreferences(SharedPreferences sharedPreferences) {
+ this.mSharedPreferences = sharedPreferences;
+ }
+
+ public void setSystemService(String name, Object service) {
+ if (mSystemServices == null) {
+ mSystemServices = new ArrayMap<>();
+ }
+
+ mSystemServices.put(name, service);
+ }
+
+ public Object getSystemService(String name) {
+ if (mSystemServices != null) {
+ return mSystemServices.get(name);
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/contacts/common/util/AccountFilterUtil.java b/java/com/android/contacts/common/util/AccountFilterUtil.java
new file mode 100644
index 000000000..18743c65e
--- /dev/null
+++ b/java/com/android/contacts/common/util/AccountFilterUtil.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+import android.view.View;
+import android.widget.TextView;
+import com.android.contacts.common.R;
+import com.android.contacts.common.list.ContactListFilter;
+import com.android.contacts.common.list.ContactListFilterController;
+
+/** Utility class for account filter manipulation. */
+public class AccountFilterUtil {
+
+ public static final String EXTRA_CONTACT_LIST_FILTER = "contactListFilter";
+ private static final String TAG = AccountFilterUtil.class.getSimpleName();
+
+ /**
+ * Find TextView with the id "account_filter_header" and set correct text for the account filter
+ * header.
+ *
+ * @param filterContainer View containing TextView with id "account_filter_header"
+ * @return true when header text is set in the call. You may use this for conditionally showing or
+ * hiding this entire view.
+ */
+ public static boolean updateAccountFilterTitleForPeople(
+ View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) {
+ return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, false);
+ }
+
+ /**
+ * Similar to {@link #updateAccountFilterTitleForPeople(View, ContactListFilter, boolean,
+ * boolean)}, but for Phone UI.
+ */
+ public static boolean updateAccountFilterTitleForPhone(
+ View filterContainer, ContactListFilter filter, boolean showTitleForAllAccounts) {
+ return updateAccountFilterTitle(filterContainer, filter, showTitleForAllAccounts, true);
+ }
+
+ private static boolean updateAccountFilterTitle(
+ View filterContainer,
+ ContactListFilter filter,
+ boolean showTitleForAllAccounts,
+ boolean forPhone) {
+ final Context context = filterContainer.getContext();
+ final TextView headerTextView =
+ (TextView) filterContainer.findViewById(R.id.account_filter_header);
+
+ boolean textWasSet = false;
+ if (filter != null) {
+ if (forPhone) {
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+ if (showTitleForAllAccounts) {
+ headerTextView.setText(R.string.list_filter_phones);
+ textWasSet = true;
+ }
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ headerTextView.setText(
+ context.getString(R.string.listAllContactsInAccount, filter.accountName));
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ headerTextView.setText(R.string.listCustomView);
+ textWasSet = true;
+ } else {
+ Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected.");
+ }
+ } else {
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS) {
+ if (showTitleForAllAccounts) {
+ headerTextView.setText(R.string.list_filter_all_accounts);
+ textWasSet = true;
+ }
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+ headerTextView.setText(
+ context.getString(R.string.listAllContactsInAccount, filter.accountName));
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ headerTextView.setText(R.string.listCustomView);
+ textWasSet = true;
+ } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+ headerTextView.setText(R.string.listSingleContact);
+ textWasSet = true;
+ } else {
+ Log.w(TAG, "Filter type \"" + filter.filterType + "\" isn't expected.");
+ }
+ }
+ } else {
+ Log.w(TAG, "Filter is null.");
+ }
+ return textWasSet;
+ }
+
+ /** This will update filter via a given ContactListFilterController. */
+ public static void handleAccountFilterResult(
+ ContactListFilterController filterController, int resultCode, Intent data) {
+ if (resultCode == Activity.RESULT_OK) {
+ final ContactListFilter filter = data.getParcelableExtra(EXTRA_CONTACT_LIST_FILTER);
+ if (filter == null) {
+ return;
+ }
+ if (filter.filterType == ContactListFilter.FILTER_TYPE_CUSTOM) {
+ filterController.selectCustomFilter();
+ } else {
+ filterController.setContactListFilter(filter, true);
+ }
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/BitmapUtil.java b/java/com/android/contacts/common/util/BitmapUtil.java
new file mode 100644
index 000000000..20f916a3f
--- /dev/null
+++ b/java/com/android/contacts/common/util/BitmapUtil.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+
+/** Provides static functions to decode bitmaps at the optimal size */
+public class BitmapUtil {
+
+ private BitmapUtil() {}
+
+ /**
+ * Returns Width or Height of the picture, depending on which size is smaller. Doesn't actually
+ * decode the picture, so it is pretty efficient to run.
+ */
+ public static int getSmallerExtentFromBytes(byte[] bytes) {
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+
+ // don't actually decode the picture, just return its bounds
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+
+ // test what the best sample size is
+ return Math.min(options.outWidth, options.outHeight);
+ }
+
+ /**
+ * Finds the optimal sampleSize for loading the picture
+ *
+ * @param originalSmallerExtent Width or height of the picture, whichever is smaller
+ * @param targetExtent Width or height of the target view, whichever is bigger.
+ * <p>If either one of the parameters is 0 or smaller, no sampling is applied
+ */
+ public static int findOptimalSampleSize(int originalSmallerExtent, int targetExtent) {
+ // If we don't know sizes, we can't do sampling.
+ if (targetExtent < 1) {
+ return 1;
+ }
+ if (originalSmallerExtent < 1) {
+ return 1;
+ }
+
+ // Test what the best sample size is. To do that, we find the sample size that gives us
+ // the best trade-off between resulting image size and memory requirement. We allow
+ // the down-sampled image to be 20% smaller than the target size. That way we can get around
+ // unfortunate cases where e.g. a 720 picture is requested for 362 and not down-sampled at
+ // all. Why 20%? Why not. Prove me wrong.
+ int extent = originalSmallerExtent;
+ int sampleSize = 1;
+ while ((extent >> 1) >= targetExtent * 0.8f) {
+ sampleSize <<= 1;
+ extent >>= 1;
+ }
+
+ return sampleSize;
+ }
+
+ /** Decodes the bitmap with the given sample size */
+ public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
+ final BitmapFactory.Options options;
+ if (sampleSize <= 1) {
+ options = null;
+ } else {
+ options = new BitmapFactory.Options();
+ options.inSampleSize = sampleSize;
+ }
+ return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
+ }
+
+ /**
+ * Retrieves a copy of the specified drawable resource, rotated by a specified angle.
+ *
+ * @param resources The current resources.
+ * @param resourceId The resource ID of the drawable to rotate.
+ * @param angle The angle of rotation.
+ * @return Rotated drawable.
+ */
+ public static Drawable getRotatedDrawable(
+ android.content.res.Resources resources, int resourceId, float angle) {
+
+ // Get the original drawable and make a copy which will be rotated.
+ Bitmap original = BitmapFactory.decodeResource(resources, resourceId);
+ Bitmap rotated =
+ Bitmap.createBitmap(original.getWidth(), original.getHeight(), Bitmap.Config.ARGB_8888);
+
+ // Perform the rotation.
+ Canvas tempCanvas = new Canvas(rotated);
+ tempCanvas.rotate(angle, original.getWidth() / 2, original.getHeight() / 2);
+ tempCanvas.drawBitmap(original, 0, 0, null);
+
+ return new BitmapDrawable(resources, rotated);
+ }
+
+ /**
+ * Given an input bitmap, scales it to the given width/height and makes it round.
+ *
+ * @param input {@link Bitmap} to scale and crop
+ * @param targetWidth desired output width
+ * @param targetHeight desired output height
+ * @return output bitmap scaled to the target width/height and cropped to an oval. The cropping
+ * algorithm will try to fit as much of the input into the output as possible, while
+ * preserving the target width/height ratio.
+ */
+ public static Bitmap getRoundedBitmap(Bitmap input, int targetWidth, int targetHeight) {
+ if (input == null) {
+ return null;
+ }
+ final Bitmap.Config inputConfig = input.getConfig();
+ final Bitmap result =
+ Bitmap.createBitmap(
+ targetWidth, targetHeight, inputConfig != null ? inputConfig : Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(result);
+ final Paint paint = new Paint();
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setAntiAlias(true);
+ final RectF dst = new RectF(0, 0, targetWidth, targetHeight);
+ canvas.drawOval(dst, paint);
+
+ // Specifies that only pixels present in the destination (i.e. the drawn oval) should
+ // be overwritten with pixels from the input bitmap.
+ paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
+
+ final int inputWidth = input.getWidth();
+ final int inputHeight = input.getHeight();
+
+ // Choose the largest scale factor that will fit inside the dimensions of the
+ // input bitmap.
+ final float scaleBy =
+ Math.min((float) inputWidth / targetWidth, (float) inputHeight / targetHeight);
+
+ final int xCropAmountHalved = (int) (scaleBy * targetWidth / 2);
+ final int yCropAmountHalved = (int) (scaleBy * targetHeight / 2);
+
+ final Rect src =
+ new Rect(
+ inputWidth / 2 - xCropAmountHalved,
+ inputHeight / 2 - yCropAmountHalved,
+ inputWidth / 2 + xCropAmountHalved,
+ inputHeight / 2 + yCropAmountHalved);
+
+ canvas.drawBitmap(input, src, dst, paint);
+ return result;
+ }
+}
diff --git a/java/com/android/contacts/common/util/CommonDateUtils.java b/java/com/android/contacts/common/util/CommonDateUtils.java
new file mode 100644
index 000000000..312e691f8
--- /dev/null
+++ b/java/com/android/contacts/common/util/CommonDateUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.util;
+
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/** Common date utilities. */
+public class CommonDateUtils {
+
+ // All the SimpleDateFormats in this class use the UTC timezone
+ public static final SimpleDateFormat NO_YEAR_DATE_FORMAT =
+ new SimpleDateFormat("--MM-dd", Locale.US);
+ public static final SimpleDateFormat FULL_DATE_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd", Locale.US);
+ public static final SimpleDateFormat DATE_AND_TIME_FORMAT =
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+ public static final SimpleDateFormat NO_YEAR_DATE_AND_TIME_FORMAT =
+ new SimpleDateFormat("--MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
+
+ /** Exchange requires 8:00 for birthdays */
+ public static final int DEFAULT_HOUR = 8;
+}
diff --git a/java/com/android/contacts/common/util/Constants.java b/java/com/android/contacts/common/util/Constants.java
new file mode 100644
index 000000000..172e8c348
--- /dev/null
+++ b/java/com/android/contacts/common/util/Constants.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2009 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+public class Constants {
+
+ /**
+ * Log tag for performance measurement. To enable: adb shell setprop log.tag.ContactsPerf VERBOSE
+ */
+ public static final String PERFORMANCE_TAG = "ContactsPerf";
+
+ // Used for lookup URI that contains an encoded JSON string.
+ public static final String LOOKUP_URI_ENCODED = "encoded";
+}
diff --git a/java/com/android/contacts/common/util/ContactDisplayUtils.java b/java/com/android/contacts/common/util/ContactDisplayUtils.java
new file mode 100644
index 000000000..1586784db
--- /dev/null
+++ b/java/com/android/contacts/common/util/ContactDisplayUtils.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.util;
+
+import static android.provider.ContactsContract.CommonDataKinds.Phone;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import android.util.Log;
+import android.util.Patterns;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import java.util.Objects;
+
+/** Methods for handling various contact data labels. */
+public class ContactDisplayUtils {
+
+ public static final int INTERACTION_CALL = 1;
+ public static final int INTERACTION_SMS = 2;
+ private static final String TAG = ContactDisplayUtils.class.getSimpleName();
+
+ /**
+ * Checks if the given data type is a custom type.
+ *
+ * @param type Phone data type.
+ * @return {@literal true} if the type is custom. {@literal false} if not.
+ */
+ public static boolean isCustomPhoneType(Integer type) {
+ return type == Phone.TYPE_CUSTOM || type == Phone.TYPE_ASSISTANT;
+ }
+
+ /**
+ * Gets a display label for a given phone type.
+ *
+ * @param type The type of number.
+ * @param customLabel A custom label to use if the phone is determined to be of custom type
+ * determined by {@link #isCustomPhoneType(Integer))}
+ * @param interactionType whether this is a call or sms. Either {@link #INTERACTION_CALL} or
+ * {@link #INTERACTION_SMS}.
+ * @param context The application context.
+ * @return An appropriate string label
+ */
+ public static CharSequence getLabelForCallOrSms(
+ Integer type, CharSequence customLabel, int interactionType, @NonNull Context context) {
+ Objects.requireNonNull(context);
+
+ if (isCustomPhoneType(type)) {
+ return (customLabel == null) ? "" : customLabel;
+ } else {
+ int resId;
+ if (interactionType == INTERACTION_SMS) {
+ resId = getSmsLabelResourceId(type);
+ } else {
+ resId = getPhoneLabelResourceId(type);
+ if (interactionType != INTERACTION_CALL) {
+ Log.e(
+ TAG,
+ "Un-recognized interaction type: "
+ + interactionType
+ + ". Defaulting to ContactDisplayUtils.INTERACTION_CALL.");
+ }
+ }
+
+ return context.getResources().getText(resId);
+ }
+ }
+
+ /**
+ * Find a label for calling.
+ *
+ * @param type The type of number.
+ * @return An appropriate string label.
+ */
+ public static int getPhoneLabelResourceId(Integer type) {
+ if (type == null) {
+ return R.string.call_other;
+ }
+ switch (type) {
+ case Phone.TYPE_HOME:
+ return R.string.call_home;
+ case Phone.TYPE_MOBILE:
+ return R.string.call_mobile;
+ case Phone.TYPE_WORK:
+ return R.string.call_work;
+ case Phone.TYPE_FAX_WORK:
+ return R.string.call_fax_work;
+ case Phone.TYPE_FAX_HOME:
+ return R.string.call_fax_home;
+ case Phone.TYPE_PAGER:
+ return R.string.call_pager;
+ case Phone.TYPE_OTHER:
+ return R.string.call_other;
+ case Phone.TYPE_CALLBACK:
+ return R.string.call_callback;
+ case Phone.TYPE_CAR:
+ return R.string.call_car;
+ case Phone.TYPE_COMPANY_MAIN:
+ return R.string.call_company_main;
+ case Phone.TYPE_ISDN:
+ return R.string.call_isdn;
+ case Phone.TYPE_MAIN:
+ return R.string.call_main;
+ case Phone.TYPE_OTHER_FAX:
+ return R.string.call_other_fax;
+ case Phone.TYPE_RADIO:
+ return R.string.call_radio;
+ case Phone.TYPE_TELEX:
+ return R.string.call_telex;
+ case Phone.TYPE_TTY_TDD:
+ return R.string.call_tty_tdd;
+ case Phone.TYPE_WORK_MOBILE:
+ return R.string.call_work_mobile;
+ case Phone.TYPE_WORK_PAGER:
+ return R.string.call_work_pager;
+ case Phone.TYPE_ASSISTANT:
+ return R.string.call_assistant;
+ case Phone.TYPE_MMS:
+ return R.string.call_mms;
+ default:
+ return R.string.call_custom;
+ }
+ }
+
+ /**
+ * Find a label for sending an sms.
+ *
+ * @param type The type of number.
+ * @return An appropriate string label.
+ */
+ public static int getSmsLabelResourceId(Integer type) {
+ if (type == null) {
+ return R.string.sms_other;
+ }
+ switch (type) {
+ case Phone.TYPE_HOME:
+ return R.string.sms_home;
+ case Phone.TYPE_MOBILE:
+ return R.string.sms_mobile;
+ case Phone.TYPE_WORK:
+ return R.string.sms_work;
+ case Phone.TYPE_FAX_WORK:
+ return R.string.sms_fax_work;
+ case Phone.TYPE_FAX_HOME:
+ return R.string.sms_fax_home;
+ case Phone.TYPE_PAGER:
+ return R.string.sms_pager;
+ case Phone.TYPE_OTHER:
+ return R.string.sms_other;
+ case Phone.TYPE_CALLBACK:
+ return R.string.sms_callback;
+ case Phone.TYPE_CAR:
+ return R.string.sms_car;
+ case Phone.TYPE_COMPANY_MAIN:
+ return R.string.sms_company_main;
+ case Phone.TYPE_ISDN:
+ return R.string.sms_isdn;
+ case Phone.TYPE_MAIN:
+ return R.string.sms_main;
+ case Phone.TYPE_OTHER_FAX:
+ return R.string.sms_other_fax;
+ case Phone.TYPE_RADIO:
+ return R.string.sms_radio;
+ case Phone.TYPE_TELEX:
+ return R.string.sms_telex;
+ case Phone.TYPE_TTY_TDD:
+ return R.string.sms_tty_tdd;
+ case Phone.TYPE_WORK_MOBILE:
+ return R.string.sms_work_mobile;
+ case Phone.TYPE_WORK_PAGER:
+ return R.string.sms_work_pager;
+ case Phone.TYPE_ASSISTANT:
+ return R.string.sms_assistant;
+ case Phone.TYPE_MMS:
+ return R.string.sms_mms;
+ default:
+ return R.string.sms_custom;
+ }
+ }
+
+ /**
+ * Whether the given text could be a phone number.
+ *
+ * <p>Note this will miss many things that are legitimate phone numbers, for example, phone
+ * numbers with letters.
+ */
+ public static boolean isPossiblePhoneNumber(CharSequence text) {
+ return text != null && Patterns.PHONE.matcher(text.toString()).matches();
+ }
+
+ /**
+ * Returns a Spannable for the given message with a telephone {@link TtsSpan} set for the given
+ * phone number text wherever it is found within the message.
+ */
+ public static Spannable getTelephoneTtsSpannable(
+ @Nullable String message, @Nullable String phoneNumber) {
+ if (message == null) {
+ return null;
+ }
+ final Spannable spannable = new SpannableString(message);
+ int start = phoneNumber == null ? -1 : message.indexOf(phoneNumber);
+ while (start >= 0) {
+ final int end = start + phoneNumber.length();
+ final TtsSpan ttsSpan = PhoneNumberUtilsCompat.createTtsSpan(phoneNumber);
+ spannable.setSpan(
+ ttsSpan,
+ start,
+ end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // this is consistenly done in a misleading way..
+ start = message.indexOf(phoneNumber, end);
+ }
+ return spannable;
+ }
+
+ /**
+ * Retrieves a string from a string template that takes 1 phone number as argument, span the
+ * number with a telephone {@link TtsSpan}, and return the spanned string.
+ *
+ * @param resources to retrieve the string from
+ * @param stringId ID of the string
+ * @param number to pass in the template
+ * @return CharSequence with the phone number wrapped in a TtsSpan
+ */
+ public static CharSequence getTtsSpannedPhoneNumber(
+ Resources resources, int stringId, String number) {
+ String msg = resources.getString(stringId, number);
+ return ContactDisplayUtils.getTelephoneTtsSpannable(msg, number);
+ }
+
+ /**
+ * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}.
+ * Defaults to the name that is non-null.
+ *
+ * @param namePrimary the primary name.
+ * @param nameAlternative the alternative name.
+ * @param contactsPreferences the ContactsPreferences used to determine the preferred display
+ * name.
+ * @return namePrimary or nameAlternative depending on the value of displayOrderPreference.
+ */
+ public static String getPreferredDisplayName(
+ String namePrimary,
+ String nameAlternative,
+ @Nullable ContactsPreferences contactsPreferences) {
+ if (contactsPreferences == null) {
+ return namePrimary != null ? namePrimary : nameAlternative;
+ }
+ if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) {
+ return namePrimary;
+ }
+
+ if (contactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_ALTERNATIVE
+ && !TextUtils.isEmpty(nameAlternative)) {
+ return nameAlternative;
+ }
+
+ return namePrimary;
+ }
+
+ /**
+ * Returns either namePrimary or nameAlternative based on the {@link ContactsPreferences}.
+ * Defaults to the name that is non-null.
+ *
+ * @param namePrimary the primary name.
+ * @param nameAlternative the alternative name.
+ * @param contactsPreferences the ContactsPreferences used to determine the preferred sort order.
+ * @return namePrimary or nameAlternative depending on the value of displayOrderPreference.
+ */
+ public static String getPreferredSortName(
+ String namePrimary,
+ String nameAlternative,
+ @Nullable ContactsPreferences contactsPreferences) {
+ if (contactsPreferences == null) {
+ return namePrimary != null ? namePrimary : nameAlternative;
+ }
+
+ if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) {
+ return namePrimary;
+ }
+
+ if (contactsPreferences.getSortOrder() == ContactsPreferences.SORT_ORDER_ALTERNATIVE
+ && !TextUtils.isEmpty(nameAlternative)) {
+ return nameAlternative;
+ }
+
+ return namePrimary;
+ }
+}
diff --git a/java/com/android/contacts/common/util/ContactListViewUtils.java b/java/com/android/contacts/common/util/ContactListViewUtils.java
new file mode 100644
index 000000000..278c27d5c
--- /dev/null
+++ b/java/com/android/contacts/common/util/ContactListViewUtils.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.view.View;
+import android.widget.ListView;
+import com.android.contacts.common.R;
+import com.android.dialer.util.ViewUtil;
+
+/** Utilities for configuring ListViews with a card background. */
+public class ContactListViewUtils {
+
+ // These two constants will help add more padding for the text inside the card.
+ private static final double TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO = 1.1;
+
+ private static void addPaddingToView(
+ ListView listView, int parentWidth, int listSpaceWeight, int listViewWeight) {
+ if (listSpaceWeight > 0 && listViewWeight > 0) {
+ double paddingPercent =
+ (double) listSpaceWeight / (double) (listSpaceWeight * 2 + listViewWeight);
+ listView.setPadding(
+ (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO),
+ listView.getPaddingTop(),
+ (int) (parentWidth * paddingPercent * TEXT_LEFT_PADDING_TO_CARD_PADDING_RATIO),
+ listView.getPaddingBottom());
+ // The EdgeEffect and ScrollBar need to span to the edge of the ListView's padding.
+ listView.setClipToPadding(false);
+ listView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+ }
+ }
+
+ /**
+ * Add padding to {@param listView} if this configuration has set both space weight and view
+ * weight on the layout. Use this util method instead of defining the padding in the layout file
+ * so that the {@param listView}'s padding can be set proportional to the card padding.
+ *
+ * @param listView ListView that we add padding to
+ * @param rootLayout layout that contains ListView and R.id.list_card
+ */
+ public static void applyCardPaddingToView(
+ Resources resources, final ListView listView, final View rootLayout) {
+ // Set a padding on the list view so it appears in the center of the card
+ // in the layout if required.
+ final int listSpaceWeight = resources.getInteger(R.integer.contact_list_space_layout_weight);
+ final int listViewWeight = resources.getInteger(R.integer.contact_list_card_layout_weight);
+ if (listSpaceWeight > 0 && listViewWeight > 0) {
+ rootLayout.setBackgroundResource(0);
+ // Set the card view visible
+ View mCardView = rootLayout.findViewById(R.id.list_card);
+ if (mCardView == null) {
+ throw new RuntimeException(
+ "Your content must have a list card view who can be turned visible "
+ + "whenever it is necessary.");
+ }
+ mCardView.setVisibility(View.VISIBLE);
+
+ // Add extra padding to the list view to make them appear in the center of the card.
+ // In order to avoid jumping, we skip drawing the next frame of the ListView.
+ ViewUtil.doOnPreDraw(
+ listView,
+ false,
+ new Runnable() {
+ @Override
+ public void run() {
+ // Use the rootLayout.getWidth() instead of listView.getWidth() since
+ // we sometimes hide the listView until we finish loading data. This would
+ // result in incorrect padding.
+ ContactListViewUtils.addPaddingToView(
+ listView, rootLayout.getWidth(), listSpaceWeight, listViewWeight);
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/ContactLoaderUtils.java b/java/com/android/contacts/common/util/ContactLoaderUtils.java
new file mode 100644
index 000000000..e30971721
--- /dev/null
+++ b/java/com/android/contacts/common/util/ContactLoaderUtils.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.net.Uri;
+import android.provider.Contacts;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.RawContacts;
+
+/** Utility methods for the {@link ContactLoader}. */
+public final class ContactLoaderUtils {
+
+ /** Static helper, not instantiable. */
+ private ContactLoaderUtils() {}
+
+ /**
+ * Transforms the given Uri and returns a Lookup-Uri that represents the contact. For legacy
+ * contacts, a raw-contact lookup is performed. An {@link IllegalArgumentException} can be thrown
+ * if the URI is null or the authority is not recognized.
+ *
+ * <p>Do not call from the UI thread.
+ */
+ @SuppressWarnings("deprecation")
+ public static Uri ensureIsContactUri(final ContentResolver resolver, final Uri uri)
+ throws IllegalArgumentException {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri must not be null");
+ }
+
+ final String authority = uri.getAuthority();
+
+ // Current Style Uri?
+ if (ContactsContract.AUTHORITY.equals(authority)) {
+ final String type = resolver.getType(uri);
+ // Contact-Uri? Good, return it
+ if (ContactsContract.Contacts.CONTENT_ITEM_TYPE.equals(type)) {
+ return uri;
+ }
+
+ // RawContact-Uri? Transform it to ContactUri
+ if (RawContacts.CONTENT_ITEM_TYPE.equals(type)) {
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(
+ resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ // Anything else? We don't know what this is
+ throw new IllegalArgumentException("uri format is unknown");
+ }
+
+ // Legacy Style? Convert to RawContact
+ final String OBSOLETE_AUTHORITY = Contacts.AUTHORITY;
+ if (OBSOLETE_AUTHORITY.equals(authority)) {
+ // Legacy Format. Convert to RawContact-Uri and then lookup the contact
+ final long rawContactId = ContentUris.parseId(uri);
+ return RawContacts.getContactLookupUri(
+ resolver, ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId));
+ }
+
+ throw new IllegalArgumentException("uri authority is unknown");
+ }
+}
diff --git a/java/com/android/contacts/common/util/DateUtils.java b/java/com/android/contacts/common/util/DateUtils.java
new file mode 100644
index 000000000..1935d727a
--- /dev/null
+++ b/java/com/android/contacts/common/util/DateUtils.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.text.format.DateFormat;
+import android.text.format.Time;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/** Utility methods for processing dates. */
+public class DateUtils {
+
+ public static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone("UTC");
+
+ /**
+ * When parsing a date without a year, the system assumes 1970, which wasn't a leap-year. Let's
+ * add a one-off hack for that day of the year
+ */
+ public static final String NO_YEAR_DATE_FEB29TH = "--02-29";
+
+ // Variations of ISO 8601 date format. Do not change the order - it does affect the
+ // result in ambiguous cases.
+ private static final SimpleDateFormat[] DATE_FORMATS = {
+ CommonDateUtils.FULL_DATE_FORMAT,
+ CommonDateUtils.DATE_AND_TIME_FORMAT,
+ new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmssSSS'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US),
+ new SimpleDateFormat("yyyyMMdd'T'HHmm'Z'", Locale.US),
+ };
+
+ static {
+ for (SimpleDateFormat format : DATE_FORMATS) {
+ format.setLenient(true);
+ format.setTimeZone(UTC_TIMEZONE);
+ }
+ CommonDateUtils.NO_YEAR_DATE_FORMAT.setTimeZone(UTC_TIMEZONE);
+ }
+
+ /**
+ * Parses the supplied string to see if it looks like a date.
+ *
+ * @param string The string representation of the provided date
+ * @param mustContainYear If true, the string is parsed as a date containing a year. If false, the
+ * string is parsed into a valid date even if the year field is missing.
+ * @return A Calendar object corresponding to the date if the string is successfully parsed. If
+ * not, null is returned.
+ */
+ public static Calendar parseDate(String string, boolean mustContainYear) {
+ ParsePosition parsePosition = new ParsePosition(0);
+ Date date;
+ if (!mustContainYear) {
+ final boolean noYearParsed;
+ // Unfortunately, we can't parse Feb 29th correctly, so let's handle this day seperately
+ if (NO_YEAR_DATE_FEB29TH.equals(string)) {
+ return getUtcDate(0, Calendar.FEBRUARY, 29);
+ } else {
+ synchronized (CommonDateUtils.NO_YEAR_DATE_FORMAT) {
+ date = CommonDateUtils.NO_YEAR_DATE_FORMAT.parse(string, parsePosition);
+ }
+ noYearParsed = parsePosition.getIndex() == string.length();
+ }
+
+ if (noYearParsed) {
+ return getUtcDate(date, true);
+ }
+ }
+ for (int i = 0; i < DATE_FORMATS.length; i++) {
+ SimpleDateFormat f = DATE_FORMATS[i];
+ synchronized (f) {
+ parsePosition.setIndex(0);
+ date = f.parse(string, parsePosition);
+ if (parsePosition.getIndex() == string.length()) {
+ return getUtcDate(date, false);
+ }
+ }
+ }
+ return null;
+ }
+
+ private static final Calendar getUtcDate(Date date, boolean noYear) {
+ final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+ calendar.setTime(date);
+ if (noYear) {
+ calendar.set(Calendar.YEAR, 0);
+ }
+ return calendar;
+ }
+
+ private static final Calendar getUtcDate(int year, int month, int dayOfMonth) {
+ final Calendar calendar = Calendar.getInstance(UTC_TIMEZONE, Locale.US);
+ calendar.clear();
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.MONTH, month);
+ calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+ return calendar;
+ }
+
+ public static boolean isYearSet(Calendar cal) {
+ // use the Calendar.YEAR field to track whether or not the year is set instead of
+ // Calendar.isSet() because doing Calendar.get() causes Calendar.isSet() to become
+ // true irregardless of what the previous value was
+ return cal.get(Calendar.YEAR) > 1;
+ }
+
+ /**
+ * Same as {@link #formatDate(Context context, String string, boolean longForm)}, with longForm
+ * set to {@code true} by default.
+ *
+ * @param context Valid context
+ * @param string String representation of a date to parse
+ * @return Returns the same date in a cleaned up format. If the supplied string does not look like
+ * a date, return it unchanged.
+ */
+ public static String formatDate(Context context, String string) {
+ return formatDate(context, string, true);
+ }
+
+ /**
+ * Parses the supplied string to see if it looks like a date.
+ *
+ * @param context Valid context
+ * @param string String representation of a date to parse
+ * @param longForm If true, return the date formatted into its long string representation. If
+ * false, return the date formatted using its short form representation (i.e. 12/11/2012)
+ * @return Returns the same date in a cleaned up format. If the supplied string does not look like
+ * a date, return it unchanged.
+ */
+ public static String formatDate(Context context, String string, boolean longForm) {
+ if (string == null) {
+ return null;
+ }
+
+ string = string.trim();
+ if (string.length() == 0) {
+ return string;
+ }
+ final Calendar cal = parseDate(string, false);
+
+ // we weren't able to parse the string successfully so just return it unchanged
+ if (cal == null) {
+ return string;
+ }
+
+ final boolean isYearSet = isYearSet(cal);
+ final java.text.DateFormat outFormat;
+ if (!isYearSet) {
+ outFormat = getLocalizedDateFormatWithoutYear(context);
+ } else {
+ outFormat =
+ longForm ? DateFormat.getLongDateFormat(context) : DateFormat.getDateFormat(context);
+ }
+ synchronized (outFormat) {
+ outFormat.setTimeZone(UTC_TIMEZONE);
+ return outFormat.format(cal.getTime());
+ }
+ }
+
+ public static boolean isMonthBeforeDay(Context context) {
+ char[] dateFormatOrder = DateFormat.getDateFormatOrder(context);
+ for (int i = 0; i < dateFormatOrder.length; i++) {
+ if (dateFormatOrder[i] == 'd') {
+ return false;
+ }
+ if (dateFormatOrder[i] == 'M') {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns a SimpleDateFormat object without the year fields by using a regular expression to
+ * eliminate the year in the string pattern. In the rare occurence that the resulting pattern
+ * cannot be reconverted into a SimpleDateFormat, it uses the provided context to determine
+ * whether the month field should be displayed before the day field, and returns either "MMMM dd"
+ * or "dd MMMM" converted into a SimpleDateFormat.
+ */
+ public static java.text.DateFormat getLocalizedDateFormatWithoutYear(Context context) {
+ final String pattern =
+ ((SimpleDateFormat) SimpleDateFormat.getDateInstance(java.text.DateFormat.LONG))
+ .toPattern();
+ // Determine the correct regex pattern for year.
+ // Special case handling for Spanish locale by checking for "de"
+ final String yearPattern =
+ pattern.contains("de") ? "[^Mm]*[Yy]+[^Mm]*" : "[^DdMm]*[Yy]+[^DdMm]*";
+ try {
+ // Eliminate the substring in pattern that matches the format for that of year
+ return new SimpleDateFormat(pattern.replaceAll(yearPattern, ""));
+ } catch (IllegalArgumentException e) {
+ return new SimpleDateFormat(DateUtils.isMonthBeforeDay(context) ? "MMMM dd" : "dd MMMM");
+ }
+ }
+
+ /**
+ * Given a calendar (possibly containing only a day of the year), returns the earliest possible
+ * anniversary of the date that is equal to or after the current point in time if the date does
+ * not contain a year, or the date converted to the local time zone (if the date contains a year.
+ *
+ * @param target The date we wish to convert(in the UTC time zone).
+ * @return If date does not contain a year (year < 1900), returns the next earliest anniversary
+ * that is after the current point in time (in the local time zone). Otherwise, returns the
+ * adjusted Date in the local time zone.
+ */
+ public static Date getNextAnnualDate(Calendar target) {
+ final Calendar today = Calendar.getInstance();
+ today.setTime(new Date());
+
+ // Round the current time to the exact start of today so that when we compare
+ // today against the target date, both dates are set to exactly 0000H.
+ today.set(Calendar.HOUR_OF_DAY, 0);
+ today.set(Calendar.MINUTE, 0);
+ today.set(Calendar.SECOND, 0);
+ today.set(Calendar.MILLISECOND, 0);
+
+ final boolean isYearSet = isYearSet(target);
+ final int targetYear = target.get(Calendar.YEAR);
+ final int targetMonth = target.get(Calendar.MONTH);
+ final int targetDay = target.get(Calendar.DAY_OF_MONTH);
+ final boolean isFeb29 = (targetMonth == Calendar.FEBRUARY && targetDay == 29);
+ final GregorianCalendar anniversary = new GregorianCalendar();
+ // Convert from the UTC date to the local date. Set the year to today's year if the
+ // there is no provided year (targetYear < 1900)
+ anniversary.set(!isYearSet ? today.get(Calendar.YEAR) : targetYear, targetMonth, targetDay);
+ // If the anniversary's date is before the start of today and there is no year set,
+ // increment the year by 1 so that the returned date is always equal to or greater than
+ // today. If the day is a leap year, keep going until we get the next leap year anniversary
+ // Otherwise if there is already a year set, simply return the exact date.
+ if (!isYearSet) {
+ int anniversaryYear = today.get(Calendar.YEAR);
+ if (anniversary.before(today) || (isFeb29 && !anniversary.isLeapYear(anniversaryYear))) {
+ // If the target date is not Feb 29, then set the anniversary to the next year.
+ // Otherwise, keep going until we find the next leap year (this is not guaranteed
+ // to be in 4 years time).
+ do {
+ anniversaryYear += 1;
+ } while (isFeb29 && !anniversary.isLeapYear(anniversaryYear));
+ anniversary.set(anniversaryYear, targetMonth, targetDay);
+ }
+ }
+ return anniversary.getTime();
+ }
+
+ /**
+ * Determine the difference, in days between two dates. Uses similar logic as the {@link
+ * android.text.format.DateUtils.getRelativeTimeSpanString} method.
+ *
+ * @param time Instance of time object to use for calculations.
+ * @param date1 First date to check.
+ * @param date2 Second date to check.
+ * @return The absolute difference in days between the two dates.
+ */
+ public static int getDayDifference(Time time, long date1, long date2) {
+ time.set(date1);
+ int startDay = Time.getJulianDay(date1, time.gmtoff);
+
+ time.set(date2);
+ int currentDay = Time.getJulianDay(date2, time.gmtoff);
+
+ return Math.abs(currentDay - startDay);
+ }
+}
diff --git a/java/com/android/contacts/common/util/FabUtil.java b/java/com/android/contacts/common/util/FabUtil.java
new file mode 100644
index 000000000..b1bb2e653
--- /dev/null
+++ b/java/com/android/contacts/common/util/FabUtil.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.graphics.Outline;
+import android.view.View;
+import android.view.ViewOutlineProvider;
+import android.widget.ListView;
+import com.android.contacts.common.R;
+import com.android.dialer.compat.CompatUtils;
+
+/** Provides static functions to work with views */
+public class FabUtil {
+
+ private static final ViewOutlineProvider OVAL_OUTLINE_PROVIDER =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ outline.setOval(0, 0, view.getWidth(), view.getHeight());
+ }
+ };
+
+ private FabUtil() {}
+
+ /**
+ * Configures the floating action button, clipping it to a circle and setting its translation z
+ *
+ * @param fabView the float action button's view
+ * @param res the resources file
+ */
+ public static void setupFloatingActionButton(View fabView, Resources res) {
+ if (CompatUtils.isLollipopCompatible()) {
+ fabView.setOutlineProvider(OVAL_OUTLINE_PROVIDER);
+ fabView.setTranslationZ(
+ res.getDimensionPixelSize(R.dimen.floating_action_button_translation_z));
+ }
+ }
+
+ /**
+ * Adds padding to the bottom of the given {@link ListView} so that the floating action button
+ * does not obscure any content.
+ *
+ * @param listView to add the padding to
+ * @param res valid resources object
+ */
+ public static void addBottomPaddingToListViewForFab(ListView listView, Resources res) {
+ final int fabPadding =
+ res.getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding);
+ listView.setPaddingRelative(
+ listView.getPaddingStart(),
+ listView.getPaddingTop(),
+ listView.getPaddingEnd(),
+ listView.getPaddingBottom() + fabPadding);
+ listView.setClipToPadding(false);
+ }
+}
diff --git a/java/com/android/contacts/common/util/MaterialColorMapUtils.java b/java/com/android/contacts/common/util/MaterialColorMapUtils.java
new file mode 100644
index 000000000..a2d9847ec
--- /dev/null
+++ b/java/com/android/contacts/common/util/MaterialColorMapUtils.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Trace;
+import com.android.contacts.common.R;
+
+public class MaterialColorMapUtils {
+
+ private final TypedArray sPrimaryColors;
+ private final TypedArray sSecondaryColors;
+
+ public MaterialColorMapUtils(Resources resources) {
+ sPrimaryColors =
+ resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors);
+ sSecondaryColors =
+ resources.obtainTypedArray(com.android.contacts.common.R.array.letter_tile_colors_dark);
+ }
+
+ public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) {
+ final int primaryColor = resources.getColor(R.color.quickcontact_default_photo_tint_color);
+ final int secondaryColor =
+ resources.getColor(R.color.quickcontact_default_photo_tint_color_dark);
+ return new MaterialPalette(primaryColor, secondaryColor);
+ }
+
+ /**
+ * Returns the hue component of a color int.
+ *
+ * @return A value between 0.0f and 1.0f
+ */
+ public static float hue(int color) {
+ int r = (color >> 16) & 0xFF;
+ int g = (color >> 8) & 0xFF;
+ int b = color & 0xFF;
+
+ int V = Math.max(b, Math.max(r, g));
+ int temp = Math.min(b, Math.min(r, g));
+
+ float H;
+
+ if (V == temp) {
+ H = 0;
+ } else {
+ final float vtemp = V - temp;
+ final float cr = (V - r) / vtemp;
+ final float cg = (V - g) / vtemp;
+ final float cb = (V - b) / vtemp;
+
+ if (r == V) {
+ H = cb - cg;
+ } else if (g == V) {
+ H = 2 + cr - cb;
+ } else {
+ H = 4 + cg - cr;
+ }
+
+ H /= 6.f;
+ if (H < 0) {
+ H++;
+ }
+ }
+
+ return H;
+ }
+
+ /**
+ * Return primary and secondary colors from the Material color palette that are similar to {@param
+ * color}.
+ */
+ public MaterialPalette calculatePrimaryAndSecondaryColor(int color) {
+ Trace.beginSection("calculatePrimaryAndSecondaryColor");
+
+ final float colorHue = hue(color);
+ float minimumDistance = Float.MAX_VALUE;
+ int indexBestMatch = 0;
+ for (int i = 0; i < sPrimaryColors.length(); i++) {
+ final int primaryColor = sPrimaryColors.getColor(i, 0);
+ final float comparedHue = hue(primaryColor);
+ // No need to be perceptually accurate when calculating color distances since
+ // we are only mapping to 15 colors. Being slightly inaccurate isn't going to change
+ // the mapping very often.
+ final float distance = Math.abs(comparedHue - colorHue);
+ if (distance < minimumDistance) {
+ minimumDistance = distance;
+ indexBestMatch = i;
+ }
+ }
+
+ Trace.endSection();
+ return new MaterialPalette(
+ sPrimaryColors.getColor(indexBestMatch, 0), sSecondaryColors.getColor(indexBestMatch, 0));
+ }
+
+ public static class MaterialPalette implements Parcelable {
+
+ public static final Creator<MaterialPalette> CREATOR =
+ new Creator<MaterialPalette>() {
+ @Override
+ public MaterialPalette createFromParcel(Parcel in) {
+ return new MaterialPalette(in);
+ }
+
+ @Override
+ public MaterialPalette[] newArray(int size) {
+ return new MaterialPalette[size];
+ }
+ };
+ public final int mPrimaryColor;
+ public final int mSecondaryColor;
+
+ public MaterialPalette(int primaryColor, int secondaryColor) {
+ mPrimaryColor = primaryColor;
+ mSecondaryColor = secondaryColor;
+ }
+
+ private MaterialPalette(Parcel in) {
+ mPrimaryColor = in.readInt();
+ mSecondaryColor = in.readInt();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ MaterialPalette other = (MaterialPalette) obj;
+ if (mPrimaryColor != other.mPrimaryColor) {
+ return false;
+ }
+ if (mSecondaryColor != other.mSecondaryColor) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mPrimaryColor;
+ result = prime * result + mSecondaryColor;
+ return result;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mPrimaryColor);
+ dest.writeInt(mSecondaryColor);
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/NameConverter.java b/java/com/android/contacts/common/util/NameConverter.java
new file mode 100644
index 000000000..ae3275d14
--- /dev/null
+++ b/java/com/android/contacts/common/util/NameConverter.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.util;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.Uri.Builder;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.text.TextUtils;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Utility class for converting between a display name and structured name (and vice-versa), via
+ * calls to the contact provider.
+ */
+public class NameConverter {
+
+ /** The array of fields that comprise a structured name. */
+ public static final String[] STRUCTURED_NAME_FIELDS =
+ new String[] {
+ StructuredName.PREFIX,
+ StructuredName.GIVEN_NAME,
+ StructuredName.MIDDLE_NAME,
+ StructuredName.FAMILY_NAME,
+ StructuredName.SUFFIX
+ };
+
+ /**
+ * Converts the given structured name (provided as a map from {@link StructuredName} fields to
+ * corresponding values) into a display name string.
+ *
+ * <p>Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread. See ContactsProvider2.completeName()
+ * for the underlying method call.
+ *
+ * @param context Activity context.
+ * @param structuredName The structured name map to convert.
+ * @return The display name computed from the structured name map.
+ */
+ public static String structuredNameToDisplayName(
+ Context context, Map<String, String> structuredName) {
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+ for (String key : STRUCTURED_NAME_FIELDS) {
+ if (structuredName.containsKey(key)) {
+ appendQueryParameter(builder, key, structuredName.get(key));
+ }
+ }
+ return fetchDisplayName(context, builder.build());
+ }
+
+ /**
+ * Converts the given structured name (provided as ContentValues) into a display name string.
+ *
+ * @param context Activity context.
+ * @param values The content values containing values comprising the structured name.
+ */
+ public static String structuredNameToDisplayName(Context context, ContentValues values) {
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+ for (String key : STRUCTURED_NAME_FIELDS) {
+ if (values.containsKey(key)) {
+ appendQueryParameter(builder, key, values.getAsString(key));
+ }
+ }
+ return fetchDisplayName(context, builder.build());
+ }
+
+ /** Helper method for fetching the display name via the given URI. */
+ private static String fetchDisplayName(Context context, Uri uri) {
+ String displayName = null;
+ Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ uri,
+ new String[] {
+ StructuredName.DISPLAY_NAME,
+ },
+ null,
+ null,
+ null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ displayName = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return displayName;
+ }
+
+ /**
+ * Converts the given display name string into a structured name (as a map from {@link
+ * StructuredName} fields to corresponding values).
+ *
+ * <p>Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread.
+ *
+ * @param context Activity context.
+ * @param displayName The display name to convert.
+ * @return The structured name map computed from the display name.
+ */
+ public static Map<String, String> displayNameToStructuredName(
+ Context context, String displayName) {
+ Map<String, String> structuredName = new TreeMap<String, String>();
+ Builder builder = ContactsContract.AUTHORITY_URI.buildUpon().appendPath("complete_name");
+
+ appendQueryParameter(builder, StructuredName.DISPLAY_NAME, displayName);
+ Cursor cursor =
+ context
+ .getContentResolver()
+ .query(builder.build(), STRUCTURED_NAME_FIELDS, null, null, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ for (int i = 0; i < STRUCTURED_NAME_FIELDS.length; i++) {
+ structuredName.put(STRUCTURED_NAME_FIELDS[i], cursor.getString(i));
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return structuredName;
+ }
+
+ /**
+ * Converts the given display name string into a structured name (inserting the structured values
+ * into a new or existing ContentValues object).
+ *
+ * <p>Note that this operates via a call back to the ContactProvider, but it does not access the
+ * database, so it should be safe to call from the UI thread.
+ *
+ * @param context Activity context.
+ * @param displayName The display name to convert.
+ * @param contentValues The content values object to place the structured name values into. If
+ * null, a new one will be created and returned.
+ * @return The ContentValues object containing the structured name fields derived from the display
+ * name.
+ */
+ public static ContentValues displayNameToStructuredName(
+ Context context, String displayName, ContentValues contentValues) {
+ if (contentValues == null) {
+ contentValues = new ContentValues();
+ }
+ Map<String, String> mapValues = displayNameToStructuredName(context, displayName);
+ for (String key : mapValues.keySet()) {
+ contentValues.put(key, mapValues.get(key));
+ }
+ return contentValues;
+ }
+
+ private static void appendQueryParameter(Builder builder, String field, String value) {
+ if (!TextUtils.isEmpty(value)) {
+ builder.appendQueryParameter(field, value);
+ }
+ }
+
+ /**
+ * Parses phonetic name and returns parsed data (family, middle, given) as ContentValues. Parsed
+ * data should be {@link StructuredName#PHONETIC_FAMILY_NAME}, {@link
+ * StructuredName#PHONETIC_MIDDLE_NAME}, and {@link StructuredName#PHONETIC_GIVEN_NAME}. If this
+ * method cannot parse given phoneticName, null values will be stored.
+ *
+ * @param phoneticName Phonetic name to be parsed
+ * @param values ContentValues to be used for storing data. If null, new instance will be created.
+ * @return ContentValues with parsed data. Those data can be null.
+ */
+ public static StructuredNameDataItem parsePhoneticName(
+ String phoneticName, StructuredNameDataItem item) {
+ String family = null;
+ String middle = null;
+ String given = null;
+
+ if (!TextUtils.isEmpty(phoneticName)) {
+ String[] strings = phoneticName.split(" ", 3);
+ switch (strings.length) {
+ case 1:
+ family = strings[0];
+ break;
+ case 2:
+ family = strings[0];
+ given = strings[1];
+ break;
+ case 3:
+ family = strings[0];
+ middle = strings[1];
+ given = strings[2];
+ break;
+ }
+ }
+
+ if (item == null) {
+ item = new StructuredNameDataItem();
+ }
+ item.setPhoneticFamilyName(family);
+ item.setPhoneticMiddleName(middle);
+ item.setPhoneticGivenName(given);
+ return item;
+ }
+
+ /** Constructs and returns a phonetic full name from given parts. */
+ public static String buildPhoneticName(String family, String middle, String given) {
+ if (!TextUtils.isEmpty(family) || !TextUtils.isEmpty(middle) || !TextUtils.isEmpty(given)) {
+ StringBuilder sb = new StringBuilder();
+ if (!TextUtils.isEmpty(family)) {
+ sb.append(family.trim()).append(' ');
+ }
+ if (!TextUtils.isEmpty(middle)) {
+ sb.append(middle.trim()).append(' ');
+ }
+ if (!TextUtils.isEmpty(given)) {
+ sb.append(given.trim()).append(' ');
+ }
+ sb.setLength(sb.length() - 1); // Yank the last space
+ return sb.toString();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/SearchUtil.java b/java/com/android/contacts/common/util/SearchUtil.java
new file mode 100644
index 000000000..314d565b2
--- /dev/null
+++ b/java/com/android/contacts/common/util/SearchUtil.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.support.annotation.VisibleForTesting;
+
+/** Methods related to search. */
+public class SearchUtil {
+
+ /**
+ * Given a string with lines delimited with '\n', finds the matching line to the given substring.
+ *
+ * @param contents The string to search.
+ * @param substring The substring to search for.
+ * @return A MatchedLine object containing the matching line and the startIndex of the substring
+ * match within that line.
+ */
+ public static MatchedLine findMatchingLine(String contents, String substring) {
+ final MatchedLine matched = new MatchedLine();
+
+ // Snippet may contain multiple lines separated by "\n".
+ // Locate the lines of the content that contain the substring.
+ final int index = SearchUtil.contains(contents, substring);
+ if (index != -1) {
+ // Match found. Find the corresponding line.
+ int start = index - 1;
+ while (start > -1) {
+ if (contents.charAt(start) == '\n') {
+ break;
+ }
+ start--;
+ }
+ int end = index + 1;
+ while (end < contents.length()) {
+ if (contents.charAt(end) == '\n') {
+ break;
+ }
+ end++;
+ }
+ matched.line = contents.substring(start + 1, end);
+ matched.startIndex = index - (start + 1);
+ }
+ return matched;
+ }
+
+ /**
+ * Similar to String.contains() with two main differences:
+ *
+ * <p>1) Only searches token prefixes. A token is defined as any combination of letters or
+ * numbers.
+ *
+ * <p>2) Returns the starting index where the substring is found.
+ *
+ * @param value The string to search.
+ * @param substring The substring to look for.
+ * @return The starting index where the substring is found. {@literal -1} if substring is not
+ * found in value.
+ */
+ @VisibleForTesting
+ static int contains(String value, String substring) {
+ if (value.length() < substring.length()) {
+ return -1;
+ }
+
+ // i18n support
+ // Generate the code points for the substring once.
+ // There will be a maximum of substring.length code points. But may be fewer.
+ // Since the array length is not an accurate size, we need to keep a separate variable.
+ final int[] substringCodePoints = new int[substring.length()];
+ int substringLength = 0; // may not equal substring.length()!!
+ for (int i = 0; i < substring.length(); ) {
+ final int codePoint = Character.codePointAt(substring, i);
+ substringCodePoints[substringLength] = codePoint;
+ substringLength++;
+ i += Character.charCount(codePoint);
+ }
+
+ for (int i = 0; i < value.length(); i = findNextTokenStart(value, i)) {
+ int numMatch = 0;
+ for (int j = i; j < value.length() && numMatch < substringLength; ++numMatch) {
+ int valueCp = Character.toLowerCase(value.codePointAt(j));
+ int substringCp = substringCodePoints[numMatch];
+ if (valueCp != substringCp) {
+ break;
+ }
+ j += Character.charCount(valueCp);
+ }
+ if (numMatch == substringLength) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Find the start of the next token. A token is composed of letters and numbers. Any other
+ * character are considered delimiters.
+ *
+ * @param line The string to search for the next token.
+ * @param startIndex The index to start searching. 0 based indexing.
+ * @return The index for the start of the next token. line.length() if next token not found.
+ */
+ @VisibleForTesting
+ static int findNextTokenStart(String line, int startIndex) {
+ int index = startIndex;
+
+ // If already in token, eat remainder of token.
+ while (index <= line.length()) {
+ if (index == line.length()) {
+ // No more tokens.
+ return index;
+ }
+ final int codePoint = line.codePointAt(index);
+ if (!Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ index += Character.charCount(codePoint);
+ }
+
+ // Out of token, eat all consecutive delimiters.
+ while (index <= line.length()) {
+ if (index == line.length()) {
+ return index;
+ }
+ final int codePoint = line.codePointAt(index);
+ if (Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ index += Character.charCount(codePoint);
+ }
+
+ return index;
+ }
+
+ /**
+ * Anything other than letter and numbers are considered delimiters. Remove start and end
+ * delimiters since they are not relevant to search.
+ *
+ * @param query The query string to clean.
+ * @return The cleaned query. Empty string if all characters are cleaned out.
+ */
+ public static String cleanStartAndEndOfSearchQuery(String query) {
+ int start = 0;
+ while (start < query.length()) {
+ int codePoint = query.codePointAt(start);
+ if (Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ start += Character.charCount(codePoint);
+ }
+
+ if (start == query.length()) {
+ // All characters are delimiters.
+ return "";
+ }
+
+ int end = query.length() - 1;
+ while (end > -1) {
+ if (Character.isLowSurrogate(query.charAt(end))) {
+ // Assume valid i18n string. There should be a matching high surrogate before it.
+ end--;
+ }
+ int codePoint = query.codePointAt(end);
+ if (Character.isLetterOrDigit(codePoint)) {
+ break;
+ }
+ end--;
+ }
+
+ // end is a letter or digit.
+ return query.substring(start, end + 1);
+ }
+
+ public static class MatchedLine {
+
+ public int startIndex = -1;
+ public String line;
+
+ @Override
+ public String toString() {
+ return "MatchedLine{" + "line='" + line + '\'' + ", startIndex=" + startIndex + '}';
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/StopWatch.java b/java/com/android/contacts/common/util/StopWatch.java
new file mode 100644
index 000000000..b944b9867
--- /dev/null
+++ b/java/com/android/contacts/common/util/StopWatch.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.util.Log;
+import java.util.ArrayList;
+
+/** A {@link StopWatch} records start, laps and stop, and print them to logcat. */
+public class StopWatch {
+
+ private final String mLabel;
+
+ private final ArrayList<Long> mTimes = new ArrayList<>();
+ private final ArrayList<String> mLapLabels = new ArrayList<>();
+
+ private StopWatch(String label) {
+ mLabel = label;
+ lap("");
+ }
+
+ /** Create a new instance and start it. */
+ public static StopWatch start(String label) {
+ return new StopWatch(label);
+ }
+
+ /** Return a dummy instance that does no operations. */
+ public static StopWatch getNullStopWatch() {
+ return NullStopWatch.INSTANCE;
+ }
+
+ /** Record a lap. */
+ public void lap(String lapLabel) {
+ mTimes.add(System.currentTimeMillis());
+ mLapLabels.add(lapLabel);
+ }
+
+ /** Stop it and log the result, if the total time >= {@code timeThresholdToLog}. */
+ public void stopAndLog(String TAG, int timeThresholdToLog) {
+
+ lap("");
+
+ final long start = mTimes.get(0);
+ final long stop = mTimes.get(mTimes.size() - 1);
+
+ final long total = stop - start;
+ if (total < timeThresholdToLog) {
+ return;
+ }
+
+ final StringBuilder sb = new StringBuilder();
+ sb.append(mLabel);
+ sb.append(",");
+ sb.append(total);
+ sb.append(": ");
+
+ long last = start;
+ for (int i = 1; i < mTimes.size(); i++) {
+ final long current = mTimes.get(i);
+ sb.append(mLapLabels.get(i));
+ sb.append(",");
+ sb.append((current - last));
+ sb.append(" ");
+ last = current;
+ }
+ Log.v(TAG, sb.toString());
+ }
+
+ private static class NullStopWatch extends StopWatch {
+
+ public static final NullStopWatch INSTANCE = new NullStopWatch();
+
+ public NullStopWatch() {
+ super(null);
+ }
+
+ @Override
+ public void lap(String lapLabel) {
+ // noop
+ }
+
+ @Override
+ public void stopAndLog(String TAG, int timeThresholdToLog) {
+ // noop
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/util/TelephonyManagerUtils.java b/java/com/android/contacts/common/util/TelephonyManagerUtils.java
new file mode 100644
index 000000000..b664268ca
--- /dev/null
+++ b/java/com/android/contacts/common/util/TelephonyManagerUtils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.util;
+
+import android.content.Context;
+import android.telephony.TelephonyManager;
+
+/** This class provides several TelephonyManager util functions. */
+public class TelephonyManagerUtils {
+
+ /**
+ * Gets the voicemail tag from Telephony Manager.
+ *
+ * @param context Current application context
+ * @return Voicemail tag, the alphabetic identifier associated with the voice mail number.
+ */
+ public static String getVoiceMailAlphaTag(Context context) {
+ final TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ final String voiceMailLabel = telephonyManager.getVoiceMailAlphaTag();
+ return voiceMailLabel;
+ }
+
+ /**
+ * @param context Current application context.
+ * @return True if there is a subscription which supports video calls. False otherwise.
+ */
+ public static boolean hasVideoCallSubscription(Context context) {
+ // TODO: Check the telephony manager's subscriptions to see if any support video calls.
+ return true;
+ }
+}
diff --git a/java/com/android/contacts/common/util/TrafficStatsTags.java b/java/com/android/contacts/common/util/TrafficStatsTags.java
new file mode 100644
index 000000000..b0e7fb583
--- /dev/null
+++ b/java/com/android/contacts/common/util/TrafficStatsTags.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.contacts.common.util;
+
+public class TrafficStatsTags {
+
+ public static final int CONTACT_PHOTO_DOWNLOAD_TAG = 0x0001;
+ public static final int TAG_MAX = 0x9999;
+}
diff --git a/java/com/android/contacts/common/util/UriUtils.java b/java/com/android/contacts/common/util/UriUtils.java
new file mode 100644
index 000000000..4690942ba
--- /dev/null
+++ b/java/com/android/contacts/common/util/UriUtils.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.util;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+import java.util.List;
+
+/** Utility methods for dealing with URIs. */
+public class UriUtils {
+
+ /** Static helper, not instantiable. */
+ private UriUtils() {}
+
+ /** Checks whether two URI are equal, taking care of the case where either is null. */
+ public static boolean areEqual(Uri uri1, Uri uri2) {
+ if (uri1 == null && uri2 == null) {
+ return true;
+ }
+ if (uri1 == null || uri2 == null) {
+ return false;
+ }
+ return uri1.equals(uri2);
+ }
+
+ /** Parses a string into a URI and returns null if the given string is null. */
+ public static Uri parseUriOrNull(String uriString) {
+ if (uriString == null) {
+ return null;
+ }
+ return Uri.parse(uriString);
+ }
+
+ /** Converts a URI into a string, returns null if the given URI is null. */
+ public static String uriToString(Uri uri) {
+ return uri == null ? null : uri.toString();
+ }
+
+ public static boolean isEncodedContactUri(Uri uri) {
+ if (uri == null) {
+ return false;
+ }
+ final String lastPathSegment = uri.getLastPathSegment();
+ if (lastPathSegment == null) {
+ return false;
+ }
+ return lastPathSegment.equals(Constants.LOOKUP_URI_ENCODED);
+ }
+
+ /**
+ * @return {@code uri} as-is if the authority is of contacts provider. Otherwise or {@code uri} is
+ * null, return null otherwise
+ */
+ public static Uri nullForNonContactsUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ return ContactsContract.AUTHORITY.equals(uri.getAuthority()) ? uri : null;
+ }
+
+ /** Parses the given URI to determine the original lookup key of the contact. */
+ public static String getLookupKeyFromUri(Uri lookupUri) {
+ // Would be nice to be able to persist the lookup key somehow to avoid having to parse
+ // the uri entirely just to retrieve the lookup key, but every uri is already parsed
+ // once anyway to check if it is an encoded JSON uri, so this has negligible effect
+ // on performance.
+ if (lookupUri != null && !UriUtils.isEncodedContactUri(lookupUri)) {
+ final List<String> segments = lookupUri.getPathSegments();
+ // This returns the third path segment of the uri, where the lookup key is located.
+ // See {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ return (segments.size() < 3) ? null : Uri.encode(segments.get(2));
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java
new file mode 100644
index 000000000..2988a5a58
--- /dev/null
+++ b/java/com/android/contacts/common/widget/ActivityTouchLinearLayout.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.LinearLayout;
+import com.android.dialer.util.TouchPointManager;
+
+/**
+ * Linear layout for an activity that listens to all touch events on the screen and saves the touch
+ * point. Typically touch events are handled by child views--this class intercepts those touch
+ * events before passing them on to the child.
+ */
+public class ActivityTouchLinearLayout extends LinearLayout {
+
+ public ActivityTouchLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+ TouchPointManager.getInstance().setPoint((int) ev.getRawX(), (int) ev.getRawY());
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/contacts/common/widget/FloatingActionButtonController.java b/java/com/android/contacts/common/widget/FloatingActionButtonController.java
new file mode 100644
index 000000000..f03129779
--- /dev/null
+++ b/java/com/android/contacts/common/widget/FloatingActionButtonController.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.widget;
+
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ImageButton;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.FabUtil;
+import com.android.dialer.animation.AnimUtils;
+
+/** Controls the movement and appearance of the FAB (Floating Action Button). */
+public class FloatingActionButtonController {
+
+ public static final int ALIGN_MIDDLE = 0;
+ public static final int ALIGN_QUARTER_END = 1;
+ public static final int ALIGN_END = 2;
+
+ private static final int FAB_SCALE_IN_DURATION = 266;
+ private static final int FAB_SCALE_IN_FADE_IN_DELAY = 100;
+ private static final int FAB_ICON_FADE_OUT_DURATION = 66;
+
+ private final int mAnimationDuration;
+ private final int mFloatingActionButtonWidth;
+ private final int mFloatingActionButtonMarginRight;
+ private final View mFloatingActionButtonContainer;
+ private final ImageButton mFloatingActionButton;
+ private final Interpolator mFabInterpolator;
+ private int mScreenWidth;
+
+ public FloatingActionButtonController(Activity activity, View container, ImageButton button) {
+ Resources resources = activity.getResources();
+ mFabInterpolator =
+ AnimationUtils.loadInterpolator(activity, android.R.interpolator.fast_out_slow_in);
+ mFloatingActionButtonWidth =
+ resources.getDimensionPixelSize(R.dimen.floating_action_button_width);
+ mFloatingActionButtonMarginRight =
+ resources.getDimensionPixelOffset(R.dimen.floating_action_button_margin_right);
+ mAnimationDuration = resources.getInteger(R.integer.floating_action_button_animation_duration);
+ mFloatingActionButtonContainer = container;
+ mFloatingActionButton = button;
+ FabUtil.setupFloatingActionButton(mFloatingActionButtonContainer, resources);
+ }
+
+ /**
+ * Passes the screen width into the class. Necessary for translation calculations. Should be
+ * called as soon as parent View width is available.
+ *
+ * @param screenWidth The width of the screen in pixels.
+ */
+ public void setScreenWidth(int screenWidth) {
+ mScreenWidth = screenWidth;
+ }
+
+ public boolean isVisible() {
+ return mFloatingActionButtonContainer.getVisibility() == View.VISIBLE;
+ }
+
+ /**
+ * Sets FAB as View.VISIBLE or View.GONE.
+ *
+ * @param visible Whether or not to make the container visible.
+ */
+ public void setVisible(boolean visible) {
+ mFloatingActionButtonContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+
+ public void changeIcon(Drawable icon, String description) {
+ if (mFloatingActionButton.getDrawable() != icon
+ || !mFloatingActionButton.getContentDescription().equals(description)) {
+ mFloatingActionButton.setImageDrawable(icon);
+ mFloatingActionButton.setContentDescription(description);
+ }
+ }
+
+ /**
+ * Updates the FAB location (middle to right position) as the PageView scrolls.
+ *
+ * @param positionOffset A fraction used to calculate position of the FAB during page scroll.
+ */
+ public void onPageScrolled(float positionOffset) {
+ // As the page is scrolling, if we're on the first tab, update the FAB position so it
+ // moves along with it.
+ mFloatingActionButtonContainer.setTranslationX(
+ (int) (positionOffset * getTranslationXForAlignment(ALIGN_END)));
+ }
+
+ /**
+ * Aligns the FAB to the described location
+ *
+ * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+ * @param animate Whether or not to animate the transition.
+ */
+ public void align(int align, boolean animate) {
+ align(align, 0 /*offsetX */, 0 /* offsetY */, animate);
+ }
+
+ /**
+ * Aligns the FAB to the described location plus specified additional offsets.
+ *
+ * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+ * @param offsetX Additional offsetX to translate by.
+ * @param offsetY Additional offsetY to translate by.
+ * @param animate Whether or not to animate the transition.
+ */
+ public void align(int align, int offsetX, int offsetY, boolean animate) {
+ if (mScreenWidth == 0) {
+ return;
+ }
+
+ int translationX = getTranslationXForAlignment(align);
+
+ // Skip animation if container is not shown; animation causes container to show again.
+ if (animate && mFloatingActionButtonContainer.isShown()) {
+ mFloatingActionButtonContainer
+ .animate()
+ .translationX(translationX + offsetX)
+ .translationY(offsetY)
+ .setInterpolator(mFabInterpolator)
+ .setDuration(mAnimationDuration)
+ .start();
+ } else {
+ mFloatingActionButtonContainer.setTranslationX(translationX + offsetX);
+ mFloatingActionButtonContainer.setTranslationY(offsetY);
+ }
+ }
+
+ /**
+ * Resizes width and height of the floating action bar container.
+ *
+ * @param dimension The new dimensions for the width and height.
+ * @param animate Whether to animate this change.
+ */
+ public void resize(int dimension, boolean animate) {
+ if (animate) {
+ AnimUtils.changeDimensions(mFloatingActionButtonContainer, dimension, dimension);
+ } else {
+ mFloatingActionButtonContainer.getLayoutParams().width = dimension;
+ mFloatingActionButtonContainer.getLayoutParams().height = dimension;
+ mFloatingActionButtonContainer.requestLayout();
+ }
+ }
+
+ /**
+ * Scales the floating action button from no height and width to its actual dimensions. This is an
+ * animation for showing the floating action button.
+ *
+ * @param delayMs The delay for the effect, in milliseconds.
+ */
+ public void scaleIn(int delayMs) {
+ setVisible(true);
+ AnimUtils.scaleIn(mFloatingActionButtonContainer, FAB_SCALE_IN_DURATION, delayMs);
+ AnimUtils.fadeIn(
+ mFloatingActionButton, FAB_SCALE_IN_DURATION, delayMs + FAB_SCALE_IN_FADE_IN_DELAY, null);
+ }
+
+ /** Immediately remove the affects of the last call to {@link #scaleOut}. */
+ public void resetIn() {
+ mFloatingActionButton.setAlpha(1f);
+ mFloatingActionButton.setVisibility(View.VISIBLE);
+ mFloatingActionButtonContainer.setScaleX(1);
+ mFloatingActionButtonContainer.setScaleY(1);
+ }
+
+ /**
+ * Scales the floating action button from its actual dimensions to no height and width. This is an
+ * animation for hiding the floating action button.
+ */
+ public void scaleOut() {
+ AnimUtils.scaleOut(mFloatingActionButtonContainer, mAnimationDuration);
+ // Fade out the icon faster than the scale out animation, so that the icon scaling is less
+ // obvious. We don't want it to scale, but the resizing the container is not as performant.
+ AnimUtils.fadeOut(mFloatingActionButton, FAB_ICON_FADE_OUT_DURATION, null);
+ }
+
+ /**
+ * Calculates the X offset of the FAB to the given alignment, adjusted for whether or not the view
+ * is in RTL mode.
+ *
+ * @param align One of ALIGN_MIDDLE, ALIGN_QUARTER_RIGHT, or ALIGN_RIGHT.
+ * @return The translationX for the given alignment.
+ */
+ public int getTranslationXForAlignment(int align) {
+ int result = 0;
+ switch (align) {
+ case ALIGN_MIDDLE:
+ // Moves the FAB to exactly center screen.
+ return 0;
+ case ALIGN_QUARTER_END:
+ // Moves the FAB a quarter of the screen width.
+ result = mScreenWidth / 4;
+ break;
+ case ALIGN_END:
+ // Moves the FAB half the screen width. Same as aligning right with a marginRight.
+ result =
+ mScreenWidth / 2 - mFloatingActionButtonWidth / 2 - mFloatingActionButtonMarginRight;
+ break;
+ }
+ if (isLayoutRtl()) {
+ result *= -1;
+ }
+ return result;
+ }
+
+ private boolean isLayoutRtl() {
+ return mFloatingActionButtonContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+}
diff --git a/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java
new file mode 100644
index 000000000..d84d8f757
--- /dev/null
+++ b/java/com/android/contacts/common/widget/LayoutSuppressingImageView.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.common.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ * Custom {@link ImageView} that improves layouting performance.
+ *
+ * <p>This improves the performance by not passing requestLayout() to its parent, taking advantage
+ * of knowing that image size won't change once set.
+ */
+public class LayoutSuppressingImageView extends ImageView {
+
+ public LayoutSuppressingImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ public void requestLayout() {
+ forceLayout();
+ }
+}
diff --git a/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
new file mode 100644
index 000000000..63f8ca580
--- /dev/null
+++ b/java/com/android/contacts/common/widget/SelectPhoneAccountDialogFragment.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.contacts.common.widget;
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import com.android.contacts.common.R;
+import com.android.contacts.common.compat.PhoneAccountCompat;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Dialog that allows the user to select a phone accounts for a given action. Optionally provides
+ * the choice to set the phone account as default.
+ */
+public class SelectPhoneAccountDialogFragment extends DialogFragment {
+
+ private static final String ARG_TITLE_RES_ID = "title_res_id";
+ private static final String ARG_CAN_SET_DEFAULT = "can_set_default";
+ private static final String ARG_ACCOUNT_HANDLES = "account_handles";
+ private static final String ARG_IS_DEFAULT_CHECKED = "is_default_checked";
+ private static final String ARG_LISTENER = "listener";
+ private static final String ARG_CALL_ID = "call_id";
+
+ private int mTitleResId;
+ private boolean mCanSetDefault;
+ private List<PhoneAccountHandle> mAccountHandles;
+ private boolean mIsSelected;
+ private boolean mIsDefaultChecked;
+ private SelectPhoneAccountListener mListener;
+
+ public SelectPhoneAccountDialogFragment() {}
+
+ /**
+ * Create new fragment instance with default title and no option to set as default.
+ *
+ * @param accountHandles The {@code PhoneAccountHandle}s available to select from.
+ * @param listener The listener for the results of the account selection.
+ */
+ public static SelectPhoneAccountDialogFragment newInstance(
+ List<PhoneAccountHandle> accountHandles,
+ SelectPhoneAccountListener listener,
+ @Nullable String callId) {
+ return newInstance(
+ R.string.select_account_dialog_title, false, accountHandles, listener, callId);
+ }
+
+ /**
+ * Create new fragment instance. This method also allows specifying a custom title and "set
+ * default" checkbox.
+ *
+ * @param titleResId The resource ID for the string to use in the title of the dialog.
+ * @param canSetDefault {@code true} if the dialog should include an option to set the selection
+ * as the default. False otherwise.
+ * @param accountHandles The {@code PhoneAccountHandle}s available to select from.
+ * @param listener The listener for the results of the account selection.
+ */
+ public static SelectPhoneAccountDialogFragment newInstance(
+ int titleResId,
+ boolean canSetDefault,
+ List<PhoneAccountHandle> accountHandles,
+ SelectPhoneAccountListener listener,
+ @Nullable String callId) {
+ ArrayList<PhoneAccountHandle> accountHandlesCopy = new ArrayList<>();
+ if (accountHandles != null) {
+ accountHandlesCopy.addAll(accountHandles);
+ }
+ SelectPhoneAccountDialogFragment fragment = new SelectPhoneAccountDialogFragment();
+ final Bundle args = new Bundle();
+ args.putInt(ARG_TITLE_RES_ID, titleResId);
+ args.putBoolean(ARG_CAN_SET_DEFAULT, canSetDefault);
+ args.putParcelableArrayList(ARG_ACCOUNT_HANDLES, accountHandlesCopy);
+ args.putParcelable(ARG_LISTENER, listener);
+ args.putString(ARG_CALL_ID, callId);
+ fragment.setArguments(args);
+ fragment.setListener(listener);
+ return fragment;
+ }
+
+ public void setListener(SelectPhoneAccountListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(ARG_IS_DEFAULT_CHECKED, mIsDefaultChecked);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mTitleResId = getArguments().getInt(ARG_TITLE_RES_ID);
+ mCanSetDefault = getArguments().getBoolean(ARG_CAN_SET_DEFAULT);
+ mAccountHandles = getArguments().getParcelableArrayList(ARG_ACCOUNT_HANDLES);
+ mListener = getArguments().getParcelable(ARG_LISTENER);
+ if (savedInstanceState != null) {
+ mIsDefaultChecked = savedInstanceState.getBoolean(ARG_IS_DEFAULT_CHECKED);
+ }
+ mIsSelected = false;
+
+ final DialogInterface.OnClickListener selectionListener =
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mIsSelected = true;
+ PhoneAccountHandle selectedAccountHandle = mAccountHandles.get(which);
+ Bundle result = new Bundle();
+ result.putParcelable(
+ SelectPhoneAccountListener.EXTRA_SELECTED_ACCOUNT_HANDLE, selectedAccountHandle);
+ result.putBoolean(SelectPhoneAccountListener.EXTRA_SET_DEFAULT, mIsDefaultChecked);
+ result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId());
+ if (mListener != null) {
+ mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_SELECTED, result);
+ }
+ }
+ };
+
+ final CompoundButton.OnCheckedChangeListener checkListener =
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton check, boolean isChecked) {
+ mIsDefaultChecked = isChecked;
+ }
+ };
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ ListAdapter selectAccountListAdapter =
+ new SelectAccountListAdapter(
+ builder.getContext(), R.layout.select_account_list_item, mAccountHandles);
+
+ AlertDialog dialog =
+ builder
+ .setTitle(mTitleResId)
+ .setAdapter(selectAccountListAdapter, selectionListener)
+ .create();
+
+ if (mCanSetDefault) {
+ // Generate custom checkbox view, lint suppressed since no appropriate parent (is dialog)
+ @SuppressLint("InflateParams")
+ LinearLayout checkboxLayout =
+ (LinearLayout)
+ LayoutInflater.from(builder.getContext())
+ .inflate(R.layout.default_account_checkbox, null);
+
+ CheckBox cb = (CheckBox) checkboxLayout.findViewById(R.id.default_account_checkbox_view);
+ cb.setOnCheckedChangeListener(checkListener);
+ cb.setChecked(mIsDefaultChecked);
+
+ dialog.getListView().addFooterView(checkboxLayout);
+ }
+
+ return dialog;
+ }
+
+ @Override
+ public void onStop() {
+ if (!mIsSelected && mListener != null) {
+ Bundle result = new Bundle();
+ result.putString(SelectPhoneAccountListener.EXTRA_CALL_ID, getCallId());
+ mListener.onReceiveResult(SelectPhoneAccountListener.RESULT_DISMISSED, result);
+ }
+ super.onStop();
+ }
+
+ @Nullable
+ private String getCallId() {
+ return getArguments().getString(ARG_CALL_ID);
+ }
+
+ public static class SelectPhoneAccountListener extends ResultReceiver {
+
+ static final int RESULT_SELECTED = 1;
+ static final int RESULT_DISMISSED = 2;
+
+ static final String EXTRA_SELECTED_ACCOUNT_HANDLE = "extra_selected_account_handle";
+ static final String EXTRA_SET_DEFAULT = "extra_set_default";
+ static final String EXTRA_CALL_ID = "extra_call_id";
+
+ public SelectPhoneAccountListener() {
+ super(new Handler());
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (resultCode == RESULT_SELECTED) {
+ onPhoneAccountSelected(
+ resultData.getParcelable(EXTRA_SELECTED_ACCOUNT_HANDLE),
+ resultData.getBoolean(EXTRA_SET_DEFAULT),
+ resultData.getString(EXTRA_CALL_ID));
+ } else if (resultCode == RESULT_DISMISSED) {
+ onDialogDismissed(resultData.getString(EXTRA_CALL_ID));
+ }
+ }
+
+ public void onPhoneAccountSelected(
+ PhoneAccountHandle selectedAccountHandle, boolean setDefault, @Nullable String callId) {}
+
+ public void onDialogDismissed(@Nullable String callId) {}
+ }
+
+ private static class SelectAccountListAdapter extends ArrayAdapter<PhoneAccountHandle> {
+
+ private int mResId;
+
+ public SelectAccountListAdapter(
+ Context context, int resource, List<PhoneAccountHandle> accountHandles) {
+ super(context, resource, accountHandles);
+ mResId = resource;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ View rowView;
+ final ViewHolder holder;
+
+ if (convertView == null) {
+ // Cache views for faster scrolling
+ rowView = inflater.inflate(mResId, null);
+ holder = new ViewHolder();
+ holder.labelTextView = (TextView) rowView.findViewById(R.id.label);
+ holder.numberTextView = (TextView) rowView.findViewById(R.id.number);
+ holder.imageView = (ImageView) rowView.findViewById(R.id.icon);
+ rowView.setTag(holder);
+ } else {
+ rowView = convertView;
+ holder = (ViewHolder) rowView.getTag();
+ }
+
+ PhoneAccountHandle accountHandle = getItem(position);
+ PhoneAccount account =
+ getContext().getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ if (account == null) {
+ return rowView;
+ }
+ holder.labelTextView.setText(account.getLabel());
+ if (account.getAddress() == null
+ || TextUtils.isEmpty(account.getAddress().getSchemeSpecificPart())) {
+ holder.numberTextView.setVisibility(View.GONE);
+ } else {
+ holder.numberTextView.setVisibility(View.VISIBLE);
+ holder.numberTextView.setText(
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ account.getAddress().getSchemeSpecificPart()));
+ }
+ holder.imageView.setImageDrawable(
+ PhoneAccountCompat.createIconDrawable(account, getContext()));
+ return rowView;
+ }
+
+ private static final class ViewHolder {
+
+ TextView labelTextView;
+ TextView numberTextView;
+ ImageView imageView;
+ }
+ }
+}
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 @@
+<!-- Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.app">
+
+ <uses-permission android:name="android.permission.CALL_PHONE"/>
+ <uses-permission android:name="android.permission.READ_CONTACTS"/>
+ <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
+ <uses-permission android:name="android.permission.READ_CALL_LOG"/>
+ <uses-permission android:name="android.permission.WRITE_CALL_LOG"/>
+ <uses-permission android:name="android.permission.READ_PROFILE"/>
+ <uses-permission android:name="android.permission.MANAGE_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS"/>
+ <uses-permission android:name="android.permission.GET_ACCOUNTS_PRIVILEGED"/>
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+ <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS"/>
+ <uses-permission android:name="android.permission.NFC"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
+ <uses-permission android:name="android.permission.MODIFY_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WRITE_SETTINGS"/>
+ <uses-permission android:name="android.permission.USE_CREDENTIALS"/>
+ <uses-permission android:name="android.permission.VIBRATE"/>
+ <uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
+ <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL"/>
+ <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL"/>
+ <uses-permission android:name="android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK"/>
+ <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+ <uses-permission android:name="android.permission.BROADCAST_STICKY"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+
+ <!-- This tells the activity manager to not delay any of our activity
+ start requests, even if they happen immediately after the user
+ presses home. -->
+ <uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <application
+ android:backupAgent='com.android.dialer.backup.DialerBackupAgent'
+ android:fullBackupOnly="true"
+ android:restoreAnyVersion="true"
+ android:name="com.android.dialer.app.DialerApplication">
+
+ <activity
+ android:exported="false"
+ android:label="@string/manage_blocked_numbers_label"
+ android:name="com.android.dialer.app.filterednumber.BlockedNumbersSettingsActivity"
+ android:parentActivityName="com.android.dialer.app.settings.DialerSettingsActivity"
+ android:theme="@style/ManageBlockedNumbersStyle">
+ <intent-filter>
+ <action android:name="com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ </activity>
+
+ <receiver android:name="com.android.dialer.app.calllog.CallLogReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.NEW_VOICEMAIL"/>
+ <data
+ android:host="com.android.voicemail"
+ android:mimeType="vnd.android.cursor.item/voicemail"
+ android:scheme="content"
+ />
+ </intent-filter>
+ <intent-filter android:priority="100">
+ <action android:name="android.intent.action.BOOT_COMPLETED"/>
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:directBootAware="true"
+ android:exported="false"
+ android:name="com.android.dialer.app.calllog.CallLogNotificationsService"
+ />
+
+ <receiver
+ android:directBootAware="true"
+ android:name="com.android.dialer.app.calllog.MissedCallNotificationReceiver">
+ <intent-filter>
+ <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION"/>
+ </intent-filter>
+ </receiver>
+
+ <provider
+ android:authorities="com.android.dialer.files"
+ android:exported="false"
+ android:grantUriPermissions="true"
+ android:name="android.support.v4.content.FileProvider">
+ <meta-data
+ android:name="android.support.FILE_PROVIDER_PATHS"
+ android:resource="@xml/file_paths"/>
+ </provider>
+ </application>
+</manifest>
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.
+ *
+ * <p>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<Void, Void, Void>() {
+ @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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<String> 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> 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<FrameLayout> {
+
+ @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}.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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()}.
+ *
+ * <p>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.
+ *
+ * <p>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 *#*#<code>#*#*. If a secret
+ * code is encountered an Intent is started with the android_secret_code://<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 *#*#<code>#*#*
+ 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.
+ *
+ * <p>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<PhoneAccountHandle> 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<PhoneAccountHandle> 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<String> deviceIds = new ArrayList<String>();
+ 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).
+ *
+ * <p>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
+ *
+ * <p>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 <code>layoutId</code> 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<CharSequence> 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.
+ *
+ * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
+ * may 25,20:02".
+ *
+ * <p>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<Long> 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.
+ *
+ * <p>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<Uri> 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<Long, Integer> 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<Void, Void, Boolean> loadDataTask =
+ new AsyncTask<Void, Void, Boolean>() {
+ @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.
+ *
+ * <p>It uses the next {@code count} rows in the cursor to extract the types.
+ *
+ * <p>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.
+ *
+ * <p>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}.
+ *
+ * <pre class="prettyprint"> 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); </pre>
+ */
+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<GetLastOutgoingCallArgs, Void, String> {
+
+ 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<Void, Void, PhoneCallDetails[]>() {
+ @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<Void, Void, Void>() {
+ @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<Void, Void, Void>() {
+ @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<Void, Void, Void>() {
+ @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<Void, Void, Void>() {
+ @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<String> 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.
+ *
+ * <p>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".
+ *
+ * <p>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.
+ *
+ * <p>For entries that are not grouped with others, we do not need to create a group of size one.
+ *
+ * <p>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 "".
+ *
+ * <p>If more than one call for the caller, {Number of Calls} is: "{number of calls} calls.",
+ * otherwise "".
+ *
+ * <p>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}.
+ *
+ * <p>For outgoing calls: If outgoing: Call to {Name/Number] {Call Type} {Call Time}.
+ *
+ * <p>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.
+ *
+ * <p>The {Phone Account} refers to the account/SIM through which the call was placed or received
+ * in multi-SIM devices.
+ *
+ * <p>Examples: 3 calls. New Voicemail. Missed call from Joe Smith mobile 2 hours ago on SIM 1.
+ *
+ * <p>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 <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID = R.string.description_incoming_missed_call;
+ } else if (lastCallType == AppCompatConstants.CALLS_INCOMING_TYPE) {
+ //Message: Answered call from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID = R.string.description_incoming_answered_call;
+ } else if (lastCallType == AppCompatConstants.CALLS_VOICEMAIL_TYPE) {
+ //Message: (Unread) [V/v]oicemail from <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>,
+ //<PhoneAccount>.
+ stringID =
+ isRead ? R.string.description_read_voicemail : R.string.description_unread_voicemail;
+ } else {
+ //Message: Call to <NameOrNumber>, <TypeOrLocation>, <TimeOfCall>, <PhoneAccount>.
+ 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.
+ *
+ * <p>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<Void, Void, ?> 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.
+ *
+ * <p>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<NewCall> 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<NewCall> 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<NewCall> 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<NewCall> 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<NewCall> 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.
+ *
+ * <p>It handles the following actions:
+ *
+ * <ul>
+ * <li>Updating voicemail notifications
+ * <li>Marking new voicemails as old
+ * <li>Updating missed call notifications
+ * <li>Marking new missed calls as old
+ * <li>Calling back from a missed call
+ * <li>Sending an SMS from a missed call
+ * </ul>
+ */
+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.
+ *
+ * <p>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.
+ *
+ * <p>It must be a {@link Uri}.
+ */
+ public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
+ /**
+ * Action to update the missed call notifications.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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<Integer> 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<Void, Void, Void> task =
+ new AsyncTask<Void, Void, Void>() {
+ @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.
+ *
+ * <p>Clears the notification if there are no new voicemails, and notifies if the given URI
+ * corresponds to a new voicemail.
+ *
+ * <p>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<NewCall> 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<String, String> 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<NewCall> 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<Uri, Integer> 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<Uri, Integer> 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.
+ *
+ * <p>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.
+ *
+ * <p>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<ContentValues> 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<NewCall> 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: <caller name || handle>
+ // More than 1 missed call: <number of calls> + "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<PhoneAccountHandle> getSubscriptionPhoneAccounts(Context context) {
+ List<PhoneAccountHandle> subscriptionAccountHandles = new ArrayList<PhoneAccountHandle>();
+ final List<PhoneAccountHandle> 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<CharSequence> mDescriptionItems = new ArrayList<>();
+
+ /**
+ * Creates a new instance of the helper.
+ *
+ * <p>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.
+ *
+ * <p>If created today, DATE is 'Today' If created this year, DATE is 'MMM dd' Otherwise, DATE is
+ * 'MMM dd, yyyy'
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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).
+ *
+ * <p>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.
+ *
+ * <p>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<Pair<PhoneAccountHandle, CharSequence>, Boolean> mVoicemailQueryCache =
+ new ConcurrentHashMap<>();
+
+ private final Map<PhoneAccountHandle, String> mPhoneAccountLabelCache = new HashMap<>();
+ private final Map<PhoneAccountHandle, Integer> mPhoneAccountColorCache = new HashMap<>();
+ private final Map<PhoneAccountHandle, Boolean> 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<PhoneAccountHandle, CharSequence> 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.
+ *
+ * <p>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.
+ *
+ * <p>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<NumberWithCountryIso, ContactInfo> mCache;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final OnContactInfoChangedListener mOnContactInfoChangedListener;
+ private final BlockingQueue<ContactInfoRequest> mUpdateRequests;
+ private final Handler mHandler;
+ private QueryThread mContactInfoQueryThread;
+ private volatile boolean mRequestProcessingDisabled = false;
+
+ private static class InnerHandler extends Handler {
+
+ private final WeakReference<ContactInfoCache> contactInfoCacheWeakReference;
+
+ public InnerHandler(WeakReference<ContactInfoCache> 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<NumberWithCountryIso, ContactInfo> 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<ContactInfo> 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.
+ *
+ * <p>Upon completion it also updates the cache in the call log, if it is different from {@code
+ * callLogInfo}.
+ *
+ * <p>The number might be either a SIP address or a phone number.
+ *
+ * <p>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.
+ *
+ * <p>It also provides the current contact info stored in the call log for this number.
+ *
+ * <p>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<ContactInfoRequest> {
+
+ 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<NumberWithCountryIso, ContactInfo> 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<NumberWithCountryIso, ContactInfo> 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}.
+ *
+ * <p>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".
+ *
+ * <p>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<View> mPressedDialpadKeys = new HashSet<View>(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<PhoneAccountHandle> 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.
+ *
+ * <p>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.)
+ *
+ * <p>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
+ *
+ * <p>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.
+ *
+ * <p>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).
+ *
+ * <p>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<Cursor> class to asynchronously load SmartDial search results. */
+public class SmartDialCursorLoader extends AsyncTaskLoader<Cursor> {
+
+ 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<ContactNumber> 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<Cursor>,
+ 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<Cursor> 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<Cursor> 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<Cursor> 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<Cursor>, 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<Cursor> 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<Cursor> loader, Cursor data) {
+ mAdapter.swapCursor(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> 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<ContactEntryListAdapter>
+ 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<Cursor> 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
+ *
+ * <p>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<OnDragDropListener> mOnDragDropListeners = new ArrayList<OnDragDropListener>();
+ 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.
+ *
+ * <p>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<OnPageChangeListener> mOnPageChangeListeners =
+ new ArrayList<OnPageChangeListener>();
+ 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<Fragment> 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<ContactEntry> 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<ContactEntry> mContactEntryComparator =
+ new Comparator<ContactEntry>() {
+ @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.
+ *
+ * <p>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}.
+ *
+ * <p>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<Object> 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<ContentProviderOperation> 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.
+ *
+ * <p>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<ContactEntry> toArrange) {
+ final PriorityQueue<ContactEntry> pinnedQueue =
+ new PriorityQueue<>(PIN_LIMIT, mContactEntryComparator);
+
+ final List<ContactEntry> 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.
+ *
+ * <p>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<ContentProviderOperation> getReflowedPinningOperations(
+ ArrayList<ContactEntry> list, int oldPos, int newPinPos) {
+ final ArrayList<ContentProviderOperation> 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<SmartDialMatchPosition> 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<Cursor> 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<Integer> mItemIdTopMap = new LongSparseArray<>();
+ private final LongSparseArray<Integer> mItemIdLeftMap = new LongSparseArray<>();
+ private final ContactTileView.Listener mContactTileAdapterListener =
+ new ContactTileAdapterListener();
+ private final LoaderManager.LoaderCallbacks<Cursor> 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}
+ *
+ * <p>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<Animator> animators = new ArrayList<Animator>();
+ 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<Cursor> {
+
+ @Override
+ public CursorLoader onCreateLoader(int id, Bundle args) {
+ if (DEBUG) {
+ LogUtil.d("ContactTileLoaderListener.onCreateLoader", null);
+ }
+ return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+ if (DEBUG) {
+ LogUtil.d("ContactTileLoaderListener.onLoadFinished", null);
+ }
+ mContactTileAdapter.setContactCursor(data);
+ setEmptyViewVisibility(mContactTileAdapter.getCount() == 0);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> 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 @@
+<!-- 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.
+-->
+
+<!-- This manifest file contains activites that are subclasses by
+ Google Dialer. TODO: Need to stop subclassing activities and move this
+ back into the main manifest file. -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.app">
+
+ <application>
+
+ <activity
+ android:exported="false"
+ android:label="@string/dialer_settings_label"
+ android:name="com.android.dialer.app.settings.DialerSettingsActivity"
+ android:parentActivityName="com.android.dialer.app.DialtactsActivity"
+ android:theme="@style/SettingsStyle">
+ </activity>
+
+ <activity
+ android:label="@string/callDetailTitle"
+ android:name="com.android.dialer.app.CallDetailActivity"
+ android:parentActivityName="com.android.dialer.calllog.CallLogActivity"
+ android:theme="@style/CallDetailActivityTheme">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ <data android:mimeType="vnd.android.cursor.item/calls"/>
+ </intent-filter>
+ </activity>
+
+ <!-- The entrance point for Phone UI.
+ stateAlwaysHidden is set to suppress keyboard show up on
+ dialpad screen. -->
+ <activity
+ android:clearTaskOnLaunch="true"
+ android:directBootAware="true"
+ android:label="@string/launcherActivityLabel"
+ android:launchMode="singleTask"
+ android:name="com.android.dialer.app.DialtactsActivity"
+ android:resizeableActivity="true"
+ android:theme="@style/DialtactsActivityTheme"
+ android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:mimeType="vnd.android.cursor.item/phone"/>
+ <data android:mimeType="vnd.android.cursor.item/person"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="voicemail"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.DIAL"/>
+ <category android:name="android.intent.category.DEFAULT"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.LAUNCHER"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+ <action android:name="android.intent.action.DIAL"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:scheme="tel"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+
+ <data android:mimeType="vnd.android.cursor.dir/calls"/>
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.CALL_BUTTON"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.BROWSABLE"/>
+ </intent-filter>
+ <!-- This was never intended to be public, but is here for backward
+ compatibility. Use Intent.ACTION_DIAL instead. -->
+ <intent-filter>
+ <action android:name="com.android.phone.action.TOUCH_DIALER"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.TAB"/>
+ </intent-filter>
+ <intent-filter android:label="@string/callHistoryIconLabel">
+ <action android:name="com.android.phone.action.RECENT_CALLS"/>
+
+ <category android:name="android.intent.category.DEFAULT"/>
+ <category android:name="android.intent.category.TAB"/>
+ </intent-filter>
+
+ <meta-data
+ android:name="com.android.keyguard.layout"
+ android:resource="@layout/keyguard_preview"/>
+ </activity>
+
+ </application>
+
+</manifest>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/setting_disabled_color" android:state_enabled="false"/>
+ <item android:color="@color/setting_primary_color"/>
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@color/setting_disabled_color" android:state_enabled="false"/>
+ <item android:color="@color/setting_secondary_color"/>
+</selector>
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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_star.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_star.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png
Binary files 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
--- /dev/null
+++ b/java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png
Binary files 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:endColor="#ff0a242d"
+ android:startColor="#ff020709"/>
+</shape>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/floating_action_button_touch_tint">
+ <item android:id="@android:id/mask">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+</ripple> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_content_copy_24dp"
+ android:tint="@color/call_detail_footer_icon_tint"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_create_24dp"
+ android:tint="@color/call_detail_footer_icon_tint"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_report_24dp"
+ android:tint="@color/call_detail_footer_icon_tint"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_unblock"
+ android:tint="@color/call_detail_footer_icon_tint"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false">
+ <bitmap
+ android:src="@drawable/ic_pause_24dp"
+ android:tint="@color/voicemail_icon_disabled_tint"/>
+ </item>
+
+ <item>
+ <bitmap
+ android:src="@drawable/ic_pause_24dp"
+ android:tint="@color/voicemail_playpause_icon_tint"/>
+ </item>
+
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true">
+
+ <item android:state_enabled="false">
+ <bitmap
+ android:src="@drawable/ic_play_arrow_24dp"
+ android:tint="@color/voicemail_icon_disabled_tint"/>
+ </item>
+
+ <item>
+ <bitmap
+ android:src="@drawable/ic_play_arrow_24dp"
+ android:tint="@color/voicemail_playpause_icon_tint"/>
+ </item>
+
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_results_phone"
+ android:tint="@color/search_shortcut_icon_color"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_vm_sound_off_dis" android:state_enabled="false"/>
+ <item android:drawable="@drawable/ic_vm_sound_off_dk"/>
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_vm_sound_on_dis" android:state_enabled="false"/>
+ <item android:drawable="@drawable/ic_vm_sound_on_dk"/>
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_handle"
+ android:tint="@color/actionbar_background_color">
+</bitmap> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:src="@drawable/ic_handle"
+ android:tint="@color/voicemail_icon_disabled_tint">
+</bitmap> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="#fff"/>
+ </shape>
+ </item>
+</ripple>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/ic_overflow_menu"
+ android:tint="@color/actionbar_icon_color"/>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@color/searchbox_background_color"/>
+ <corners android:radius="2dp"/>
+</shape>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="true">
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ android:width="2dip"
+ />
+ </shape>
+ </item>
+ <!-- I am not defining a secondary progress colour - we don't use it. -->
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_already_played"
+ android:width="2dip"
+ />
+ </shape>
+ </clip>
+ </item>
+ </layer-list>
+ </item>
+ <item>
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/background">
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ android:width="2dip"
+ />
+ </shape>
+ </item>
+ <!-- I am not defining a secondary progress colour - we don't use it. -->
+ <item android:id="@android:id/progress">
+ <clip>
+ <shape android:shape="line">
+ <stroke
+ android:color="@color/voicemail_playback_seek_bar_yet_to_play"
+ android:width="2dip"
+ />
+ </shape>
+ </clip>
+ </item>
+ </layer-list>
+ </item>
+</selector>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * 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.
+ */
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_enabled="false">
+ <shape>
+ <solid android:color="@color/material_grey_300"/>
+ </shape>
+ </item>
+ <item>
+ <shape>
+ <solid android:color="@color/dialer_theme_color"/>
+ </shape>
+ </item>
+</selector> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="0"
+ android:endColor="#1a000000"
+ android:startColor="@null"
+ android:type="linear"/>
+</shape>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <gradient
+ android:angle="90"
+ android:endColor="@null"
+ android:startColor="#1a000000"
+ android:type="linear"/>
+</shape> \ 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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.dialer.app.dialpad.DialpadFragment$DialpadSlidingRelativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <!-- spacer view -->
+ <View
+ android:id="@+id/spacer"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="4"
+ android:background="#00000000"/>
+
+ <!-- Dialpad shadow -->
+ <View
+ android:layout_width="@dimen/shadow_length"
+ android:layout_height="match_parent"
+ android:background="@drawable/shadow_fade_left"/>
+
+ <RelativeLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="6">
+
+ <include
+ layout="@layout/dialpad_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <!-- "Dialpad chooser" UI, shown only when the user brings up the
+ Dialer while a call is already in progress.
+ When this UI is visible, the other Dialer elements
+ (the textfield/button and the dialpad) are hidden. -->
+
+ <ListView
+ android:id="@+id/dialpadChooser"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/background_dialer_light"
+ android:visibility="gone"/>
+
+ <!-- Margin bottom and alignParentBottom don't work well together, so use a Space instead. -->
+ <Space
+ android:id="@+id/dialpad_floating_action_button_margin_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/floating_action_button_margin_bottom"
+ android:layout_alignParentBottom="true"/>
+
+ <FrameLayout
+ android:id="@+id/dialpad_floating_action_button_container"
+ android:layout_width="@dimen/floating_action_button_width"
+ android:layout_height="@dimen/floating_action_button_height"
+ android:layout_above="@id/dialpad_floating_action_button_margin_bottom"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/fab_green">
+
+ <ImageButton
+ android:id="@+id/dialpad_floating_action_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/floating_action_button"
+ android:contentDescription="@string/description_dial_button"
+ android:src="@drawable/fab_ic_call"/>
+
+ </FrameLayout>
+
+ </RelativeLayout>
+
+ </LinearLayout>
+</view>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="4"
+ android:orientation="vertical">
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ <ImageView
+ android:id="@+id/emptyListViewImage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:importantForAccessibility="no"/>
+
+ <TextView
+ android:id="@+id/emptyListViewMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:gravity="center_horizontal|top"
+ android:textColor="@color/empty_list_text_color"
+ android:textSize="@dimen/empty_list_message_text_size"/>
+
+ <TextView
+ android:id="@+id/emptyListViewAction"
+ style="@style/TextActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:gravity="center_horizontal"/>
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+ </LinearLayout>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="6"/>
+
+</merge>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout showing the type of account filter for phone favorite screen
+ (or, new phone "all" screen).
+ This is very similar to account_filter_header.xml but different in its
+ top padding. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/account_filter_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/contact_browser_list_header_left_margin"
+ android:layout_marginEnd="@dimen/contact_browser_list_header_right_margin"
+ android:paddingTop="8dip"
+ android:background="?android:attr/selectableItemBackground"
+ android:orientation="vertical"
+ android:visibility="gone">
+ <TextView
+ android:id="@+id/account_filter_header"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:paddingStart="@dimen/contact_browser_list_item_text_indent"/>
+ <TextView
+ android:id="@+id/contact_list_all_empty"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/contact_phone_list_empty_description_padding"
+ android:paddingBottom="@dimen/contact_phone_list_empty_description_padding"
+ android:paddingStart="8dip"
+ android:text="@string/listFoundAllContactsZero"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="@dimen/contact_phone_list_empty_description_size"
+ android:visibility="gone"/>
+</LinearLayout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/all_contacts_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <fragment
+ android:id="@+id/all_contacts_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:name="com.android.dialer.app.list.AllContactsFragment"/>
+</FrameLayout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pinned_header_list_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- Shown only when an Account filter is set.
+ - paddingTop should be here to show "shade" effect correctly. -->
+ <!-- TODO: Remove the filter header. -->
+ <include layout="@layout/account_filter_header"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1">
+ <view
+ android:id="@android:id/list"
+ style="@style/DialtactsTheme"
+ class="com.android.contacts.common.list.PinnedHeaderListView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="?attr/contact_browser_list_padding_left"
+ android:layout_marginEnd="?attr/contact_browser_list_padding_right"
+ android:paddingTop="18dp"
+ android:fadingEdge="none"
+ android:fastScrollEnabled="true"
+ android:nestedScrollingEnabled="true"/>
+
+ <com.android.dialer.app.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+
+ </FrameLayout>
+</LinearLayout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/blocked_number_container_padding"
+ android:background="@android:color/white"
+ android:focusable="true"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/blocked_number_footer_textview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/block_number_footer_message_vvm"
+ android:textColor="@color/blocked_number_secondary_text_color"
+ android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+ </LinearLayout>
+</LinearLayout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/blocked_number_fragment"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/blocked_number_background"
+ android:orientation="vertical">
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:divider="@null"
+ android:headerDividersEnabled="false"/>
+
+</LinearLayout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/blocked_numbers_disabled_for_emergency"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="27dp"
+ android:paddingBottom="29dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:paddingEnd="44dp"
+ android:background="@color/blocked_number_disabled_emergency_background_color"
+ android:focusable="true"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <TextView
+ style="@style/BlockedNumbersDescriptionTextStyle"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/blocked_numbers_disabled_emergency_header_label"/>
+
+ <TextView
+ style="@style/BlockedNumbersDescriptionTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/blocked_numbers_disabled_emergency_desc"/>
+
+ </LinearLayout>
+
+ <android.support.v7.widget.CardView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ card_view:cardCornerRadius="0dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@android:color/white"
+ android:focusable="true"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/blocked_number_text_view"
+ style="@android:style/TextAppearance.Material.Subhead"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:gravity="center_vertical"
+ android:text="@string/block_list"
+ android:textColor="@color/blocked_number_header_color"/>
+
+ <RelativeLayout
+ android:id="@+id/import_settings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <TextView
+ android:id="@+id/import_description"
+ style="@style/BlockedNumbersDescriptionTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="11dp"
+ android:paddingBottom="27dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:paddingEnd="@dimen/blocked_number_container_padding"
+ android:text="@string/blocked_call_settings_import_description"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+
+ <Button
+ android:id="@+id/import_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/import_description"
+ android:text="@string/blocked_call_settings_import_button"/>
+
+ <Button
+ android:id="@+id/view_numbers_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:layout_below="@id/import_description"
+ android:layout_toStartOf="@id/import_button"
+ android:text="@string/blocked_call_settings_view_numbers_button"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginTop="8dp"
+ android:layout_below="@id/import_button"
+ android:background="@color/divider_line_color"/>
+
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@+id/migrate_promo"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone"
+ tools:visibility="visible">
+
+ <TextView
+ android:id="@+id/migrate_promo_header"
+ style="@android:style/TextAppearance.Material.Subhead"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ android:paddingStart="@dimen/blocked_number_container_padding"
+ android:paddingEnd="@dimen/blocked_number_container_padding"
+ android:gravity="center_vertical"
+ android:text="@string/migrate_blocked_numbers_dialog_title"
+ android:textColor="@color/blocked_number_header_color"/>
+
+ <TextView
+ android:id="@+id/migrate_promo_description"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/blocked_number_container_padding"
+ android:layout_marginStart="@dimen/blocked_number_container_padding"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:text="@string/migrate_blocked_numbers_dialog_message"
+ android:textColor="@color/secondary_text_color"/>
+
+ <Button
+ android:id="@+id/migrate_promo_allow_button"
+ style="@style/DialerPrimaryFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/blocked_number_container_padding"
+ android:layout_marginStart="@dimen/blocked_number_container_padding"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:layout_gravity="end"
+ android:text="@string/migrate_blocked_numbers_dialog_allow_button"/>
+
+ <View
+ style="@style/FullWidthDivider"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/add_number_linear_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/blocked_number_add_top_margin"
+ android:paddingBottom="@dimen/blocked_number_add_bottom_margin"
+ android:paddingStart="@dimen/blocked_number_horizontal_margin"
+ android:background="?android:attr/selectableItemBackground"
+ android:baselineAligned="false"
+ android:clickable="true"
+ android:contentDescription="@string/addBlockedNumber"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/add_number_icon"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:importantForAccessibility="no"/>
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/blocked_number_horizontal_margin"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/add_number_textview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:text="@string/addBlockedNumber"
+ android:textColor="@color/blocked_number_primary_text_color"
+ android:textSize="@dimen/blocked_number_primary_text_size"/>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <View
+ android:id="@+id/blocked_number_list_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginStart="72dp"
+ android:background="@color/divider_line_color"/>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_number_item.xml b/java/com/android/dialer/app/res/layout/blocked_number_item.xml
new file mode 100644
index 000000000..92ebdc35d
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_number_item.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/caller_information"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/blocked_number_horizontal_margin"
+ android:background="@android:color/white"
+ android:baselineAligned="false"
+ android:focusable="true"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:layout_marginTop="@dimen/blocked_number_top_margin"
+ android:layout_marginBottom="@dimen/blocked_number_bottom_margin"
+ android:focusable="true"/>
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/blocked_number_horizontal_margin"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/caller_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="@color/blocked_number_primary_text_color"
+ android:textSize="@dimen/blocked_number_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/caller_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="@color/blocked_number_secondary_text_color"
+ android:textSize="@dimen/blocked_number_settings_description_text_size"/>
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/delete_button"
+ android:layout_width="@dimen/blocked_number_delete_icon_size"
+ android:layout_height="@dimen/blocked_number_delete_icon_size"
+ android:layout_marginEnd="24dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/description_blocked_number_list_delete"
+ android:scaleType="center"
+ android:src="@drawable/ic_remove"
+ android:tint="@color/blocked_number_icon_tint"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml b/java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml
new file mode 100644
index 000000000..0c4874c0f
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/blocked_numbers_activity_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/action_bar_height">
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail.xml b/java/com/android/dialer/app/res/layout/call_detail.xml
new file mode 100644
index 000000000..58a7bf0dc
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_detail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_call_log">
+
+ <!--
+ The list view is under everything.
+ It contains a first header element which is hidden under the controls UI.
+ When scrolling, the controls move up until the name bar hits the top.
+ -->
+ <ListView
+ android:id="@+id/history"
+ android:layout_width="match_parent"
+ android:layout_height="fill_parent"/>
+
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_footer.xml b/java/com/android/dialer/app/res/layout/call_detail_footer.xml
new file mode 100644
index 000000000..57713448e
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail_footer.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/divider_line_thickness"
+ android:background="@color/call_log_action_divider"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_copy"
+ style="@style/CallDetailActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_call_detail_content_copy"
+ android:text="@string/action_copy_number_text"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_edit_before_call"
+ style="@style/CallDetailActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_call_detail_edit"
+ android:text="@string/action_edit_number_before_call"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_report"
+ style="@style/CallDetailActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_call_detail_report"
+ android:text="@string/action_report_number"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_header.xml b/java/com/android/dialer/app/res/layout/call_detail_header.xml
new file mode 100644
index 000000000..fd85f0af1
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail_header.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/caller_information"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_detail_top_margin"
+ android:paddingBottom="@dimen/call_detail_bottom_margin"
+ android:paddingStart="@dimen/call_detail_horizontal_margin"
+ android:background="@color/background_dialer_white"
+ android:baselineAligned="false"
+ android:elevation="@dimen/call_detail_elevation"
+ android:focusable="true"
+ android:orientation="horizontal">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:layout_marginTop="3dp"
+ android:layout_alignParentStart="true"
+ android:layout_gravity="top"
+ android:focusable="true"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/call_detail_horizontal_margin"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/caller_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="2dp"
+ android:layout_marginBottom="3dp"
+ android:includeFontPadding="false"
+ android:singleLine="true"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/caller_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="1dp"
+ android:singleLine="true"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+
+ <TextView
+ android:id="@+id/phone_account_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/call_back_button"
+ android:layout_width="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_height="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_marginEnd="4dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/description_call_log_call_action"
+ android:scaleType="center"
+ android:src="@drawable/ic_call_24dp"
+ android:tint="@color/call_log_list_item_primary_action_icon_tint"
+ android:visibility="gone"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_detail_history_item.xml b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
new file mode 100644
index 000000000..5958ee81c
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_detail_history_item.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/call_log_inner_margin"
+ android:paddingBottom="@dimen/call_log_inner_margin"
+ android:paddingStart="@dimen/call_detail_horizontal_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+ <view
+ android:id="@+id/call_type_icon"
+ class="com.android.dialer.app.calllog.CallTypeIconsView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"/>
+ <TextView
+ android:id="@+id/call_type_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ android:textColor="?android:textColorPrimary"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ <TextView
+ android:id="@+id/duration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_alert_item.xml b/java/com/android/dialer/app/res/layout/call_log_alert_item.xml
new file mode 100644
index 000000000..1e487c288
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_alert_item.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/container"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/app/res/layout/call_log_fragment.xml b/java/com/android/dialer/app/res/layout/call_log_fragment.xml
new file mode 100644
index 000000000..64f7c10e6
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_fragment.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout parameters are set programmatically. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_call_log"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/modal_message_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"/>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/floating_action_button_list_bottom_padding"
+ android:paddingStart="@dimen/call_log_horizontal_margin"
+ android:paddingEnd="@dimen/call_log_horizontal_margin"
+ android:background="@color/background_dialer_call_log"
+ android:clipToPadding="false"/>
+
+ <com.android.dialer.app.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_gravity="center"
+ android:gravity="center_vertical"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item.xml b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
new file mode 100644
index 000000000..d54415369
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item.xml
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_log_list_item"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Day group heading. Used to show a "today", "yesterday", "last week" or "other" heading
+ above a group of call log entries. -->
+ <TextView
+ android:id="@+id/call_log_day_group_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start"
+ android:layout_marginStart="@dimen/call_log_start_margin"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:fontFamily="sans-serif-medium"
+ android:textColor="@color/call_log_day_group_heading_color"
+ android:textSize="@dimen/call_log_day_group_heading_size"
+ android:paddingTop="@dimen/call_log_day_group_padding_top"
+ android:paddingBottom="@dimen/call_log_day_group_padding_bottom"/>
+
+ <android.support.v7.widget.CardView
+ android:id="@+id/call_log_row"
+ style="@style/CallLogCardStyle">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- Primary area containing the contact badge and caller information -->
+ <LinearLayout
+ android:id="@+id/primary_action_view"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/call_log_start_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin"
+ android:paddingTop="@dimen/call_log_vertical_padding"
+ android:paddingBottom="@dimen/call_log_vertical_padding"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:focusable="true"
+ android:nextFocusRight="@+id/call_back_action"
+ android:nextFocusLeft="@+id/quick_contact_photo">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:paddingTop="2dp"
+ android:nextFocusRight="@id/primary_action_view"
+ android:layout_gravity="top"
+ android:focusable="true"/>
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:gravity="center_vertical"
+ android:layout_marginStart="@dimen/call_log_list_item_info_margin_start">
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/call_log_name_margin_bottom"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:textColor="@color/call_log_primary_color"
+ android:textSize="@dimen/call_log_primary_text_size"
+ android:singleLine="true"/>
+
+ <LinearLayout
+ android:id="@+id/call_type"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <view
+ class="com.android.dialer.app.calllog.CallTypeIconsView"
+ android:id="@+id/call_type_icons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:layout_gravity="center_vertical"/>
+
+ <ImageView
+ android:id="@+id/work_profile_icon"
+ android:src="@drawable/ic_work_profile"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:scaleType="center"
+ android:visibility="gone"/>
+
+ <TextView
+ android:id="@+id/call_location_and_date"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:layout_gravity="center_vertical"
+ android:textColor="@color/call_log_detail_color"
+ android:textSize="@dimen/call_log_detail_text_size"
+ android:singleLine="true"/>
+
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/call_account_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_log_call_account_margin_bottom"
+ android:layout_marginEnd="@dimen/call_log_icon_margin"
+ android:textColor="?android:textColorSecondary"
+ android:textSize="@dimen/call_log_detail_text_size"
+ android:visibility="gone"
+ android:singleLine="true"/>
+
+ <TextView
+ android:id="@+id/voicemail_transcription"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_log_icon_margin"
+ android:textColor="@color/call_log_voicemail_transcript_color"
+ android:textSize="@dimen/call_log_voicemail_transcription_text_size"
+ android:ellipsize="marquee"
+ android:autoLink="all"
+ android:visibility="gone"
+ android:singleLine="false"
+ android:maxLines="10"/>
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/primary_action_button"
+ android:layout_width="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_height="@dimen/call_log_list_item_primary_action_dimen"
+ android:layout_gravity="center_vertical"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:scaleType="center"
+ android:tint="@color/call_log_list_item_primary_action_icon_tint"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <!-- Viewstub with additional expandable actions for a call log entry -->
+ <ViewStub
+ android:id="@+id/call_log_entry_actions_stub"
+ android:inflatedId="@+id/call_log_entry_actions"
+ android:layout="@layout/call_log_list_item_actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"/>
+
+ </LinearLayout>
+
+ </android.support.v7.widget.CardView>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml b/java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml
new file mode 100644
index 000000000..5b857afa0
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_log_action_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:importantForAccessibility="1"
+ android:orientation="vertical"
+ android:visibility="visible">
+
+ <com.android.dialer.app.voicemail.VoicemailPlaybackLayout
+ android:id="@+id/voicemail_playback_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="@color/call_log_action_divider"/>
+
+ <LinearLayout
+ android:id="@+id/call_action"
+ style="@style/CallLogActionStyle"
+ android:paddingTop="@dimen/call_log_actions_top_padding">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_call_24dp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/call_action_text"
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/description_call_log_call_action"/>
+
+ <TextView
+ android:id="@+id/call_type_or_location_text"
+ style="@style/CallLogActionSupportTextStyle"/>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/video_call_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/quantum_ic_videocam_white_24"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_video_call"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/create_new_contact_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_person_add_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/search_shortcut_create_new_contact"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/add_to_existing_contact_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_person_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/search_shortcut_add_to_contact"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/send_message_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_message_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_send_message"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/call_with_note_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_call_note_white_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_with_a_note"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/call_compose_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_phone_attach"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/share_and_call"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/report_not_spam_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_not_spam"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_remove_spam"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/block_report_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_block_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_block_report_number"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/block_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_block_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_block_number"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/unblock_action"
+ style="@style/CallLogActionStyle"
+ android:visibility="gone">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_unblock"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_unblock_number"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/details_action"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/ic_info_outline_24dp"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_details"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/share_voicemail"
+ android:visibility="gone"
+ style="@style/CallLogActionStyle">
+
+ <ImageView
+ style="@style/CallLogActionIconStyle"
+ android:src="@drawable/quantum_ic_send_black_24"/>
+
+ <TextView
+ style="@style/CallLogActionTextStyle"
+ android:text="@string/call_log_action_share_voicemail"/>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml b/java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml
new file mode 100644
index 000000000..e00529614
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Layout of a single item in the Dialer's "Dialpad chooser" UI. -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/icon"
+ android:layout_width="64dp"
+ android:layout_height="64dp"
+ android:scaleType="center"/>
+
+ <TextView
+ android:id="@+id/text"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textColor="@color/dialpad_primary_text_color"/>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/dialpad_fragment.xml b/java/com/android/dialer/app/res/layout/dialpad_fragment.xml
new file mode 100644
index 000000000..2cf198fcb
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/dialpad_fragment.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.dialer.app.dialpad.DialpadFragment$DialpadSlidingRelativeLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- spacer view -->
+ <View
+ android:id="@+id/spacer"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="#00000000"/>
+ <!-- Dialpad shadow -->
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/shadow_length"
+ android:background="@drawable/shadow_fade_up"/>
+ <include layout="@layout/dialpad_view"/>
+ <!-- "Dialpad chooser" UI, shown only when the user brings up the
+ Dialer while a call is already in progress.
+ When this UI is visible, the other Dialer elements
+ (the textfield/button and the dialpad) are hidden. -->
+ <ListView
+ android:id="@+id/dialpadChooser"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="@color/background_dialer_light"
+ android:visibility="gone"/>
+
+ </LinearLayout>
+
+ <!-- Margin bottom and alignParentBottom don't work well together, so use a Space instead. -->
+ <Space
+ android:id="@+id/dialpad_floating_action_button_margin_bottom"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/floating_action_button_margin_bottom"
+ android:layout_alignParentBottom="true"/>
+
+ <FrameLayout
+ android:id="@+id/dialpad_floating_action_button_container"
+ android:layout_width="@dimen/floating_action_button_width"
+ android:layout_height="@dimen/floating_action_button_height"
+ android:layout_above="@id/dialpad_floating_action_button_margin_bottom"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/fab_green">
+
+ <ImageButton
+ android:id="@+id/dialpad_floating_action_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/floating_action_button"
+ android:contentDescription="@string/description_dial_button"
+ android:src="@drawable/fab_ic_call"/>
+
+ </FrameLayout>
+
+</view>
diff --git a/java/com/android/dialer/app/res/layout/dialtacts_activity.xml b/java/com/android/dialer/app/res/layout/dialtacts_activity.xml
new file mode 100644
index 000000000..042b4a5e8
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/dialtacts_activity.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/dialtacts_mainlayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_light"
+ android:clipChildren="false"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@+id/dialtacts_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false">
+ <!-- The main contacts grid -->
+ <FrameLayout
+ android:id="@+id/dialtacts_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"/>
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/floating_action_button_container"
+ android:layout_width="@dimen/floating_action_button_width"
+ android:layout_height="@dimen/floating_action_button_height"
+ android:layout_marginBottom="@dimen/floating_action_button_margin_bottom"
+ android:layout_gravity="center_horizontal|bottom"
+ android:background="@drawable/dialer_fab"
+ app:layout_behavior="com.android.dialer.app.FloatingActionButtonBehavior">
+
+ <ImageButton
+ android:id="@+id/floating_action_button"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/floating_action_button"
+ android:contentDescription="@string/action_menu_dialpad_button"
+ android:src="@drawable/fab_ic_dial"/>
+
+ </FrameLayout>
+
+ <!-- Host container for the contact tile drag shadow -->
+ <FrameLayout
+ android:id="@+id/activity_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ImageView
+ android:id="@+id/contact_tile_drag_shadow_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:visibility="gone"/>
+ </FrameLayout>
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/java/com/android/dialer/app/res/layout/empty_content_view.xml b/java/com/android/dialer/app/res/layout/empty_content_view.xml
new file mode 100644
index 000000000..96a6a0262
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/empty_content_view.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/emptyListViewImage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"/>
+
+ <TextView
+ android:id="@+id/emptyListViewMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:gravity="center_horizontal|top"
+ android:textColor="@color/empty_list_text_color"
+ android:textSize="@dimen/empty_list_message_text_size"/>
+
+ <TextView
+ android:id="@+id/emptyListViewAction"
+ style="@style/TextActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="16dp"
+ android:paddingRight="16dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:gravity="center_horizontal"/>
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="40dp"/>
+
+</merge>
diff --git a/java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml b/java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml
new file mode 100644
index 000000000..e245aaca0
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+ <ImageView
+ android:id="@+id/emptyListViewImage"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:layout_width="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:gravity="center_horizontal" />
+
+ <TextView
+ android:id="@+id/emptyListViewMessage"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal|top"
+ android:textSize="@dimen/empty_list_message_text_size"
+ android:textColor="@color/empty_list_text_color"
+ android:paddingRight="16dp"
+ android:paddingLeft="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"/>
+
+ <TextView
+ android:id="@+id/emptyListViewAction"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:layout_gravity="center_horizontal"
+ android:paddingRight="16dp"
+ android:paddingLeft="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ style="@style/TextActionStyle" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="40dp" />
+
+</merge> \ No newline at end of file
diff --git a/java/com/android/dialer/app/res/layout/keyguard_preview.xml b/java/com/android/dialer/app/res/layout/keyguard_preview.xml
new file mode 100644
index 000000000..41fe89165
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/keyguard_preview.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="25dp"
+ android:background="@color/dialer_theme_color_dark"/>
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:background="#ffffff"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/lists_fragment.xml b/java/com/android/dialer/app/res/layout/lists_fragment.xml
new file mode 100644
index 000000000..442b428f2
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/lists_fragment.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lists_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:animateLayoutChanges="true">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <!-- TODO: Apply background color to ActionBar instead of a FrameLayout. For now, this is
+ the easiest way to preserve correct pane scrolling and searchbar collapse/expand
+ behaviors. -->
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/action_bar_height_large"
+ android:background="@color/actionbar_background_color"
+ android:elevation="@dimen/tab_elevation"/>
+
+ <com.android.contacts.common.list.ViewPagerTabs
+ android:id="@+id/lists_pager_header"
+ style="@style/DialtactsActionBarTabTextStyle"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/tab_height"
+ android:layout_gravity="top"
+ android:elevation="@dimen/tab_elevation"
+ android:orientation="horizontal"
+ android:textAllCaps="true"/>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/lists_pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+ <!-- Sets android:importantForAccessibility="no" to avoid being announced when navigating with
+ talkback enabled. It will still be announced when user drag or drop contact onto it.
+ This is required since drag and drop event is only sent to views are visible when drag
+ starts. -->
+ <com.android.dialer.app.list.RemoveView
+ android:id="@+id/remove_view"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/tab_height"
+ android:layout_marginTop="@dimen/action_bar_height_large"
+ android:contentDescription="@string/remove_contact"
+ android:importantForAccessibility="no">
+
+ <LinearLayout
+ android:id="@+id/remove_view_content"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/actionbar_background_color"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/remove_view_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:layout_marginBottom="8dp"
+ android:src="@drawable/ic_remove"
+ android:tint="@color/remove_text_color"/>
+
+ <TextView
+ android:id="@+id/remove_view_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/remove_contact"
+ android:textColor="@color/remove_text_color"
+ android:textSize="@dimen/remove_text_size"/>
+
+ </LinearLayout>
+
+ </com.android.dialer.app.list.RemoveView>
+
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml b/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
new file mode 100644
index 000000000..92b2e8e53
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<view
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/contact_tile"
+ class="com.android.dialer.app.list.PhoneFavoriteSquareTileView"
+ android:paddingBottom="@dimen/contact_tile_divider_width"
+ android:paddingEnd="@dimen/contact_tile_divider_width">
+
+ <RelativeLayout
+ android:id="@+id/contact_favorite_card"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:focusable="true"
+ android:nextFocusRight="@+id/contact_tile_secondary_button">
+
+ <com.android.contacts.common.widget.LayoutSuppressingImageView
+ android:id="@+id/contact_tile_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="centerCrop"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="6"/>
+ <View
+ android:id="@+id/shadow_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="4"
+ android:background="@drawable/shadow_contact_photo"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:paddingBottom="@dimen/contact_tile_text_bottom_padding"
+ android:paddingStart="@dimen/contact_tile_text_side_padding"
+ android:paddingEnd="@dimen/contact_tile_text_side_padding"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/contact_tile_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="3dip"
+ android:fontFamily="sans-serif-medium"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textColor="@color/contact_tile_name_color"
+ android:textSize="15sp"/>
+ <ImageView
+ android:id="@+id/contact_star_icon"
+ android:layout_width="@dimen/favorites_star_icon_size"
+ android:layout_height="@dimen/favorites_star_icon_size"
+ android:layout_marginStart="3dp"
+ android:src="@drawable/ic_star"
+ android:visibility="gone"/>
+ </LinearLayout>
+ <TextView
+ android:id="@+id/contact_tile_phone_type"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:fadingEdgeLength="3dip"
+ android:fontFamily="sans-serif"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:textAlignment="viewStart"
+ android:textColor="@color/contact_tile_name_color"
+ android:textSize="11sp"/>
+ </LinearLayout>
+
+ <View
+ android:id="@+id/contact_tile_push_state"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/item_background_material_dark"
+ android:importantForAccessibility="no"/>
+
+ <ImageButton
+ android:id="@id/contact_tile_secondary_button"
+ android:layout_width="@dimen/contact_tile_info_button_height_and_width"
+ android:layout_height="@dimen/contact_tile_info_button_height_and_width"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:paddingTop="8dp"
+ android:paddingBottom="4dp"
+ android:paddingStart="4dp"
+ android:paddingEnd="4dp"
+ android:paddingLeft="4dp"
+ android:paddingRight="9dp"
+ android:background="@drawable/item_background_material_dark"
+ android:contentDescription="@string/description_view_contact_detail"
+ android:scaleType="center"
+ android:src="@drawable/ic_more_vert_24dp"/>
+
+ </RelativeLayout>
+</view>
diff --git a/java/com/android/dialer/app/res/layout/search_edittext.xml b/java/com/android/dialer/app/res/layout/search_edittext.xml
new file mode 100644
index 000000000..1b4f9c4a4
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/search_edittext.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/search_view_container"
+ class="com.android.dialer.app.widget.SearchEditTextLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="@dimen/search_top_margin"
+ android:layout_marginBottom="@dimen/search_bottom_margin"
+ android:layout_marginLeft="@dimen/search_margin_horizontal"
+ android:layout_marginRight="@dimen/search_margin_horizontal"
+ android:background="@drawable/rounded_corner"
+ android:elevation="@dimen/search_box_elevation"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:id="@+id/search_box_collapsed"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/search_box_left_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/search_magnifying_glass"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="@dimen/search_box_icon_size"
+ android:padding="@dimen/search_box_search_icon_padding"
+ android:importantForAccessibility="no"
+ android:scaleType="center"
+ android:src="@drawable/ic_ab_search"
+ android:tint="@color/searchbox_icon_tint"/>
+
+ <TextView
+ android:id="@+id/search_box_start_search"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:layout_marginLeft="@dimen/search_box_collapsed_text_margin_left"
+ android:fontFamily="@string/search_font_family"
+ android:gravity="center_vertical"
+ android:hint="@string/dialer_hint_find_contact"
+ android:textColorHint="@color/searchbox_hint_text_color"
+ android:textSize="@dimen/search_collapsed_text_size"/>
+
+ <ImageView
+ android:id="@+id/voice_search_button"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/description_start_voice_search"
+ android:scaleType="center"
+ android:src="@drawable/ic_mic_grey600"
+ android:tint="@color/searchbox_icon_tint"/>
+
+ <ImageButton
+ android:id="@+id/dialtacts_options_menu_button"
+ android:layout_width="@dimen/search_box_icon_size"
+ android:layout_height="match_parent"
+ android:paddingEnd="@dimen/search_box_right_padding"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:contentDescription="@string/action_menu_overflow_description"
+ android:scaleType="center"
+ android:src="@drawable/ic_overflow_menu"
+ android:tint="@color/searchbox_icon_tint"/>
+
+ </LinearLayout>
+
+ <include layout="@layout/search_bar_expanded"/>
+
+</view>
diff --git a/java/com/android/dialer/app/res/layout/speed_dial_fragment.xml b/java/com/android/dialer/app/res/layout/speed_dial_fragment.xml
new file mode 100644
index 000000000..c778c6bc4
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/speed_dial_fragment.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false">
+
+ <FrameLayout
+ android:id="@+id/contact_tile_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:paddingStart="@dimen/favorites_row_start_padding"
+ android:paddingEnd="@dimen/favorites_row_end_padding">
+ <com.android.dialer.app.list.PhoneFavoriteListView
+ android:id="@+id/contact_tile_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="@dimen/favorites_row_top_padding"
+ android:paddingBottom="@dimen/floating_action_button_list_bottom_padding"
+ android:clipToPadding="false"
+ android:divider="@null"
+ android:fadingEdge="none"
+ android:nestedScrollingEnabled="true"
+ android:numColumns="@integer/contact_tile_column_count_in_favorites"/>
+ </FrameLayout>
+
+ <com.android.dialer.app.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+
+</FrameLayout>
diff --git a/java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml b/java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml
new file mode 100644
index 000000000..be691748a
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/blocked_number_background"
+ android:orientation="vertical">
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:divider="@null"
+ android:headerDividersEnabled="false"/>
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:background="@android:color/white">
+
+ <Button
+ android:id="@+id/import_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/blocked_number_container_padding"
+ android:layout_alignParentEnd="true"
+ android:text="@string/blocked_call_settings_import_button"/>
+
+ <Button
+ android:id="@+id/cancel_button"
+ style="@style/DialerFlatButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/import_description"
+ android:layout_toLeftOf="@id/import_button"
+ android:text="@android:string/cancel"/>
+
+ </RelativeLayout>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml b/java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml
new file mode 100644
index 000000000..7fff9d204
--- /dev/null
+++ b/java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="64dp"
+ android:layout_marginEnd="24dp"
+ android:background="@color/background_dialer_call_log_list_item"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/playback_state_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textSize="14sp"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/voicemail_playback_top_padding"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/playback_position_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:textSize="14sp"/>
+
+ <SeekBar
+ android:id="@+id/playback_seek"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:contentDescription="@string/description_playback_seek"
+ android:max="0"
+ android:progress="0"
+ android:progressDrawable="@drawable/seekbar_drawable"
+ android:thumb="@drawable/ic_voicemail_seek_handle"/>
+
+ <TextView
+ android:id="@+id/total_duration_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:textSize="14sp"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/playback_speakerphone"
+ style="@style/VoicemailPlaybackLayoutButtonStyle"
+ android:contentDescription="@string/description_playback_speakerphone"
+ android:src="@drawable/ic_volume_down_24dp"
+ android:tint="@color/voicemail_icon_tint"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/playback_start_stop"
+ style="@style/VoicemailPlaybackLayoutButtonStyle"
+ android:contentDescription="@string/voicemail_play_start_pause"
+ android:src="@drawable/ic_play_arrow"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <ImageButton
+ android:id="@+id/delete_voicemail"
+ style="@style/VoicemailPlaybackLayoutButtonStyle"
+ android:contentDescription="@string/call_log_trash_voicemail"
+ android:src="@drawable/ic_delete_24dp"
+ android:tint="@color/voicemail_icon_tint"/>
+
+ <Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/app/res/menu/dialpad_options.xml b/java/com/android/dialer/app/res/menu/dialpad_options.xml
new file mode 100644
index 000000000..2921ea3bb
--- /dev/null
+++ b/java/com/android/dialer/app/res/menu/dialpad_options.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_2s_pause"
+ android:showAsAction="withText"
+ android:title="@string/add_2sec_pause"/>
+ <item
+ android:id="@+id/menu_add_wait"
+ android:showAsAction="withText"
+ android:title="@string/add_wait"/>
+ <item
+ android:id="@+id/menu_call_with_note"
+ android:showAsAction="withText"
+ android:title="@string/call_with_a_note"/>
+</menu>
diff --git a/java/com/android/dialer/app/res/menu/dialtacts_options.xml b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
new file mode 100644
index 000000000..434aa81d9
--- /dev/null
+++ b/java/com/android/dialer/app/res/menu/dialtacts_options.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/menu_delete_all"
+ android:title="@string/call_log_delete_all"/>
+ <item
+ android:id="@+id/menu_clear_frequents"
+ android:title="@string/menu_clear_frequents"/>
+ <item
+ android:id="@+id/menu_call_settings"
+ android:title="@string/dialer_settings_label"/>
+
+</menu>
diff --git a/java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..15c41423b
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..3088f7502
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..e87de01fb
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..b866b79a7
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png b/java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png
new file mode 100644
index 000000000..26f51f153
--- /dev/null
+++ b/java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png
Binary files differ
diff --git a/java/com/android/dialer/app/res/values/animation_constants.xml b/java/com/android/dialer/app/res/values/animation_constants.xml
new file mode 100644
index 000000000..91230cd54
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/animation_constants.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="fade_duration">300</integer>
+
+ <!-- Swipe constants -->
+ <integer name="swipe_escape_velocity">100</integer>
+ <integer name="escape_animation_duration">200</integer>
+ <integer name="max_escape_animation_duration">400</integer>
+ <integer name="max_dismiss_velocity">2000</integer>
+ <integer name="snap_animation_duration">350</integer>
+ <integer name="swipe_scroll_slop">2</integer>
+ <dimen name="min_swipe">0dip</dimen>
+ <dimen name="min_vert">10dip</dimen>
+ <dimen name="min_lock">20dip</dimen>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/attrs.xml b/java/com/android/dialer/app/res/values/attrs.xml
new file mode 100644
index 000000000..b346390f7
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/attrs.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+
+ <declare-styleable name="SearchEditTextLayout"/>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
new file mode 100644
index 000000000..b88e55276
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -0,0 +1,115 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+-->
+
+<resources>
+ <color name="dialer_red_highlight_color">#ff1744</color>
+ <color name="dialer_green_highlight_color">#00c853</color>
+
+ <color name="dialer_button_text_color">#fff</color>
+ <color name="dialer_flat_button_text_color">@color/dialer_theme_color</color>
+
+ <!-- Color for the setting text. -->
+ <color name="setting_primary_color">@color/dialer_primary_text_color</color>
+ <!-- Color for the setting description text. -->
+ <color name="setting_secondary_color">@color/dialer_secondary_text_color</color>
+ <color name="setting_disabled_color">#aaaaaa</color>
+ <color name="setting_background_color">#ffffff</color>
+ <color name="setting_button_color">#eee</color>
+
+ <!-- 54% black -->
+ <color name="call_log_icon_tint">#8a000000</color>
+ <!-- 87% black -->
+ <color name="call_log_primary_color">#de000000</color>
+ <!-- 54% black -->
+ <color name="call_log_detail_color">#8a000000</color>
+ <!-- 87% black -->
+ <color name="call_log_voicemail_transcript_color">#de000000</color>
+ <!-- 70% black -->
+ <color name="call_log_action_color">#b3000000</color>
+ <!-- 54% black -->
+ <color name="call_log_day_group_heading_color">#8a000000</color>
+ <!-- 87% black-->
+ <color name="call_log_unread_text_color">#de000000</color>
+ <color name="call_log_list_item_primary_action_icon_tint">@color/call_log_icon_tint</color>
+
+ <color name="voicemail_icon_tint">@color/call_log_icon_tint</color>
+ <color name="voicemail_icon_disabled_tint">#80000000</color>
+ <color name="voicemail_playpause_icon_tint">@color/voicemail_icon_tint</color>
+ <!-- Colour of voicemail progress bar to the right of position indicator. -->
+ <color name="voicemail_playback_seek_bar_yet_to_play">#cecece</color>
+ <!-- Colour of voicemail progress bar to the left of position indicator. -->
+ <color name="voicemail_playback_seek_bar_already_played">@color/dialer_theme_color</color>
+
+ <!-- Background color of new dialer activity -->
+ <color name="background_dialer_light">#fafafa</color>
+ <!-- Background color for search results and call details -->
+ <color name="background_dialer_results">#f9f9f9</color>
+ <color name="background_dialer_call_log">@color/background_dialer_light</color>
+
+ <!-- Color of the 1dp divider that separates favorites -->
+ <color name="favorite_contacts_separator_color">#d0d0d0</color>
+
+ <!-- Color of the contact name in favorite tiles -->
+ <color name="contact_tile_name_color">#ffffff</color>
+
+ <color name="contact_list_name_text_color">@color/dialer_primary_text_color</color>
+
+ <!-- Undo dialogue color -->
+ <color name="undo_dialogue_text_color">#4d4d4d</color>
+
+ <color name="empty_list_text_color">#b2b2b2</color>
+
+ <color name="remove_text_color">#ffffff</color>
+
+ <!-- Text color for the "Remove" text when a contact is dragged on top of the remove view -->
+ <color name="remove_highlighted_text_color">#FF3F3B</color>
+
+ <!-- Color of the bottom border below the contacts grid on the main dialer screen. -->
+ <color name="contacts_grid_bottom_border_color">#16000000</color>
+
+ <!-- Color of actions in expanded call log entries. This text color represents actions such
+ as call back, play voicemail, etc. -->
+ <color name="call_log_action_text">@color/dialer_theme_color</color>
+
+ <!-- Color for missed call icons. -->
+ <color name="missed_call">#ff2e58</color>
+ <!-- Color for answered or outgoing call icons. -->
+ <color name="answered_call">@color/dialer_green_highlight_color</color>
+ <!-- Color for blocked call icons. -->
+ <color name="blocked_call">@color/dialer_secondary_text_color</color>
+
+ <color name="dialer_dialpad_touch_tint">@color/dialer_theme_color_20pct</color>
+
+ <color name="floating_action_button_touch_tint">#80ffffff</color>
+
+ <color name="call_log_action_divider">#eeeeee</color>
+ <color name="divider_line_color">#D8D8D8</color>
+
+ <!-- Colors for blocked numbers list -->
+ <color name="blocked_number_primary_text_color">@color/dialer_primary_text_color</color>
+ <color name="blocked_number_secondary_text_color">@color/dialer_secondary_text_color</color>
+ <color name="blocked_number_icon_tint">#616161</color>
+ <color name="blocked_number_background">#FFFFFF</color>
+ <color name="blocked_number_block_color">#F44336</color>
+ <color name="blocked_number_header_color">@color/dialer_theme_color</color>
+ <color name="blocked_number_disabled_emergency_header_color">#616161</color>
+ <color name="blocked_number_disabled_emergency_background_color">#E0E0E0</color>
+ <color name="add_blocked_number_icon_color">#bdbdbd</color>
+ <!-- Grey 700 -->
+ <color name="call_detail_footer_text_color">#616161</color>
+ <color name="call_detail_footer_icon_tint">@color/call_detail_footer_text_color</color>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/dimens.xml b/java/com/android/dialer/app/res/values/dimens.xml
new file mode 100644
index 000000000..f3fd63350
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/dimens.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+-->
+<resources>
+ <dimen name="button_horizontal_padding">16dp</dimen>
+ <dimen name="divider_line_thickness">1dp</dimen>
+
+ <!--
+ Drag to remove view (in dp because it is used in conjunction with a statically
+ sized icon
+ -->
+ <dimen name="remove_text_size">16dp</dimen>
+
+ <!-- Call Log -->
+ <dimen name="call_log_horizontal_margin">8dp</dimen>
+ <dimen name="call_log_call_action_size">32dp</dimen>
+ <dimen name="call_log_call_action_width">54dp</dimen>
+ <dimen name="call_log_icon_margin">4dp</dimen>
+ <dimen name="call_log_inner_margin">13dp</dimen>
+ <dimen name="call_log_outer_margin">8dp</dimen>
+ <dimen name="call_log_start_margin">8dp</dimen>
+ <dimen name="call_log_indent_margin">24dp</dimen>
+ <dimen name="call_log_name_margin_bottom">2dp</dimen>
+ <dimen name="call_log_call_account_margin_bottom">2dp</dimen>
+ <dimen name="call_log_vertical_padding">8dp</dimen>
+ <dimen name="call_log_list_item_height">56dp</dimen>
+ <dimen name="call_log_list_item_info_margin_start">16dp</dimen>
+ <dimen name="show_call_history_list_item_height">72dp</dimen>
+
+ <!-- Size of contact photos in the call log and call details. -->
+ <dimen name="contact_photo_size">48dp</dimen>
+ <dimen name="call_detail_button_spacing">2dip</dimen>
+ <dimen name="call_detail_horizontal_margin">20dp</dimen>
+ <dimen name="call_detail_top_margin">16dp</dimen>
+ <dimen name="call_detail_bottom_margin">16dp</dimen>
+ <dimen name="call_detail_header_top_margin">20dp</dimen>
+ <dimen name="call_detail_header_bottom_margin">9dp</dimen>
+ <dimen name="call_detail_elevation">0.5dp</dimen>
+ <dimen name="call_detail_action_item_padding_horizontal">28dp</dimen>
+ <dimen name="call_detail_action_item_padding_vertical">16dp</dimen>
+ <dimen name="call_detail_action_item_drawable_padding">28dp</dimen>
+ <dimen name="call_detail_action_item_text_size">16sp</dimen>
+ <dimen name="transcription_top_margin">18dp</dimen>
+ <dimen name="transcription_bottom_margin">18dp</dimen>
+
+ <!-- Size of call provider icon width and height -->
+ <dimen name="call_provider_small_icon_size">12dp</dimen>
+
+ <!-- Match call_button_height to Phone's dimens/in_call_end_button_height -->
+ <dimen name="call_button_height">74dp</dimen>
+
+ <!-- Dimensions for speed dial tiles -->
+ <dimen name="contact_tile_divider_width">1dp</dimen>
+ <dimen name="contact_tile_info_button_height_and_width">36dp</dimen>
+ <item name="contact_tile_height_to_width_ratio" type="dimen">76%</item>
+ <dimen name="contact_tile_text_side_padding">12dp</dimen>
+ <dimen name="contact_tile_text_bottom_padding">9dp</dimen>
+ <dimen name="favorites_row_top_padding">2dp</dimen>
+ <dimen name="favorites_row_bottom_padding">0dp</dimen>
+ <dimen name="favorites_row_start_padding">1dp</dimen>
+
+ <!-- Padding from the last contact tile will provide the end padding. -->
+ <dimen name="favorites_row_end_padding">0dp</dimen>
+ <dimen name="favorites_row_undo_text_side_padding">32dp</dimen>
+
+ <!-- Size of the star icon on the favorites tile. -->
+ <dimen name="favorites_star_icon_size">12dp</dimen>
+
+ <!-- Padding for the tooltip -->
+ <dimen name="dismiss_button_padding_start">20dip</dimen>
+ <dimen name="dismiss_button_padding_end">28dip</dimen>
+
+ <!-- Margin to the left and right of the search box. -->
+ <dimen name="search_margin_horizontal">8dp</dimen>
+ <!-- Margin above the search box. -->
+ <dimen name="search_top_margin">8dp</dimen>
+ <!-- Margin below the search box. -->
+ <dimen name="search_bottom_margin">8dp</dimen>
+ <dimen name="search_collapsed_text_size">14sp</dimen>
+ <!-- Search box interior padding - left -->
+ <dimen name="search_box_left_padding">8dp</dimen>
+ <!-- Search box interior padding - right -->
+ <dimen name="search_box_right_padding">8dp</dimen>
+ <dimen name="search_box_search_icon_padding">2dp</dimen>
+ <dimen name="search_box_collapsed_text_margin_left">22dp</dimen>
+ <dimen name="search_list_padding_top">16dp</dimen>
+ <dimen name="search_box_elevation">3dp</dimen>
+
+ <!-- Padding for icons to increase their touch target. Icons are typically 24 dps in size
+ so this extra padding makes the entire touch target 40dp -->
+ <dimen name="icon_padding">8dp</dimen>
+
+ <!-- Length of dialpad's shadows in dialer. -->
+ <dimen name="shadow_length">10dp</dimen>
+
+ <dimen name="empty_list_message_top_padding">20dp</dimen>
+ <dimen name="empty_list_message_text_size">16sp</dimen>
+
+ <!-- Dimensions for individual preference cards -->
+ <dimen name="preference_padding_top">16dp</dimen>
+ <dimen name="preference_padding_bottom">16dp</dimen>
+ <dimen name="preference_side_margin">16dp</dimen>
+ <dimen name="preference_summary_line_spacing_extra">4dp</dimen>
+
+ <dimen name="call_log_list_item_primary_action_dimen">48dp</dimen>
+
+ <!-- Dimensions for promo cards -->
+ <dimen name="promo_card_icon_size">24dp</dimen>
+ <dimen name="promo_card_start_padding">16dp</dimen>
+ <dimen name="promo_card_top_padding">21dp</dimen>
+ <dimen name="promo_card_main_padding">24dp</dimen>
+ <dimen name="promo_card_title_padding">12dp</dimen>
+ <dimen name="promo_card_action_vertical_padding">4dp</dimen>
+ <dimen name="promo_card_action_end_padding">4dp</dimen>
+ <dimen name="promo_card_action_between_padding">11dp</dimen>
+ <dimen name="promo_card_line_spacing">4dp</dimen>
+
+ <dimen name="voicemail_playback_top_padding">12dp</dimen>
+
+ <!-- Size of entries in blocked numbers list -->
+ <dimen name="blocked_number_container_padding">16dp</dimen>
+ <dimen name="blocked_number_horizontal_margin">16dp</dimen>
+ <dimen name="blocked_number_top_margin">16dp</dimen>
+ <dimen name="blocked_number_bottom_margin">16dp</dimen>
+ <dimen name="blocked_number_add_top_margin">8dp</dimen>
+ <dimen name="blocked_number_add_bottom_margin">8dp</dimen>
+ <dimen name="blocked_number_primary_text_size">16sp</dimen>
+ <dimen name="blocked_number_secondary_text_size">12sp</dimen>
+ <dimen name="blocked_number_delete_icon_size">32dp</dimen>
+ <dimen name="blocked_number_search_text_size">14sp</dimen>
+ <dimen name="blocked_number_settings_description_text_size">14sp</dimen>
+ <dimen name="blocked_number_header_height">48dp</dimen>
+
+ <dimen name="call_type_icon_size">12dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/donottranslate_config.xml b/java/com/android/dialer/app/res/values/donottranslate_config.xml
new file mode 100644
index 000000000..e7a8e6fc3
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/donottranslate_config.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <!-- If true, enable vibration (haptic feedback) for dialer key presses.
+ The pattern is set on a per-platform basis using config_virtualKeyVibePattern.
+ TODO: If enough users are annoyed by this, we might eventually
+ need to make it a user preference rather than a per-platform
+ resource. -->
+ <bool name="config_enable_dialer_key_vibration">true</bool>
+
+ <!-- If true, show an onscreen "Dial" button in the dialer.
+ In practice this is used on all platforms even the ones with hard SEND/END
+ keys, but for maximum flexibility it's controlled by a flag here
+ (which can be overridden on a per-product basis.) -->
+ <bool name="config_show_onscreen_dial_button">true</bool>
+
+ <!-- Regular expression for prohibiting certain phone numbers in dialpad.
+ Ignored if empty. -->
+ <string name="config_prohibited_phone_number_regexp"></string>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/ids.xml b/java/com/android/dialer/app/res/values/ids.xml
new file mode 100644
index 000000000..8566f26b6
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/ids.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <item name="call_detail_delete_menu_item" type="id"/>
+ <item name="context_menu_copy_to_clipboard" type="id"/>
+ <item name="context_menu_copy_transcript_to_clipboard" type="id"/>
+ <item name="context_menu_edit_before_call" type="id"/>
+ <item name="context_menu_block_report_spam" type="id"/>
+ <item name="context_menu_block" type="id"/>
+ <item name="context_menu_unblock" type="id"/>
+ <item name="context_menu_report_not_spam" type="id"/>
+ <item name="settings_header_sounds_and_vibration" type="id"/>
+ <item name="block_id" type="id"/>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/strings.xml b/java/com/android/dialer/app/res/values/strings.xml
new file mode 100644
index 000000000..689ee1ba8
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/strings.xml
@@ -0,0 +1,960 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Application name used in Settings/Apps. Default label for activities
+ that don't specify a label. -->
+ <string name="applicationLabel">Phone</string>
+
+ <!-- Title for the activity that dials the phone, when launched directly into the dialpad -->
+ <string name="launcherDialpadActivityLabel">Phone Keypad</string>
+ <!-- The description text for the dialer tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE] -->
+ <string name="dialerIconLabel">Phone</string>
+
+ <!-- The description text for the call log tab.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+
+ [CHAR LIMIT=NONE] -->
+ <string name="callHistoryIconLabel">Call history</string>
+
+ <!-- Text for a menu item to report a call as having been incorrectly identified. [CHAR LIMIT=48] -->
+ <string name="action_report_number">Report inaccurate number</string>
+
+ <!-- Option displayed in context menu to copy long pressed phone number. [CHAR LIMIT=48] -->
+ <string name="action_copy_number_text">Copy number</string>
+
+ <!-- Option displayed in context menu to copy long pressed voicemail transcription. [CHAR LIMIT=48] -->
+ <string name="copy_transcript_text">Copy transcription</string>
+
+ <!-- Label for action to block a number. [CHAR LIMIT=48] -->
+ <string name="action_block_number">Block number</string>
+
+ <!-- Label for action to unblock a number [CHAR LIMIT=48]-->
+ <string name="action_unblock_number">Unblock number</string>
+
+ <!-- Menu item in call details used to remove a call or voicemail from the call log. -->
+ <string name="call_details_delete">Delete</string>
+
+ <!-- Label for action to edit a number before calling it. [CHAR LIMIT=48] -->
+ <string name="action_edit_number_before_call">Edit number before call</string>
+
+ <!-- Menu item used to remove all calls from the call log -->
+ <string name="call_log_delete_all">Clear call history</string>
+
+ <!-- Menu item used to delete a voicemail. [CHAR LIMIT=30] -->
+ <string name="call_log_trash_voicemail">Delete voicemail</string>
+
+ <!-- Text for snackbar to undo a voicemail delete. [CHAR LIMIT=30] -->
+ <string name="snackbar_voicemail_deleted">Voicemail deleted</string>
+
+ <!-- Text for undo button in snackbar for voicemail deletion. [CHAR LIMIT=10] -->
+ <string name="snackbar_voicemail_deleted_undo">UNDO</string>
+
+ <!-- Title of the confirmation dialog for clearing the call log. [CHAR LIMIT=37] -->
+ <string name="clearCallLogConfirmation_title">Clear call history?</string>
+
+ <!-- Confirmation dialog for clearing the call log. [CHAR LIMIT=NONE] -->
+ <string name="clearCallLogConfirmation">This will delete all calls from your history</string>
+
+ <!-- Title of the "Clearing call log" progress-dialog [CHAR LIMIT=35] -->
+ <string name="clearCallLogProgress_title">Clearing call history\u2026</string>
+
+ <!-- Title used for the activity for placing a call. This name appears
+ in activity disambig dialogs -->
+ <string name="userCallActivityLabel" product="default">Phone</string>
+
+ <!-- Notification strings -->
+ <!-- Missed call notification label, used when there's exactly one missed call -->
+ <string name="notification_missedCallTitle">Missed call</string>
+ <!-- Missed call notification label, used when there's exactly one missed call from work contact -->
+ <string name="notification_missedWorkCallTitle">Missed work call</string>
+ <!-- Missed call notification label, used when there are two or more missed calls -->
+ <string name="notification_missedCallsTitle">Missed calls</string>
+ <!-- Missed call notification message used when there are multiple missed calls -->
+ <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string>
+ <!-- Message for "call back" Action, which is displayed in the missed call notificaiton.
+ The user will be able to call back to the person or the phone number.
+ [CHAR LIMIT=18] -->
+ <string name="notification_missedCall_call_back">Call back</string>
+ <!-- Message for "reply via sms" action, which is displayed in the missed call notification.
+ The user will be able to send text messages using the phone number.
+ [CHAR LIMIT=18] -->
+ <string name="notification_missedCall_message">Message</string>
+ <!-- Hardcoded number used for restricted incoming phone numbers. -->
+ <string name="handle_restricted" translatable="false">RESTRICTED</string>
+ <!-- Format for a post call message. (ex. John Doe: Give me a call when you're free.) -->
+ <string name="post_call_notification_message"><xliff:g id="name">%1$s</xliff:g>: <xliff:g id="message">%2$s</xliff:g></string>
+
+ <!-- Title of the notification of new voicemails. [CHAR LIMIT=30] -->
+ <plurals name="notification_voicemail_title">
+ <item quantity="one">Voicemail</item>
+ <item quantity="other">
+ <xliff:g id="count">%1$d</xliff:g>
+ Voicemails
+ </item>
+ </plurals>
+
+ <!-- Used in the notification of a new voicemail for the action to play the voicemail. -->
+ <string name="notification_action_voicemail_play">Play</string>
+
+ <!-- Used to build a list of names or phone numbers, to indicate the callers who left
+ voicemails.
+ The first argument may be one or more callers, the most recent ones.
+ The second argument is an additional callers.
+ This string is used to build a list of callers.
+
+ [CHAR LIMIT=10]
+ -->
+ <string name="notification_voicemail_callers_list"><xliff:g id="newer_callers">%1$s</xliff:g>,
+ <xliff:g id="older_caller">%2$s</xliff:g>
+ </string>
+
+ <!-- Text used in the ticker to notify the user of the latest voicemail. [CHAR LIMIT=30] -->
+ <string name="notification_new_voicemail_ticker">New voicemail from
+ <xliff:g id="caller">%1$s</xliff:g>
+ </string>
+
+ <!-- Message to show when there is an error playing back the voicemail. [CHAR LIMIT=40] -->
+ <string name="voicemail_playback_error">Couldn\'t play voicemail</string>
+
+ <!-- Message to display whilst we are waiting for the content to be fetched. [CHAR LIMIT=40] -->
+ <string name="voicemail_fetching_content">Loading voicemail\u2026</string>
+
+ <!-- Message to display whilst we are waiting for the content to be archived. [CHAR LIMIT=40] -->
+ <string name="voicemail_archiving_content">Archiving voicemail\u2026</string>
+
+ <!-- Message to display if we fail to get content within a suitable time period. [CHAR LIMIT=40] -->
+ <string name="voicemail_fetching_timout">Couldn\'t load voicemail</string>
+
+ <!-- The header to show that call log is only showing voicemail calls. [CHAR LIMIT=40] -->
+ <string name="call_log_voicemail_header">Calls with voicemail only</string>
+
+ <!-- The header to show that call log is only showing incoming calls. [CHAR LIMIT=40] -->
+ <string name="call_log_incoming_header">Incoming calls only</string>
+
+ <!-- The header to show that call log is only showing outgoing calls. [CHAR LIMIT=40] -->
+ <string name="call_log_outgoing_header">Outgoing calls only</string>
+
+ <!-- The header to show that call log is only showing missed calls. [CHAR LIMIT=40] -->
+ <string name="call_log_missed_header">Missed calls only</string>
+
+ <!-- The counter for calls in a group and the date of the latest call as shown in the call log [CHAR LIMIT=15] -->
+ <string name="call_log_item_count_and_date">(<xliff:g id="count">%1$d</xliff:g>)
+ <xliff:g id="date">%2$s</xliff:g>
+ </string>
+
+ <!-- String describing the Search ImageButton
+
+ Used by AccessibilityService to announce the purpose of the button.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_search_button">search</string>
+
+ <!-- String describing the Dial ImageButton
+
+ Used by AccessibilityService to announce the purpose of the button.
+ -->
+ <string name="description_dial_button">dial</string>
+
+ <!-- String describing the digits text box containing the number to dial.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_digits_edittext">number to dial</string>
+
+ <!-- String describing the button in the voicemail playback to start/stop playback.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_playback_start_stop">Play or stop playback</string>
+
+ <!-- String describing the button in the voicemail playback to switch on/off speakerphone.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_playback_speakerphone">Switch on or off speakerphone</string>
+
+ <!-- String describing the seekbar in the voicemail playback to seek playback position.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_playback_seek">Seek playback position</string>
+
+ <!-- String describing the button in the voicemail playback to decrease playback rate.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_rate_decrease">Decrease playback rate</string>
+
+ <!-- String describing the button in the voicemail playback to increase playback rate.
+
+ Used by AccessibilityService to announce the purpose of the view.
+ -->
+ <string name="description_rate_increase">Increase playback rate</string>
+
+ <!-- Content description for the fake action menu button that brings up the call history
+ activity -->
+ <string name="action_menu_call_history_description">Call history</string>
+
+ <!-- Content description for the fake action menu overflow button.
+ This should be same as the description for the real action menu
+ overflow button available in ActionBar.
+ [CHAR LIMIT=NONE] -->
+ <string msgid="2295659037509008453" name="action_menu_overflow_description">More options</string>
+
+ <!-- Content description for the button that displays the dialpad
+ [CHAR LIMIT=NONE] -->
+ <string name="action_menu_dialpad_button">key pad</string>
+
+ <!-- Menu item used to show only outgoing in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_outgoing_only">Show outgoing only</string>
+
+ <!-- Menu item used to show only incoming in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_incoming_only">Show incoming only</string>
+
+ <!-- Menu item used to show only missed in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_missed_only">Show missed only</string>
+
+ <!-- Menu item used to show only voicemails in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_voicemails_only">Show voicemails only</string>
+
+ <!-- Menu item used to show all calls in the call log. [CHAR LIMIT=30] -->
+ <string name="menu_show_all_calls">Show all calls</string>
+
+ <!-- Menu items for dialpad options as part of Pause and Wait ftr [CHAR LIMIT=30] -->
+ <string name="add_2sec_pause">Add 2-sec pause</string>
+ <string name="add_wait">Add wait</string>
+
+ <!-- Label for the dialer app setting page [CHAR LIMIT=30]-->
+ <string name="dialer_settings_label">Settings</string>
+
+ <!-- Menu item to display all contacts [CHAR LIMIT=30] -->
+ <string name="menu_allContacts">All contacts</string>
+
+ <!-- Title bar for call detail screen -->
+ <string name="callDetailTitle">Call details</string>
+
+ <!-- Toast for call detail screen when couldn't read the requested details -->
+ <string name="toast_call_detail_error">Details not available</string>
+
+ <!-- Item label: jump to the in-call DTMF dialpad.
+ (Part of a list of options shown in the dialer when another call
+ is already in progress.) -->
+ <string name="dialer_useDtmfDialpad">Use touch tone keypad</string>
+
+ <!-- Item label: jump to the in-call UI.
+ (Part of a list of options shown in the dialer when another call
+ is already in progress.) -->
+ <string name="dialer_returnToInCallScreen">Return to call in progress</string>
+
+ <!-- Item label: use the Dialer's keypad to add another call.
+ (Part of a list of options shown in the dialer when another call
+ is already in progress.) -->
+ <string name="dialer_addAnotherCall">Add call</string>
+
+ <!-- Title for incoming call type. [CHAR LIMIT=40] -->
+ <string name="type_incoming">Incoming call</string>
+
+ <!-- Title for incoming call which was transferred to another device. [CHAR LIMIT=60] -->
+ <string name="type_incoming_pulled">Incoming call transferred to another device</string>
+
+ <!-- Title for outgoing call type. [CHAR LIMIT=40] -->
+ <string name="type_outgoing">Outgoing call</string>
+
+ <!-- Title for outgoing call which was transferred to another device. [CHAR LIMIT=60] -->
+ <string name="type_outgoing_pulled">Outgoing call transferred to another device</string>
+
+ <!-- Title for missed call type. [CHAR LIMIT=40] -->
+ <string name="type_missed">Missed call</string>
+
+ <!-- Title for incoming video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_incoming_video">Incoming video call</string>
+
+ <!-- Title for incoming video call in call details screen which was transferred to another device.
+ [CHAR LIMIT=60] -->
+ <string name="type_incoming_video_pulled">Incoming video call transferred to another device</string>
+
+ <!-- Title for outgoing video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_outgoing_video">Outgoing video call</string>
+
+ <!-- Title for outgoing video call in call details screen which was transferred to another device.
+ [CHAR LIMIT=60] -->
+ <string name="type_outgoing_video_pulled">Outgoing video call transferred to another device</string>
+
+ <!-- Title for missed video call in call details screen [CHAR LIMIT=60] -->
+ <string name="type_missed_video">Missed video call</string>
+
+ <!-- Title for voicemail details screen -->
+ <string name="type_voicemail">Voicemail</string>
+
+ <!-- Title for rejected call type. [CHAR LIMIT=40] -->
+ <string name="type_rejected">Declined call</string>
+
+ <!-- Title for blocked call type. [CHAR LIMIT=40] -->
+ <string name="type_blocked">Blocked call</string>
+
+ <!-- Title for "answered elsewhere" call type. This will happen if a call was ringing
+ simultaneously on multiple devices, and the user answered it on a device other than the
+ current device. [CHAR LIMIT=60] -->
+ <string name="type_answered_elsewhere">Call answered on another device</string>
+
+ <!-- Description for incoming calls going to voice mail vs. not -->
+ <string name="actionIncomingCall">Incoming calls</string>
+
+ <!-- String describing the icon in the call log used to play a voicemail.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_call_log_play_button">Play voicemail</string>
+
+ <!-- String describing the button to view the contact for the current number.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_view_contact">View contact <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- String describing the button to call a number or contact.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_call">Call <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- String describing the button to access the contact details for a name or number.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_contact_details">Contact details for <xliff:g id="nameOrNumber">%1$s</xliff:g></string>
+
+ <!-- String describing the button to access the contact details for a name or number when the
+ when the number is a suspected spam.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_spam_contact_details">Contact details for suspected spam caller <xliff:g id="nameOrNumber">%1$s</xliff:g></string>
+
+ <!-- String indicating the number of calls to/from a caller in the call log.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ -->
+ <string name="description_num_calls"><xliff:g id="numberOfCalls">%1$s</xliff:g> calls.</string>
+
+ <!-- String indicating a call log entry had video capabilities.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_video_call">Video call.</string>
+
+ <!-- String describing the button to SMS a number or contact.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_send_text_message">Send SMS to <xliff:g id="name">%1$s</xliff:g></string>
+
+ <!-- String describing the icon in the call log used to represent an unheard voicemail left to
+ the user.
+
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ This is especially valuable for views without textual representation like ImageView.
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="description_call_log_unheard_voicemail">Unheard voicemail</string>
+
+ <!-- String describing the icon used to start a voice search -->
+ <string name="description_start_voice_search">Start voice search</string>
+
+ <!-- Menu item used to call a contact, containing the number of the contact to call -->
+ <string name="menu_callNumber">Call <xliff:g id="number">%s</xliff:g></string>
+
+ <!-- String used for displaying calls to the voicemail number in the call log -->
+ <string name="voicemail">Voicemail</string>
+
+ <!-- A nicely formatted call duration displayed when viewing call details for duration less than 1 minute. For example "28 sec" -->
+ <string name="callDetailsShortDurationFormat"><xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+
+ <!-- A nicely formatted call duration displayed when viewing call details. For example "42 min 28 sec" -->
+ <string name="callDetailsDurationFormat"><xliff:g example="42" id="minutes">%s</xliff:g> min <xliff:g example="28" id="seconds">%s</xliff:g> sec</string>
+
+ <!-- The string 'Today'. This value is used in the voicemailCallLogDateTimeFormat rather than an
+ explicit date string, e.g. Jul 25, 2014, in the event that a voicemail was created on the
+ current day -->
+ <string name="voicemailCallLogToday">@string/call_log_header_today</string>
+
+ <!-- A format string used for displaying the date and time for a voicemail call log. For example: Jul 25, 2014 at 2:49 PM
+ The date will be replaced by 'Today' for voicemails created on the current day. For example: Today at 2:49 PM -->
+ <string name="voicemailCallLogDateTimeFormat"><xliff:g example="Jul 25, 2014" id="date">%1$s</xliff:g> at <xliff:g example="2:49 PM" id="time">%2$s</xliff:g></string>
+
+ <!-- Format for duration of voicemails which are displayed when viewing voicemail logs. For example "01:22" -->
+ <string name="voicemailDurationFormat"><xliff:g example="10" id="minutes">%1$02d</xliff:g>:<xliff:g example="20" id="seconds">%2$02d</xliff:g></string>
+
+ <!-- A format string used for displaying the date, time and duration for a voicemail call log. For example: Jul 25, 2014 at 2:49 PM • 00:34 -->
+ <string name="voicemailCallLogDateTimeFormatWithDuration"><xliff:g example="Jul 25, 2014 at 2:49PM" id="dateAndTime">%1$s</xliff:g> \u2022 <xliff:g example="01:22" id="duration">%2$s</xliff:g></string>
+
+ <!-- Dialog message which is shown when the user tries to make a phone call
+ to prohibited phone numbers [CHAR LIMIT=NONE] -->
+ <string msgid="4313552620858880999" name="dialog_phone_call_prohibited_message">Can\'t call this number</string>
+
+ <!-- Dialog message which is shown when the user tries to check voicemail
+ while the system isn't ready for the access. [CHAR LIMIT=NONE] -->
+ <string name="dialog_voicemail_not_ready_message">To set up voicemail, go to Menu &gt; Settings.</string>
+
+ <!-- Dialog message which is shown when the user tries to check voicemail
+ while the system is in airplane mode. The user cannot access to
+ voicemail service in Airplane mode. [CHAR LIMI=NONE] -->
+ <string name="dialog_voicemail_airplane_mode_message">To call voicemail, first turn off Airplane mode.</string>
+
+ <!-- Message that appears in the favorites tab of the Phone app when the contact list has not fully loaded yet (below the favorite and frequent contacts) [CHAR LIMIT=20] -->
+ <string name="contact_list_loading">Loading\u2026</string>
+
+ <!-- The title of a dialog that displays the IMEI of the phone -->
+ <string name="imei">IMEI</string>
+
+ <!-- The title of a dialog that displays the MEID of the CDMA phone -->
+ <string name="meid">MEID</string>
+
+ <!-- Dialog text displayed when loading a phone number from the SIM card for speed dial -->
+ <string name="simContacts_emptyLoading">Loading from SIM card\u2026</string>
+
+ <!-- Dialog title displayed when loading a phone number from the SIM card for speed dial -->
+ <string name="simContacts_title">SIM card contacts</string>
+
+ <!-- Message displayed when there is no application available to handle the add contact menu option. [CHAR LIMIT=NONE] -->
+ <string name="add_contact_not_available">No contacts app available</string>
+
+ <!-- Message displayed when there is no application available to handle voice search. [CHAR LIMIT=NONE] -->
+ <string name="voice_search_not_available">Voice search not available</string>
+
+ <!-- Message displayed when the Phone application has been disabled and a phone call cannot
+ be made. [CHAR LIMIT=NONE] -->
+ <string name="call_not_available">Cannot make a phone call because the Phone application has been disabled.</string>
+
+ <!-- Hint displayed in dialer search box when there is no query that is currently typed.
+ [CHAR LIMIT=30] -->
+ <string name="dialer_hint_find_contact">Search contacts</string>
+
+ <!-- Hint displayed in add blocked number search box when there is no query typed.
+ [CHAR LIMIT=45] -->
+ <string name="block_number_search_hint">Add number or search contacts</string>
+
+ <!-- String resource for the font-family to use for the call log activity's title -->
+ <string name="call_log_activity_title_font_family" translatable="false">sans-serif-light</string>
+
+ <!-- String resource for the font-family to use for the full call history footer -->
+ <string name="view_full_call_history_font_family" translatable="false">sans-serif</string>
+
+ <!-- Text displayed when the call log is empty. -->
+ <string name="call_log_all_empty">Your call history is empty</string>
+
+ <!-- Label of the button displayed when the call history is empty. Allows the user to make a call. -->
+ <string name="call_log_all_empty_action">Make a call</string>
+
+ <!-- Text displayed when the list of missed calls is empty -->
+ <string name="call_log_missed_empty">You have no missed calls.</string>
+
+ <!-- Text displayed when the list of voicemails is empty -->
+ <string name="call_log_voicemail_empty">Your voicemail inbox is empty.</string>
+
+ <!-- Menu option to show favorite contacts only -->
+ <string name="show_favorites_only">Show favorites only</string>
+
+ <!-- Title of activity that displays a list of all calls -->
+ <string name="call_log_activity_title">Call History</string>
+
+ <!-- Title for the call log tab containing the list of all voicemails and calls
+ [CHAR LIMIT=30] -->
+ <string name="call_log_all_title">All</string>
+
+ <!-- Title for the call log tab containing the list of all missed calls only
+ [CHAR LIMIT=30] -->
+ <string name="call_log_missed_title">Missed</string>
+
+ <!-- Title for the call log tab containing the list of all voicemail calls only
+ [CHAR LIMIT=30] -->
+ <string name="call_log_voicemail_title">Voicemail</string>
+
+ <!-- Accessibility text for the tab showing recent and favorite contacts who can be called.
+ [CHAR LIMIT=40] -->
+ <string name="tab_speed_dial">Speed dial</string>
+
+ <!-- Accessibility text for the tab showing the call history. [CHAR LIMIT=40] -->
+ <string name="tab_history">Call History</string>
+
+ <!-- Accessibility text for the tab showing the user's contacts. [CHAR LIMIT=40] -->
+ <string name="tab_all_contacts">Contacts</string>
+
+ <!-- Accessibility text for the tab showing the user's voicemails. [CHAR LIMIT=40] -->
+ <string name="tab_voicemail">Voicemail</string>
+
+ <!-- Text displayed when user swipes out a favorite contact -->
+ <string name="favorite_hidden">Removed from favorites</string>
+ <!-- Text displayed for the undo button to undo removing a favorite contact -->
+ <string name="favorite_hidden_undo">Undo</string>
+
+ <!-- Shortcut item used to call a number directly from search -->
+ <string name="search_shortcut_call_number">Call
+ <xliff:g id="number">%s</xliff:g>
+ </string>
+
+ <!-- Shortcut item used to add a number directly to a new contact from search.
+ [CHAR LIMIT=25] -->
+ <string name="search_shortcut_create_new_contact">Create new contact</string>
+
+ <!-- Shortcut item used to add a number to an existing contact directly from search.
+ [CHAR LIMIT=25] -->
+ <string name="search_shortcut_add_to_contact">Add to a contact</string>
+
+ <!-- Shortcut item used to send a text message directly from search. [CHAR LIMIT=25] -->
+ <string name="search_shortcut_send_sms_message">Send SMS</string>
+
+ <!-- Shortcut item used to make a video call directly from search. [CHAR LIMIT=25] -->
+ <string name="search_shortcut_make_video_call">Make video call</string>
+
+ <!-- Shortcut item used to block a number directly from search. [CHAR LIMIT=25] -->
+ <string name="search_shortcut_block_number">Block number</string>
+
+ <!-- Number of missed calls shown on call card [CHAR LIMIT=40] -->
+ <string name="num_missed_calls"><xliff:g id="number">%s</xliff:g> new missed calls</string>
+
+ <!-- Shown when there are no speed dial favorites. -->
+ <string name="speed_dial_empty">No one is on your speed dial yet</string>
+
+ <!-- Shown as an action when there are no speed dial favorites -->
+ <string name="speed_dial_empty_add_favorite_action">Add a favorite</string>
+
+ <!-- Shown when there are no contacts in the all contacts list. -->
+ <string name="all_contacts_empty">You don\'t have any contacts yet</string>
+
+ <!-- Shown as an action when the all contacts list is empty -->
+ <string name="all_contacts_empty_add_contact_action">Add a contact</string>
+
+ <!-- Shows up as a tooltip to provide a hint to the user that the profile pic in a contact
+ card can be tapped to bring up a list of all numbers, or long pressed to start reordering
+ [CHAR LIMIT=NONE]
+ -->
+ <string name="contact_tooltip">Touch image to see all numbers or touch &amp; hold to reorder</string>
+
+ <!-- Remove button that shows up when contact is long-pressed. [CHAR LIMIT=NONE] -->
+ <string name="remove_contact">Remove</string>
+
+ <!-- Button text for the "video call" displayed underneath an entry in the call log.
+ Tapping causes a video call to be placed to the caller represented by the call log entry.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_video_call">Video call</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which opens up a
+ messaging app to send a SMS to the number represented by the call log entry.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_send_message">Send a message</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which opens up the
+ call compose UI for the number represented by the call log entry.
+ [CHAR LIMIT=30] -->
+ <string name="share_and_call">Share and call</string>
+
+ <!-- Button text for the button displayed underneath an entry in the call log.
+ Tapping navigates the user to the call details screen where the user can view details for
+ the call log entry. [CHAR LIMIT=30] -->
+ <string name="call_log_action_details">Call details</string>
+
+ <!-- Button text for the button displayed underneath an entry in the call log.
+ Tapping opens dialog to share voicemail archive with other apps. [CHAR LIMIT=30] -->
+ <string name="call_log_action_share_voicemail">Send to &#8230;</string>
+
+ <!-- Button text for the button displayed underneath an entry in the call log, which when
+ tapped triggers a return call to the named user. [CHAR LIMIT=30] -->
+ <string name="call_log_action_call">
+ Call <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing an incoming missed call entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_incoming_missed_call">Missed call from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing an incoming answered call entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_incoming_answered_call">Answered call from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing an "unread" voicemail entry in the voicemails tab.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_unread_voicemail">Unread voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing a "read" voicemail entry in the voicemails tab.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_read_voicemail">Voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing an outgoing call entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_outgoing_call">Call to <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>, <xliff:g example="Mobile" id="typeOrLocation">^2</xliff:g>, <xliff:g example="2 min ago" id="timeOfCall">^3</xliff:g>, <xliff:g example="on SIM 1" id="phoneAccount">^4</xliff:g>.</string>
+
+ <!-- String describing the phone account the call was made on or to. This string will be used
+ in description_incoming_missed_call, description_incoming_answered_call, and
+ description_outgoing_call.
+ Note: AccessibilityServices uses this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">^1</xliff:g></string>
+
+ <!-- String describing the secondary line number the call was received via.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
+
+ <!-- TextView text item showing the secondary line number the call was received via.
+ [CHAR LIMIT=NONE]-->
+ <string name="call_log_via_number">via <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g></string>
+
+ <!-- String describing the PhoneAccount and via number that a call was received on, if both are
+ visible.
+ Note: AccessibilityServices use this attribute to announce what the view represents.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_via_number_phone_account">on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>, via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
+
+ <!-- The order of the PhoneAccount and via number that a call was received on,
+ if both are visible.
+ [CHAR LIMIT=NONE]-->
+ <string name="call_log_via_number_phone_account"><xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g> via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g></string>
+
+ <!-- String describing the phone icon on a call log list item. When tapped, it will place a
+ call to the number represented by that call log entry. [CHAR LIMIT=NONE]-->
+ <string name="description_call_log_call_action">Call</string>
+
+ <!-- String describing the "call" action for an entry in the call log. The call back
+ action triggers a return call to the named user.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_call_action">
+ Call <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the "video call" action for an entry in the call log. The video call
+ action triggers a return video call to the named person/number.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_video_call_action">
+ Video call <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>.
+ </string>
+
+ <!-- String describing the "listen" action for an entry in the call log. The listen
+ action is shown for call log entries representing a voicemail message and this button
+ triggers playing back the voicemail.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_action">
+ Listen to voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the "play voicemail" action for an entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_play">
+ Play voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the "pause voicemail" action for an entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_pause">
+ Pause voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+
+ <!-- String describing the "delete voicemail" action for an entry in the call log.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_voicemail_delete">
+ Delete voicemail from <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- String describing the number of new voicemails, displayed as a number badge on a tab.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <plurals name="description_voicemail_unread">
+ <item quantity="one"><xliff:g id="count">%d</xliff:g> new voicemail</item>
+ <item quantity="other"><xliff:g id="count">%d</xliff:g> new voicemails</item>
+ </plurals>
+
+ <!-- Description for the "create new contact" action for an entry in the call log. This action
+ opens a screen for creating a new contact for this name or number. [CHAR LIMIT=NONE] -->
+ <string name="description_create_new_contact_action">
+ Create contact for <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- Description for the "add to existing contact" action for an entry in the call log. This
+ action opens a screen for adding this name or number to an existing contact.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_add_to_existing_contact_action">
+ Add <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g> to existing contact
+ </string>
+
+ <!-- String describing the "details" action for an entry in the call log. The details action
+ displays the call details screen for an entry in the call log. This shows the calls to
+ and from the specified number associated with the call log entry.
+ [CHAR LIMIT=NONE] -->
+ <string name="description_details_action">
+ Call details for <xliff:g example="John Smith" id="nameOrNumber">^1</xliff:g>
+ </string>
+
+ <!-- Toast message which appears when a call log entry is deleted.
+ [CHAR LIMIT=NONE] -->
+ <string name="toast_entry_removed">Deleted from call history</string>
+
+ <!-- String used as a header in the call log above calls which occurred today.
+ [CHAR LIMIT=65] -->
+ <string name="call_log_header_today">Today</string>
+
+ <!-- String used as a header in the call log above calls which occurred yesterday.
+ [CHAR LIMIT=65] -->
+ <string name="call_log_header_yesterday">Yesterday</string>
+
+ <!-- String used as a header in the call log above calls which occurred two days or more ago.
+ [CHAR LIMIT=65] -->
+ <string name="call_log_header_other">Older</string>
+
+ <!-- String a header on the call details screen. Appears above the list calls to or from a
+ particular number.
+ [CHAR LIMIT=65] -->
+ <string name="call_detail_list_header">Calls list</string>
+
+ <!-- String describing the "speaker on" button on the playback control used to listen to a
+ voicemail message. When speaker is on, playback of the voicemail will occur through the
+ phone speaker.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_speaker_on">Turn speaker on.</string>
+
+ <!-- String describing the "speaker off" button on the playback control used to listen to a
+ voicemail message. When speaker is off, playback of the voicemail will occur through the
+ phone earpiece.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_speaker_off">Turn speaker off.</string>
+
+ <!-- String describing the "play faster" button in the playback control used to listen to a
+ voicemail message. Speeds up playback of the voicemail message.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_play_faster">Play faster.</string>
+
+ <!-- String describing the "play slower" button in the playback control used to listen to a
+ voicemail message. Slows down playback of the voicemail message.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_play_slower">Play slower.</string>
+
+ <!-- String describing the "play/pause" button in the playback control used to listen to a
+ voicemail message. Starts playback or pauses ongoing playback.
+ Note: AccessibilityServices uses this attribute to announce the purpose of the button.
+ [CHAR LIMIT=NONE] -->
+ <string name="voicemail_play_start_pause">Start or pause playback.</string>
+
+ <!-- Dialer settings related strings-->
+
+ <!-- Title for "Display options" category, which controls how contacts are shown.
+ [CHAR LIMIT=40] -->
+ <string name="display_options_title">Display options</string>
+
+ <!-- Title for the "Sounds and vibration" settings control settings related to ringtones,
+ dialpad tones, and vibration for incoming calls. [CHAR LIMIT=40] -->
+ <string name="sounds_and_vibration_title">Sounds and vibration</string>
+
+ <!-- Title for "Accessibility" category, which controls settings such as TTY mode and hearing
+ aid compatability. [CHAR LIMIT=40] -->
+ <string name="accessibility_settings_title">Accessibility</string>
+
+ <!-- Setting option name to pick ringtone (a list dialog comes up). [CHAR LIMIT=30] -->
+ <string name="ringtone_title">Phone ringtone</string>
+
+ <!-- Setting option name to enable or disable vibration when ringing the phone.
+ [CHAR LIMIT=30] -->
+ <string name="vibrate_on_ring_title">"Also vibrate for calls</string>
+
+ <!-- Setting option name to enable or disable DTMF tone sound [CHAR LIMIT=30] -->
+ <string name="dtmf_tone_enable_title">Keypad tones</string>
+ <!-- Label for setting to adjust the length of DTMF tone sounds. [CHAR LIMIT=40] -->
+ <string name="dtmf_tone_length_title">Keypad tone length</string>
+ <!-- Options displayed for the length of DTMF tone sounds. [CHAR LIMIT=40] -->
+ <string-array name="dtmf_tone_length_entries">
+ <item>Normal</item>
+ <item>Long</item>
+ </string-array>
+ <string-array name="dtmf_tone_length_entry_values" translatable="false">
+ <item>0</item>
+ <item>1</item>
+ </string-array>
+
+ <!-- Title of settings screen for managing the "Respond via SMS" feature. [CHAR LIMIT=30] -->
+ <string name="respond_via_sms_setting_title">Quick responses</string>
+
+ <!-- Label for the call settings section [CHAR LIMIT=30] -->
+ <string name="call_settings_label">Calls</string>
+
+ <!-- Label for the blocked numbers settings section [CHAR LIMIT=30] -->
+ <string name="manage_blocked_numbers_label">Call blocking</string>
+
+ <!-- Label for a section describing that call blocking is temporarily disabled because an
+ emergency call was made. [CHAR LIMIT=50] -->
+ <string name="blocked_numbers_disabled_emergency_header_label">
+ Call blocking temporarily off
+ </string>
+
+ <!-- Description that call blocking is temporarily disabled because the user called an
+ emergency number, and explains that call blocking will be re-enabled after a buffer
+ period has passed. [CHAR LIMIT=NONE] -->
+ <string name="blocked_numbers_disabled_emergency_desc">
+ Call blocking has been disabled because you contacted emergency services from this phone
+ within the last 48 hours. It will be automatically reenabled once the 48 hour period
+ expires.
+ </string>
+
+ <!-- Label for fragment to import numbers from contacts marked as send to voicemail.
+ [CHAR_LIMIT=30] -->
+ <string name="import_send_to_voicemail_numbers_label">Import numbers</string>
+
+ <!-- Text informing the user they have previously marked contacts to be sent to voicemail.
+ This will be followed by two buttons, 1) to view who is marked to be sent to voicemail
+ and 2) importing these settings to Dialer's block list. [CHAR LIMIT=NONE] -->
+ <string name="blocked_call_settings_import_description">
+ You previously marked some callers to be automatically sent to voicemail via other apps.
+ </string>
+
+ <!-- Label for button to view numbers of contacts previous marked to be sent to voicemail.
+ [CHAR_LIMIT=20] -->
+ <string name="blocked_call_settings_view_numbers_button">View Numbers</string>
+
+ <!-- Label for button to import settings for sending contacts to voicemail into Dialer's block
+ list. [CHAR_LIMIT=20] -->
+ <string name="blocked_call_settings_import_button">Import</string>
+
+ <!-- String describing the delete icon on a blocked number list item.
+ When tapped, it will show a dialog confirming the unblocking of the number.
+ [CHAR LIMIT=NONE]-->
+ <string name="description_blocked_number_list_delete">Unblock number</string>
+
+ <!-- Button to bring up UI to add a number to the blocked call list. [CHAR LIMIT=40] -->
+ <string name="addBlockedNumber">Add number</string>
+
+ <!-- Footer message of number blocking screen with visual voicemail active.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_footer_message_vvm">
+ Calls from these numbers will be blocked and voicemails will be automatically deleted.
+ </string>
+
+ <!-- Footer message of number blocking screen with no visual voicemail.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_footer_message_no_vvm">
+ Calls from these numbers will be blocked, but they may still be able to leave you voicemails.
+ </string>
+
+ <!-- Heading for the block list in the "Spam and blocked cal)ls" settings. [CHAR LIMIT=64] -->
+ <string name="block_list">Blocked numbers</string>
+
+ <!-- Error message shown when user tries to add a number to the block list that was already
+ blocked. [CHAR LIMIT=64] -->
+ <string name="alreadyBlocked"><xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>
+ is already blocked.</string>
+
+ <!-- Label for the phone account settings [CHAR LIMIT=30] -->
+ <string name="phone_account_settings_label">Calling accounts</string>
+
+ <!-- Internal key for ringtone preference. -->
+ <string name="ringtone_preference_key" translatable="false">button_ringtone_key</string>
+ <!-- Internal key for vibrate when ringing preference. -->
+ <string name="vibrate_on_preference_key" translatable="false">button_vibrate_on_ring</string>
+ <!-- Internal key for vibrate when ringing preference. -->
+ <string name="play_dtmf_preference_key" translatable="false">button_play_dtmf_tone</string>
+ <!-- Internal key for DTMF tone length preference. -->
+ <string name="dtmf_tone_length_preference_key" translatable="false">button_dtmf_settings</string>
+
+ <!-- The label of the button used to turn on a single permission [CHAR LIMIT=30]-->
+ <string name="permission_single_turn_on">Turn on</string>
+
+ <!-- The label of the button used to turn on multiple permissions [CHAR LIMIT=30]-->
+ <string name="permission_multiple_turn_on">Set permissions</string>
+
+ <!-- Shown as a prompt to turn on the contacts permission to enable speed dial [CHAR LIMIT=NONE]-->
+ <string name="permission_no_speeddial">To enable speed dial, turn on the Contacts permission.</string>
+
+ <!-- Shown as a prompt to turn on the phone permission to enable the call log [CHAR LIMIT=NONE]-->
+ <string name="permission_no_calllog">To see your call log, turn on the Phone permission.</string>
+
+ <!-- Shown as a prompt to turn on the contacts permission to show all contacts [CHAR LIMIT=NONE]-->
+ <string name="permission_no_contacts">To see your contacts, turn on the Contacts permission.</string>
+
+ <!-- Shown as a prompt to turn on the phone permission to show voicemails [CHAR LIMIT=NONE]-->
+ <string name="permission_no_voicemail">To access your voicemail, turn on the Phone permission.</string>
+
+ <!-- Shown as a prompt to turn on contacts permissions to allow contact search [CHAR LIMIT=NONE]-->
+ <string name="permission_no_search">To search your contacts, turn on the Contacts permissions.</string>
+
+ <!-- Shown as a prompt to turn on the phone permission to allow a call to be placed [CHAR LIMIT=NONE]-->
+ <string name="permission_place_call">To place a call, turn on the Phone permission.</string>
+
+ <!-- Shown as a message that notifies the user that the Phone app cannot write to system settings, which is why the system settings app is being launched directly instead. [CHAR LIMIT=NONE]-->
+ <string name="toast_cannot_write_system_settings">Phone app does not have permission to write to system settings.</string>
+
+ <!-- Label under the name of a blocked number in the call log. [CHAR LIMIT=15] -->
+ <string name="blocked_number_call_log_label">Blocked</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which marks the
+ phone number represented by the call log entry as a Spam number.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_block_report_number">Block/report spam</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which marks the
+ phone number represented by the call log entry as a Spam number.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_block_number">Block number</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which removes the
+ phone number represented by the call log entry from the Spam numbers list.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_remove_spam">Not spam</string>
+
+ <!-- Button text for a button displayed underneath an entry in the call log, which removes the
+ phone number represented by the call log entry from the blacklisted numbers.
+ [CHAR LIMIT=30] -->
+ <string name="call_log_action_unblock_number">Unblock number</string>
+
+ <!-- Label under the name of a spam number in the call log. [CHAR LIMIT=15] -->
+ <string name="spam_number_call_log_label">Spam</string>
+
+ <!-- Shown as a message that notifies the user enriched calling isn't working -->
+ <string name="call_composer_connection_failed"><xliff:g id="feature">%1$s</xliff:g> unavailable right now</string>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml
new file mode 100644
index 000000000..ac4422ba2
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/styles.xml
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+
+ <style name="DialtactsTheme" parent="DialerThemeBase">
+
+ <!-- Style for the overflow button in the actionbar. -->
+ <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflow</item>
+ <item name="actionOverflowButtonStyle">@style/DialtactsActionBarOverflow</item>
+
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:windowActionBarOverlay">true</item>
+ <item name="windowActionBarOverlay">true</item>
+ <item name="android:windowActionModeOverlay">true</item>
+ <item name="windowActionModeOverlay">true</item>
+ <item name="android:actionBarStyle">@style/DialtactsActionBarStyle</item>
+ <item name="actionBarStyle">@style/DialtactsActionBarStyle</item>
+
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:overlapAnchor">true</item>
+ <item name="android:homeAsUpIndicator">@drawable/ic_back_arrow</item>
+
+ <item name="android:listViewStyle">@style/ListViewStyle</item>
+ <item name="activated_background">@drawable/list_item_activated_background</item>
+ <item name="section_header_background">@drawable/list_title_holo</item>
+ <item name="list_section_header_height">32dip</item>
+ <item name="list_item_padding_top">7dp</item>
+ <item name="list_item_padding_right">24dp</item>
+ <item name="list_item_padding_bottom">7dp</item>
+ <item name="list_item_padding_left">16dp</item>
+ <item name="list_item_gap_between_image_and_text">
+ @dimen/contact_browser_list_item_gap_between_image_and_text
+ </item>
+ <item name="list_item_gap_between_label_and_data">8dip</item>
+ <item name="list_item_presence_icon_margin">4dip</item>
+ <item name="list_item_presence_icon_size">16dip</item>
+ <item name="list_item_photo_size">@dimen/contact_browser_list_item_photo_size</item>
+ <item name="list_item_profile_photo_size">70dip</item>
+ <item name="list_item_prefix_highlight_color">@color/people_app_theme_color</item>
+ <item name="list_item_background_color">@color/background_dialer_light</item>
+ <item name="list_item_header_text_indent">8dip</item>
+ <item name="list_item_header_text_color">@color/dialer_secondary_text_color</item>
+ <item name="list_item_header_text_size">14sp</item>
+ <item name="list_item_header_height">30dip</item>
+ <item name="list_item_data_width_weight">5</item>
+ <item name="list_item_label_width_weight">3</item>
+ <item name="contact_browser_list_padding_left">0dp</item>
+ <item name="contact_browser_list_padding_right">0dp</item>
+ <item name="contact_browser_background">@color/background_dialer_results</item>
+ <item name="list_item_name_text_color">@color/contact_list_name_text_color</item>
+ <item name="list_item_name_text_size">16sp</item>
+ <item name="list_item_text_indent">@dimen/contact_browser_list_item_text_indent</item>
+ <item name="list_item_text_offset_top">-2dp</item>
+ <!-- Favorites -->
+ <item name="favorites_padding_bottom">?android:attr/actionBarSize</item>
+ <item name="dialpad_key_button_touch_tint">@color/dialer_dialpad_touch_tint</item>
+ <item name="android:textAppearanceButton">@style/DialerButtonTextStyle</item>
+
+ <!-- Video call icon -->
+ <item name="list_item_video_call_icon_size">32dip</item>
+ <item name="list_item_video_call_icon_margin">8dip</item>
+
+ <item name="dialpad_style">@style/Dialpad.Light</item>
+ </style>
+
+ <!-- Action bar overflow menu icon. -->
+ <style name="DialtactsActionBarOverflow"
+ parent="@android:style/Widget.Material.Light.ActionButton.Overflow">
+ <item name="android:src">@drawable/ic_overflow_menu</item>
+ </style>
+
+ <!-- Action bar overflow menu icon. White with no shadow. -->
+ <style name="DialtactsActionBarOverflowWhite"
+ parent="@android:style/Widget.Material.Light.ActionButton.Overflow">
+ <item name="android:src">@drawable/overflow_menu</item>
+ </style>
+
+ <style name="DialpadTheme" parent="DialtactsTheme">
+ <item name="android:textColorPrimary">#FFFFFF</item>
+ </style>
+
+ <style name="DialtactsThemeWithoutActionBarOverlay" parent="DialtactsTheme">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:windowActionBarOverlay">false</item>
+ <item name="windowActionBarOverlay">false</item>
+ <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
+ <item name="actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
+ </style>
+
+ <!-- Hide the actionbar title during the activity preview -->
+ <style name="DialtactsActivityTheme" parent="DialtactsTheme">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:actionBarStyle">@style/DialtactsActionBarWithoutTitleStyle</item>
+ <item name="actionBarStyle">@style/DialtactsActionBarWithoutTitleStyle</item>
+
+ <item name="android:fastScrollThumbDrawable">@drawable/fastscroll_thumb</item>
+ <item name="android:fastScrollTrackDrawable">@null</item>
+ </style>
+
+ <style name="CallDetailActivityTheme" parent="DialtactsThemeWithoutActionBarOverlay">
+ <item name="android:windowBackground">@color/background_dialer_results</item>
+ <item name="android:actionOverflowButtonStyle">@style/DialtactsActionBarOverflowWhite</item>
+ </style>
+
+ <style name="CallDetailActionItemStyle">
+ <item name="android:foreground">?android:attr/selectableItemBackground</item>
+ <item name="android:clickable">true</item>
+ <item name="android:drawablePadding">@dimen/call_detail_action_item_drawable_padding</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:paddingStart">@dimen/call_detail_action_item_padding_horizontal</item>
+ <item name="android:paddingEnd">@dimen/call_detail_action_item_padding_horizontal</item>
+ <item name="android:paddingTop">@dimen/call_detail_action_item_padding_vertical</item>
+ <item name="android:paddingBottom">@dimen/call_detail_action_item_padding_vertical</item>
+ <item name="android:textColor">@color/call_detail_footer_text_color</item>
+ <item name="android:textSize">@dimen/call_detail_action_item_text_size</item>
+ </style>
+
+ <style name="DialtactsActionBarStyle" parent="DialerActionBarBaseStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:background">@color/actionbar_background_color</item>
+ <item name="background">@color/actionbar_background_color</item>
+ <item name="android:titleTextStyle">@style/DialtactsActionBarTitleText</item>
+ <item name="titleTextStyle">@style/DialtactsActionBarTitleText</item>
+ <item name="android:elevation">@dimen/action_bar_elevation</item>
+ <item name="elevation">@dimen/action_bar_elevation</item>
+ <!-- Empty icon -->
+ <item name="android:icon">@android:color/transparent</item>
+ <item name="icon">@android:color/transparent</item>
+ <!-- Shift the title text to the right -->
+ <item name="android:contentInsetStart">@dimen/actionbar_contentInsetStart</item>
+ <item name="contentInsetStart">@dimen/actionbar_contentInsetStart</item>
+ </style>
+
+ <style name="DialtactsActionBarWithoutTitleStyle" parent="DialtactsActionBarStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:displayOptions"></item>
+ <item name="displayOptions"></item>
+ <item name="android:height">@dimen/action_bar_height_large</item>
+ <item name="height">@dimen/action_bar_height_large</item>
+ <!-- Override ActionBar title offset to keep search box aligned left -->
+ <item name="android:contentInsetStart">0dp</item>
+ <item name="contentInsetStart">0dp</item>
+ <item name="android:contentInsetEnd">0dp</item>
+ <item name="contentInsetEnd">0dp</item>
+ </style>
+
+ <!-- Text in the action bar at the top of the screen -->
+ <style name="DialtactsActionBarTitleText"
+ parent="@android:style/TextAppearance.Material.Widget.ActionBar.Title">
+ <item name="android:textColor">@color/actionbar_text_color</item>
+ </style>
+
+ <!-- Text style for tabs. -->
+ <style name="DialtactsActionBarTabTextStyle"
+ parent="android:style/Widget.Material.Light.ActionBar.TabText">
+ <item name="android:textColor">@color/tab_text_color</item>
+ <item name="android:textSize">@dimen/tab_text_size</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ </style>
+
+ <style name="CallLogActionStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="android:gravity">center_vertical</item>
+ </style>
+
+ <style name="CallLogActionTextStyle">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:textColor">@color/call_log_action_color</item>
+ <item name="android:textSize">@dimen/call_log_primary_text_size</item>
+ <item name="android:fontFamily">"sans-serif"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:importantForAccessibility">no</item>
+ </style>
+
+ <style name="CallLogActionSupportTextStyle" parent="@style/CallLogActionTextStyle">
+ <item name="android:textSize">@dimen/call_log_detail_text_size</item>
+ <item name="android:textColor">@color/call_log_detail_color</item>
+ </style>
+
+ <style name="CallLogActionIconStyle">
+ <item name="android:layout_width">@dimen/call_log_action_icon_dimen</item>
+ <item name="android:layout_height">@dimen/call_log_action_icon_dimen</item>
+ <item name="android:layout_marginStart">@dimen/call_log_action_icon_margin_start</item>
+ <item name="android:tint">?android:textColorSecondary</item>
+ <item name="android:importantForAccessibility">no</item>
+ </style>
+
+ <style name="DismissButtonStyle">
+ <item name="android:paddingLeft">@dimen/dismiss_button_padding_start</item>
+ <item name="android:paddingRight">@dimen/dismiss_button_padding_end</item>
+ </style>
+
+ <!-- Style applied to the "Settings" screen. Keep in sync with SettingsLight in Telephony. -->
+ <style name="SettingsStyle" parent="DialtactsThemeWithoutActionBarOverlay">
+ <!-- Setting text. -->
+ <item name="android:textColorPrimary">@color/settings_text_color_primary</item>
+ <!-- Setting description. -->
+ <item name="android:textColorSecondary">@color/settings_text_color_secondary</item>
+ <item name="android:windowBackground">@color/setting_background_color</item>
+ <item name="android:colorAccent">@color/dialtacts_theme_color</item>
+ <item name="android:textColorLink">@color/dialtacts_theme_color</item>
+ </style>
+
+ <style name="ManageBlockedNumbersStyle" parent="SettingsStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:windowActionBarOverlay">true</item>
+ <item name="windowActionBarOverlay">true</item>
+ <item name="android:actionBarStyle">@style/ManageBlockedNumbersActionBarStyle</item>
+ <item name="actionBarStyle">@style/ManageBlockedNumbersActionBarStyle</item>
+ <item name="android:fastScrollTrackDrawable">@null</item>
+ </style>
+
+ <style name="ManageBlockedNumbersActionBarStyle" parent="DialtactsActionBarWithoutTitleStyle">
+ <!-- Styles that require AppCompat compatibility, remember to update both sets -->
+ <item name="android:height">@dimen/action_bar_height</item>
+ <item name="height">@dimen/action_bar_height</item>
+ </style>
+
+ <style name="VoicemailPlaybackLayoutTextStyle">
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="VoicemailPlaybackLayoutButtonStyle">
+ <item name="android:layout_width">56dp</item>
+ <item name="android:layout_height">56dp</item>
+ <item name="android:background">@drawable/oval_ripple</item>
+ <item name="android:padding">8dp</item>
+ </style>
+
+ <style name="DialerFlatButtonStyle" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">?android:attr/selectableItemBackground</item>
+ <item name="android:paddingEnd">@dimen/button_horizontal_padding</item>
+ <item name="android:paddingStart">@dimen/button_horizontal_padding</item>
+ <item name="android:textColor">@color/dialer_flat_button_text_color</item>
+ </style>
+
+ <!-- Style for the 'primary' button in a view. Unlike the DialerFlatButtonStyle, this button -->
+ <!-- is not colored white, to draw more attention to it. -->
+ <style name="DialerPrimaryFlatButtonStyle" parent="@android:style/Widget.Material.Button">
+ <item name="android:background">@drawable/selectable_primary_flat_button</item>
+ <item name="android:paddingEnd">@dimen/button_horizontal_padding</item>
+ <item name="android:paddingStart">@dimen/button_horizontal_padding</item>
+ <item name="android:textColor">@android:color/white</item>
+ </style>
+
+ <style name="BlockedNumbersDescriptionTextStyle">
+ <item name="android:lineSpacingMultiplier">1.43</item>
+ <item name="android:paddingTop">8dp</item>
+ <item name="android:paddingBottom">8dp</item>
+ <item name="android:textSize">@dimen/blocked_number_settings_description_text_size</item>
+ </style>
+
+ <style name="FullWidthDivider">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">1dp</item>
+ <item name="android:background">?android:attr/listDivider</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/app/res/xml/display_options_settings.xml b/java/com/android/dialer/app/res/xml/display_options_settings.xml
new file mode 100644
index 000000000..0b4e11d47
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/display_options_settings.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <com.android.contacts.common.preference.SortOrderPreference
+ android:dialogTitle="@string/display_options_sort_list_by"
+ android:key="sortOrder"
+ android:title="@string/display_options_sort_list_by"/>
+
+ <com.android.contacts.common.preference.DisplayOrderPreference
+ android:dialogTitle="@string/display_options_view_names_as"
+ android:key="displayOrder"
+ android:title="@string/display_options_view_names_as"/>
+
+</PreferenceScreen>
diff --git a/java/com/android/dialer/app/res/xml/file_paths.xml b/java/com/android/dialer/app/res/xml/file_paths.xml
new file mode 100644
index 000000000..41522e4c8
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/file_paths.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<paths>
+ <!-- Offer access to files under Context.getCacheDir() -->
+ <cache-path name="my_cache"/>
+ <!-- Offer access to voicemail folder under Context.getFilesDir() -->
+ <files-path
+ name="voicemails"
+ path="voicemails/"/>
+</paths>
diff --git a/java/com/android/dialer/app/res/xml/searchable.xml b/java/com/android/dialer/app/res/xml/searchable.xml
new file mode 100644
index 000000000..0ea168589
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/searchable.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<searchable xmlns:android="http://schemas.android.com/apk/res/android"
+ android:hint="@string/dialer_hint_find_contact"
+ android:imeOptions="actionSearch"
+ android:inputType="textNoSuggestions"
+ android:label="@string/applicationLabel"
+ android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
+ /> \ No newline at end of file
diff --git a/java/com/android/dialer/app/res/xml/sound_settings.xml b/java/com/android/dialer/app/res/xml/sound_settings.xml
new file mode 100644
index 000000000..796ed2ec1
--- /dev/null
+++ b/java/com/android/dialer/app/res/xml/sound_settings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <com.android.dialer.app.settings.DefaultRingtonePreference
+ android:dialogTitle="@string/ringtone_title"
+ android:key="@string/ringtone_preference_key"
+ android:persistent="false"
+ android:ringtoneType="ringtone"
+ android:title="@string/ringtone_title"/>
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="@string/vibrate_on_preference_key"
+ android:persistent="false"
+ android:title="@string/vibrate_on_ring_title"/>
+
+ <CheckBoxPreference
+ android:defaultValue="true"
+ android:key="@string/play_dtmf_preference_key"
+ android:persistent="false"
+ android:title="@string/dtmf_tone_enable_title"/>
+
+ <ListPreference
+ android:entries="@array/dtmf_tone_length_entries"
+ android:entryValues="@array/dtmf_tone_length_entry_values"
+ android:key="@string/dtmf_tone_length_preference_key"
+ android:title="@string/dtmf_tone_length_title"/>
+
+</PreferenceScreen>
diff --git a/java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java b/java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java
new file mode 100644
index 000000000..2c464386b
--- /dev/null
+++ b/java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java
@@ -0,0 +1,155 @@
+/*
+ * 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.settings;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatDelegate;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public class AppCompatPreferenceActivity extends PreferenceActivity {
+
+ private AppCompatDelegate mDelegate;
+
+ private boolean mIsSafeToCommitTransactions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ getDelegate().installViewFactory();
+ getDelegate().onCreate(savedInstanceState);
+ super.onCreate(savedInstanceState);
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getDelegate().onPostCreate(savedInstanceState);
+ }
+
+ public ActionBar getSupportActionBar() {
+ return getDelegate().getSupportActionBar();
+ }
+
+ public void setSupportActionBar(Toolbar toolbar) {
+ getDelegate().setSupportActionBar(toolbar);
+ }
+
+ @Override
+ public MenuInflater getMenuInflater() {
+ return getDelegate().getMenuInflater();
+ }
+
+ @Override
+ public void setContentView(int layoutResID) {
+ getDelegate().setContentView(layoutResID);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ getDelegate().setContentView(view);
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().setContentView(view, params);
+ }
+
+ @Override
+ public void addContentView(View view, ViewGroup.LayoutParams params) {
+ getDelegate().addContentView(view, params);
+ }
+
+ @Override
+ protected void onPostResume() {
+ super.onPostResume();
+ getDelegate().onPostResume();
+ }
+
+ @Override
+ protected void onTitleChanged(CharSequence title, int color) {
+ super.onTitleChanged(title, color);
+ getDelegate().setTitle(title);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ getDelegate().onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getDelegate().onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ getDelegate().onDestroy();
+ }
+
+ @Override
+ public void invalidateOptionsMenu() {
+ getDelegate().invalidateOptionsMenu();
+ }
+
+ private AppCompatDelegate getDelegate() {
+ if (mDelegate == null) {
+ mDelegate = AppCompatDelegate.create(this, null);
+ }
+ return mDelegate;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mIsSafeToCommitTransactions = false;
+ }
+
+ /**
+ * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+ * whether {@link Activity#onSaveInstanceState} has been called or not.
+ *
+ * <p>Make sure that the current activity calls into {@link super.onSaveInstanceState(Bundle
+ * outState)} (if that method is overridden), so the flag is properly set.
+ */
+ public boolean isSafeToCommitTransactions() {
+ return mIsSafeToCommitTransactions;
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DefaultRingtonePreference.java b/java/com/android/dialer/app/settings/DefaultRingtonePreference.java
new file mode 100644
index 000000000..579584e0f
--- /dev/null
+++ b/java/com/android/dialer/app/settings/DefaultRingtonePreference.java
@@ -0,0 +1,64 @@
+/*
+ * 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.app.settings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.preference.RingtonePreference;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.widget.Toast;
+import com.android.dialer.app.R;
+
+/** RingtonePreference which doesn't show default ringtone setting. */
+public class DefaultRingtonePreference extends RingtonePreference {
+
+ public DefaultRingtonePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) {
+ super.onPrepareRingtonePickerIntent(ringtonePickerIntent);
+
+ /*
+ * Since this preference is for choosing the default ringtone, it
+ * doesn't make sense to show a 'Default' item.
+ */
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
+ }
+
+ @Override
+ protected void onSaveRingtone(Uri ringtoneUri) {
+ if (!Settings.System.canWrite(getContext())) {
+ Toast.makeText(
+ getContext(),
+ getContext().getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+ RingtoneManager.setActualDefaultRingtoneUri(getContext(), getRingtoneType(), ringtoneUri);
+ }
+
+ @Override
+ protected Uri onRestoreRingtone() {
+ return RingtoneManager.getActualDefaultRingtoneUri(getContext(), getRingtoneType());
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
new file mode 100644
index 000000000..b04674013
--- /dev/null
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -0,0 +1,187 @@
+/*
+ * 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.settings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.view.MenuItem;
+import android.widget.Toast;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.app.R;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.proguard.UsedByReflection;
+import java.util.List;
+
+@UsedByReflection(value = "AndroidManifest-app.xml")
+public class DialerSettingsActivity extends AppCompatPreferenceActivity {
+
+ protected SharedPreferences mPreferences;
+ private boolean migrationStatusOnBuildHeaders;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ /*
+ * The blockedCallsHeader need to be recreated if the migration status changed because
+ * the intent needs to be updated.
+ */
+ if (migrationStatusOnBuildHeaders != FilteredNumberCompat.hasMigratedToNewBlocking(this)) {
+ invalidateHeaders();
+ }
+ }
+
+ @Override
+ public void onBuildHeaders(List<Header> target) {
+ if (showDisplayOptions()) {
+ Header displayOptionsHeader = new Header();
+ displayOptionsHeader.titleRes = R.string.display_options_title;
+ displayOptionsHeader.fragment = DisplayOptionsSettingsFragment.class.getName();
+ target.add(displayOptionsHeader);
+ }
+
+ Header soundSettingsHeader = new Header();
+ soundSettingsHeader.titleRes = R.string.sounds_and_vibration_title;
+ soundSettingsHeader.fragment = SoundSettingsFragment.class.getName();
+ soundSettingsHeader.id = R.id.settings_header_sounds_and_vibration;
+ target.add(soundSettingsHeader);
+
+ if (CompatUtils.isMarshmallowCompatible()) {
+ Header quickResponseSettingsHeader = new Header();
+ Intent quickResponseSettingsIntent =
+ new Intent(TelecomManager.ACTION_SHOW_RESPOND_VIA_SMS_SETTINGS);
+ quickResponseSettingsHeader.titleRes = R.string.respond_via_sms_setting_title;
+ quickResponseSettingsHeader.intent = quickResponseSettingsIntent;
+ target.add(quickResponseSettingsHeader);
+ }
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+
+ // "Call Settings" (full settings) is shown if the current user is primary user and there
+ // is only one SIM. Before N, "Calling accounts" setting is shown if the current user is
+ // primary user and there are multiple SIMs. In N+, "Calling accounts" is shown whenever
+ // "Call Settings" is not shown.
+ boolean isPrimaryUser = isPrimaryUser();
+ if (isPrimaryUser && TelephonyManagerCompat.getPhoneCount(telephonyManager) <= 1) {
+ Header callSettingsHeader = new Header();
+ Intent callSettingsIntent = new Intent(TelecomManager.ACTION_SHOW_CALL_SETTINGS);
+ callSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ callSettingsHeader.titleRes = R.string.call_settings_label;
+ callSettingsHeader.intent = callSettingsIntent;
+ target.add(callSettingsHeader);
+ } else if ((VERSION.SDK_INT >= VERSION_CODES.N) || isPrimaryUser) {
+ Header phoneAccountSettingsHeader = new Header();
+ Intent phoneAccountSettingsIntent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS);
+ phoneAccountSettingsIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ phoneAccountSettingsHeader.titleRes = R.string.phone_account_settings_label;
+ phoneAccountSettingsHeader.intent = phoneAccountSettingsIntent;
+ target.add(phoneAccountSettingsHeader);
+ }
+ if (FilteredNumberCompat.canCurrentUserOpenBlockSettings(this)) {
+ Header blockedCallsHeader = new Header();
+ blockedCallsHeader.titleRes = R.string.manage_blocked_numbers_label;
+ blockedCallsHeader.intent = FilteredNumberCompat.createManageBlockedNumbersIntent(this);
+ target.add(blockedCallsHeader);
+ migrationStatusOnBuildHeaders = FilteredNumberCompat.hasMigratedToNewBlocking(this);
+ }
+ if (isPrimaryUser
+ && (TelephonyManagerCompat.isTtyModeSupported(telephonyManager)
+ || TelephonyManagerCompat.isHearingAidCompatibilitySupported(telephonyManager))) {
+ Header accessibilitySettingsHeader = new Header();
+ Intent accessibilitySettingsIntent =
+ new Intent(TelecomManager.ACTION_SHOW_CALL_ACCESSIBILITY_SETTINGS);
+ accessibilitySettingsHeader.titleRes = R.string.accessibility_settings_title;
+ accessibilitySettingsHeader.intent = accessibilitySettingsIntent;
+ target.add(accessibilitySettingsHeader);
+ }
+ }
+
+ /**
+ * Returns {@code true} or {@code false} based on whether the display options setting should be
+ * shown. For languages such as Chinese, Japanese, or Korean, display options aren't useful since
+ * contacts are sorted and displayed family name first by default.
+ *
+ * @return {@code true} if the display options should be shown, {@code false} otherwise.
+ */
+ private boolean showDisplayOptions() {
+ return getResources().getBoolean(R.bool.config_display_order_user_changeable)
+ && getResources().getBoolean(R.bool.config_sort_order_user_changeable);
+ }
+
+ @Override
+ public void onHeaderClick(Header header, int position) {
+ if (header.id == R.id.settings_header_sounds_and_vibration) {
+ // If we don't have the permission to write to system settings, go to system sound
+ // settings instead. Otherwise, perform the super implementation (which launches our
+ // own preference fragment.
+ if (!Settings.System.canWrite(this)) {
+ Toast.makeText(
+ this,
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
+ return;
+ }
+ }
+ super.onHeaderClick(header, position);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!isSafeToCommitTransactions()) {
+ return;
+ }
+ super.onBackPressed();
+ }
+
+ @Override
+ protected boolean isValidFragment(String fragmentName) {
+ return true;
+ }
+
+ /** @return Whether the current user is the primary user. */
+ private boolean isPrimaryUser() {
+ return getSystemService(UserManager.class).isSystemUser();
+ }
+}
diff --git a/java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java b/java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java
new file mode 100644
index 000000000..bf1637f27
--- /dev/null
+++ b/java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java
@@ -0,0 +1,30 @@
+/*
+ * 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.settings;
+
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+import com.android.dialer.app.R;
+
+public class DisplayOptionsSettingsFragment extends PreferenceFragment {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.display_options_settings);
+ }
+}
diff --git a/java/com/android/dialer/app/settings/SoundSettingsFragment.java b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
new file mode 100644
index 000000000..83ce45398
--- /dev/null
+++ b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
@@ -0,0 +1,242 @@
+/*
+ * 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.app.settings;
+
+import android.content.Context;
+import android.media.RingtoneManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+import com.android.dialer.app.R;
+import com.android.dialer.compat.SdkVersionOverride;
+import com.android.dialer.util.SettingsUtil;
+
+public class SoundSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ private static final int NO_DTMF_TONE = 0;
+ private static final int PLAY_DTMF_TONE = 1;
+
+ private static final int NO_VIBRATION_FOR_CALLS = 0;
+ private static final int DO_VIBRATION_FOR_CALLS = 1;
+
+ private static final int DTMF_TONE_TYPE_NORMAL = 0;
+
+ private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1;
+
+ private Preference mRingtonePreference;
+ private final Handler mRingtoneLookupComplete =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_RINGTONE_SUMMARY:
+ mRingtonePreference.setSummary((CharSequence) msg.obj);
+ break;
+ }
+ }
+ };
+ private final Runnable mRingtoneLookupRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ updateRingtonePreferenceSummary();
+ }
+ };
+ private CheckBoxPreference mVibrateWhenRinging;
+ private CheckBoxPreference mPlayDtmfTone;
+ private ListPreference mDtmfToneLength;
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.sound_settings);
+
+ Context context = getActivity();
+
+ mRingtonePreference = findPreference(context.getString(R.string.ringtone_preference_key));
+ mVibrateWhenRinging =
+ (CheckBoxPreference) findPreference(context.getString(R.string.vibrate_on_preference_key));
+ mPlayDtmfTone =
+ (CheckBoxPreference) findPreference(context.getString(R.string.play_dtmf_preference_key));
+ mDtmfToneLength =
+ (ListPreference)
+ findPreference(context.getString(R.string.dtmf_tone_length_preference_key));
+
+ if (hasVibrator()) {
+ mVibrateWhenRinging.setOnPreferenceChangeListener(this);
+ } else {
+ getPreferenceScreen().removePreference(mVibrateWhenRinging);
+ mVibrateWhenRinging = null;
+ }
+
+ mPlayDtmfTone.setOnPreferenceChangeListener(this);
+ mPlayDtmfTone.setChecked(shouldPlayDtmfTone());
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M
+ && telephonyManager.canChangeDtmfToneLength()
+ && (telephonyManager.isWorldPhone() || !shouldHideCarrierSettings())) {
+ mDtmfToneLength.setOnPreferenceChangeListener(this);
+ mDtmfToneLength.setValueIndex(
+ Settings.System.getInt(
+ context.getContentResolver(),
+ Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+ DTMF_TONE_TYPE_NORMAL));
+ } else {
+ getPreferenceScreen().removePreference(mDtmfToneLength);
+ mDtmfToneLength = null;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!Settings.System.canWrite(getContext())) {
+ // If the user launches this setting fragment, then toggles the WRITE_SYSTEM_SETTINGS
+ // AppOp, then close the fragment since there is nothing useful to do.
+ getActivity().onBackPressed();
+ return;
+ }
+
+ if (mVibrateWhenRinging != null) {
+ mVibrateWhenRinging.setChecked(shouldVibrateWhenRinging());
+ }
+
+ // Lookup the ringtone name asynchronously.
+ new Thread(mRingtoneLookupRunnable).start();
+ }
+
+ /**
+ * Supports onPreferenceChangeListener to look for preference changes.
+ *
+ * @param preference The preference to be changed
+ * @param objValue The value of the selection, NOT its localized display value.
+ */
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (!Settings.System.canWrite(getContext())) {
+ // A user shouldn't be able to get here, but this protects against monkey crashes.
+ Toast.makeText(
+ getContext(),
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ if (preference == mVibrateWhenRinging) {
+ boolean doVibrate = (Boolean) objValue;
+ Settings.System.putInt(
+ getActivity().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING,
+ doVibrate ? DO_VIBRATION_FOR_CALLS : NO_VIBRATION_FOR_CALLS);
+ } else if (preference == mDtmfToneLength) {
+ int index = mDtmfToneLength.findIndexOfValue((String) objValue);
+ Settings.System.putInt(
+ getActivity().getContentResolver(), Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, index);
+ }
+ return true;
+ }
+
+ /** Click listener for toggle events. */
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ if (!Settings.System.canWrite(getContext())) {
+ Toast.makeText(
+ getContext(),
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ if (preference == mPlayDtmfTone) {
+ Settings.System.putInt(
+ getActivity().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING,
+ mPlayDtmfTone.isChecked() ? PLAY_DTMF_TONE : NO_DTMF_TONE);
+ }
+ return true;
+ }
+
+ /** Updates the summary text on the ringtone preference with the name of the ringtone. */
+ private void updateRingtonePreferenceSummary() {
+ SettingsUtil.updateRingtoneName(
+ getActivity(),
+ mRingtoneLookupComplete,
+ RingtoneManager.TYPE_RINGTONE,
+ mRingtonePreference.getKey(),
+ MSG_UPDATE_RINGTONE_SUMMARY);
+ }
+
+ /**
+ * Obtain the value for "vibrate when ringing" setting. The default value is false.
+ *
+ * <p>Watch out: if the setting is missing in the device, this will try obtaining the old "vibrate
+ * on ring" setting from AudioManager, and save the previous setting to the new one.
+ */
+ private boolean shouldVibrateWhenRinging() {
+ int vibrateWhenRingingSetting =
+ Settings.System.getInt(
+ getActivity().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING,
+ NO_VIBRATION_FOR_CALLS);
+ return hasVibrator() && (vibrateWhenRingingSetting == DO_VIBRATION_FOR_CALLS);
+ }
+
+ /** Obtains the value for dialpad/DTMF tones. The default value is true. */
+ private boolean shouldPlayDtmfTone() {
+ int dtmfToneSetting =
+ Settings.System.getInt(
+ getActivity().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING,
+ PLAY_DTMF_TONE);
+ return dtmfToneSetting == PLAY_DTMF_TONE;
+ }
+
+ /** Whether the device hardware has a vibrator. */
+ private boolean hasVibrator() {
+ Vibrator vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
+ return vibrator != null && vibrator.hasVibrator();
+ }
+
+ private boolean shouldHideCarrierSettings() {
+ CarrierConfigManager configManager =
+ (CarrierConfigManager) getActivity().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ return configManager
+ .getConfig()
+ .getBoolean(CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java b/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java
new file mode 100644
index 000000000..8d70cdbe7
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailAudioManager.java
@@ -0,0 +1,252 @@
+/*
+ * 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.voicemail;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.telecom.CallAudioState;
+import com.android.dialer.common.LogUtil;
+import java.util.concurrent.RejectedExecutionException;
+
+/** This class manages all audio changes for voicemail playback. */
+public final class VoicemailAudioManager
+ implements OnAudioFocusChangeListener, WiredHeadsetManager.Listener {
+
+ private static final String TAG = "VoicemailAudioManager";
+
+ public static final int PLAYBACK_STREAM = AudioManager.STREAM_VOICE_CALL;
+
+ private AudioManager mAudioManager;
+ private VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
+ private WiredHeadsetManager mWiredHeadsetManager;
+ private boolean mWasSpeakerOn;
+ private CallAudioState mCallAudioState;
+ private boolean mBluetoothScoEnabled;
+
+ public VoicemailAudioManager(
+ Context context, VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
+ mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
+ mWiredHeadsetManager = new WiredHeadsetManager(context);
+ mWiredHeadsetManager.setListener(this);
+
+ mCallAudioState = getInitialAudioState();
+ LogUtil.i(
+ "VoicemailAudioManager.VoicemailAudioManager", "Initial audioState = " + mCallAudioState);
+ }
+
+ public void requestAudioFocus() {
+ int result =
+ mAudioManager.requestAudioFocus(
+ this, PLAYBACK_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ throw new RejectedExecutionException("Could not capture audio focus.");
+ }
+ updateBluetoothScoState(true);
+ }
+
+ public void abandonAudioFocus() {
+ updateBluetoothScoState(false);
+ mAudioManager.abandonAudioFocus(this);
+ }
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ LogUtil.d("VoicemailAudioManager.onAudioFocusChange", "focusChange=" + focusChange);
+ mVoicemailPlaybackPresenter.onAudioFocusChange(focusChange == AudioManager.AUDIOFOCUS_GAIN);
+ }
+
+ @Override
+ public void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn) {
+ LogUtil.i(
+ "VoicemailAudioManager.onWiredHeadsetPluggedInChanged",
+ "wired headset was plugged in changed: " + oldIsPluggedIn + " -> " + newIsPluggedIn);
+
+ if (oldIsPluggedIn == newIsPluggedIn) {
+ return;
+ }
+
+ int newRoute = mCallAudioState.getRoute(); // start out with existing route
+ if (newIsPluggedIn) {
+ newRoute = CallAudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ if (mWasSpeakerOn) {
+ newRoute = CallAudioState.ROUTE_SPEAKER;
+ } else {
+ newRoute = CallAudioState.ROUTE_EARPIECE;
+ }
+ }
+
+ mVoicemailPlaybackPresenter.setSpeakerphoneOn(newRoute == CallAudioState.ROUTE_SPEAKER);
+
+ // We need to call this every time even if we do not change the route because the supported
+ // routes changed either to include or not include WIRED_HEADSET.
+ setSystemAudioState(
+ new CallAudioState(false /* muted */, newRoute, calculateSupportedRoutes()));
+ }
+
+ public void setSpeakerphoneOn(boolean on) {
+ setAudioRoute(on ? CallAudioState.ROUTE_SPEAKER : CallAudioState.ROUTE_WIRED_OR_EARPIECE);
+ }
+
+ public boolean isWiredHeadsetPluggedIn() {
+ return mWiredHeadsetManager.isPluggedIn();
+ }
+
+ public void registerReceivers() {
+ // Receivers is plural because we expect to add bluetooth support.
+ mWiredHeadsetManager.registerReceiver();
+ }
+
+ public void unregisterReceivers() {
+ mWiredHeadsetManager.unregisterReceiver();
+ }
+
+ /**
+ * Bluetooth SCO (Synchronous Connection-Oriented) is the "phone" bluetooth audio. The system will
+ * route to the bluetooth headset automatically if A2DP ("media") is available, but if the headset
+ * only supports SCO then dialer must route it manually.
+ */
+ private void updateBluetoothScoState(boolean hasAudioFocus) {
+ if (hasAudioFocus) {
+ if (hasMediaAudioCapability()) {
+ mBluetoothScoEnabled = false;
+ } else {
+ mBluetoothScoEnabled = true;
+ LogUtil.i(
+ "VoicemailAudioManager.updateBluetoothScoState",
+ "bluetooth device doesn't support media, using SCO instead");
+ }
+ } else {
+ mBluetoothScoEnabled = false;
+ }
+ applyBluetoothScoState();
+ }
+
+ private void applyBluetoothScoState() {
+ if (mBluetoothScoEnabled) {
+ mAudioManager.startBluetoothSco();
+ // The doc for startBluetoothSco() states it could take seconds to establish the SCO
+ // connection, so we should probably resume the playback after we've acquired SCO.
+ // In practice the delay is unnoticeable so this is ignored for simplicity.
+ mAudioManager.setBluetoothScoOn(true);
+ } else {
+ mAudioManager.setBluetoothScoOn(false);
+ mAudioManager.stopBluetoothSco();
+ }
+ }
+
+ private boolean hasMediaAudioCapability() {
+ for (AudioDeviceInfo info : mAudioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
+ if (info.getType() == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Change the audio route, for example from earpiece to speakerphone.
+ *
+ * @param route The new audio route to use. See {@link CallAudioState}.
+ */
+ void setAudioRoute(int route) {
+ LogUtil.v(
+ "VoicemailAudioManager.setAudioRoute",
+ "route: " + CallAudioState.audioRouteToString(route));
+
+ // Change ROUTE_WIRED_OR_EARPIECE to a single entry.
+ int newRoute = selectWiredOrEarpiece(route, mCallAudioState.getSupportedRouteMask());
+
+ // If route is unsupported, do nothing.
+ if ((mCallAudioState.getSupportedRouteMask() | newRoute) == 0) {
+ LogUtil.w(
+ "VoicemailAudioManager.setAudioRoute",
+ "Asking to set to a route that is unsupported: " + newRoute);
+ return;
+ }
+
+ // Remember the new speaker state so it can be restored when the user plugs and unplugs
+ // a headset.
+ mWasSpeakerOn = newRoute == CallAudioState.ROUTE_SPEAKER;
+ setSystemAudioState(
+ new CallAudioState(false /* muted */, newRoute, mCallAudioState.getSupportedRouteMask()));
+ }
+
+ private CallAudioState getInitialAudioState() {
+ int supportedRouteMask = calculateSupportedRoutes();
+ int route = selectWiredOrEarpiece(CallAudioState.ROUTE_WIRED_OR_EARPIECE, supportedRouteMask);
+ return new CallAudioState(false /* muted */, route, supportedRouteMask);
+ }
+
+ private int calculateSupportedRoutes() {
+ int routeMask = CallAudioState.ROUTE_SPEAKER;
+ if (mWiredHeadsetManager.isPluggedIn()) {
+ routeMask |= CallAudioState.ROUTE_WIRED_HEADSET;
+ } else {
+ routeMask |= CallAudioState.ROUTE_EARPIECE;
+ }
+ return routeMask;
+ }
+
+ private int selectWiredOrEarpiece(int route, int supportedRouteMask) {
+ // Since they are mutually exclusive and one is ALWAYS valid, we allow a special input of
+ // ROUTE_WIRED_OR_EARPIECE so that callers don't have to make a call to check which is
+ // supported before calling setAudioRoute.
+ if (route == CallAudioState.ROUTE_WIRED_OR_EARPIECE) {
+ route = CallAudioState.ROUTE_WIRED_OR_EARPIECE & supportedRouteMask;
+ if (route == 0) {
+ LogUtil.e(
+ "VoicemailAudioManager.selectWiredOrEarpiece",
+ "One of wired headset or earpiece should always be valid.");
+ // assume earpiece in this case.
+ route = CallAudioState.ROUTE_EARPIECE;
+ }
+ }
+ return route;
+ }
+
+ private void setSystemAudioState(CallAudioState callAudioState) {
+ CallAudioState oldAudioState = mCallAudioState;
+ mCallAudioState = callAudioState;
+
+ LogUtil.i(
+ "VoicemailAudioManager.setSystemAudioState",
+ "changing from " + oldAudioState + " to " + mCallAudioState);
+
+ // Audio route.
+ if (mCallAudioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
+ turnOnSpeaker(true);
+ } else if (mCallAudioState.getRoute() == CallAudioState.ROUTE_EARPIECE
+ || mCallAudioState.getRoute() == CallAudioState.ROUTE_WIRED_HEADSET) {
+ // Just handle turning off the speaker, the system will handle switching between wired
+ // headset and earpiece.
+ turnOnSpeaker(false);
+ // BluetoothSco is not handled by the system so it has to be reset.
+ applyBluetoothScoState();
+ }
+ }
+
+ private void turnOnSpeaker(boolean on) {
+ if (mAudioManager.isSpeakerphoneOn() != on) {
+ LogUtil.i("VoicemailAudioManager.turnOnSpeaker", "turning speaker phone on: " + on);
+ mAudioManager.setSpeakerphoneOn(on);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.java
new file mode 100644
index 000000000..939007adf
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailErrorManager.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.voicemail;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.os.Handler;
+import com.android.dialer.app.calllog.CallLogAlertManager;
+import com.android.dialer.app.calllog.CallLogModalAlertManager;
+import com.android.dialer.app.voicemail.error.VoicemailErrorAlert;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessageCreator;
+import com.android.dialer.app.voicemail.error.VoicemailStatus;
+import com.android.dialer.app.voicemail.error.VoicemailStatusReader;
+import com.android.dialer.database.CallLogQueryHandler;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Fetches voicemail status and generate {@link VoicemailStatus} for {@link VoicemailErrorAlert} to
+ * show.
+ */
+public class VoicemailErrorManager implements CallLogQueryHandler.Listener, VoicemailStatusReader {
+
+ private final Context context;
+ private final CallLogQueryHandler callLogQueryHandler;
+ private final VoicemailErrorAlert alertItem;
+
+ private final ContentObserver statusObserver =
+ new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ maybeFetchStatus();
+ }
+ };
+
+ private boolean isForeground;
+ private boolean statusInvalidated;
+
+ public VoicemailErrorManager(
+ Context context,
+ CallLogAlertManager alertManager,
+ CallLogModalAlertManager modalAlertManager) {
+ this.context = context;
+ alertItem =
+ new VoicemailErrorAlert(
+ context, alertManager, modalAlertManager, new VoicemailErrorMessageCreator());
+ callLogQueryHandler = new CallLogQueryHandler(context, context.getContentResolver(), this);
+ maybeFetchStatus();
+ }
+
+ public ContentObserver getContentObserver() {
+ return statusObserver;
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ List<VoicemailStatus> statuses = new ArrayList<>();
+ while (statusCursor.moveToNext()) {
+ VoicemailStatus status = new VoicemailStatus(context, statusCursor);
+ if (status.isActive()) {
+ statuses.add(status);
+ }
+ }
+ alertItem.updateStatus(statuses, this);
+ // TODO: b/30668323 support error from multiple sources.
+ return;
+ }
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public boolean onCallsFetched(Cursor combinedCursor) {
+ // Do nothing
+ return false;
+ }
+
+ public void onResume() {
+ isForeground = true;
+ if (statusInvalidated) {
+ maybeFetchStatus();
+ }
+ }
+
+ public void onPause() {
+ isForeground = false;
+ statusInvalidated = false;
+ }
+
+ @Override
+ public void refresh() {
+ maybeFetchStatus();
+ }
+
+ /**
+ * Fetch the status when the dialer is in foreground, or queue a fetch when the dialer resumes.
+ */
+ private void maybeFetchStatus() {
+ if (!isForeground) {
+ // Dialer is in the background, UI should not be updated. Reload the status when it resumes.
+ statusInvalidated = true;
+ return;
+ }
+ callLogQueryHandler.fetchVoicemailStatus();
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
new file mode 100644
index 000000000..fc6a37608
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java
@@ -0,0 +1,449 @@
+/*
+ * 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.voicemail;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.support.annotation.VisibleForTesting;
+import android.support.design.widget.Snackbar;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import com.android.dialer.app.PhoneCallDetails;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogAsyncTaskUtil;
+import com.android.dialer.app.calllog.CallLogListItemViewHolder;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import java.util.Objects;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.concurrent.GuardedBy;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Displays and plays a single voicemail. See {@link VoicemailPlaybackPresenter} for details on the
+ * voicemail playback implementation.
+ *
+ * <p>This class is not thread-safe, it is thread-confined. All calls to all public methods on this
+ * class are expected to come from the main ui thread.
+ */
+@NotThreadSafe
+public class VoicemailPlaybackLayout extends LinearLayout
+ implements VoicemailPlaybackPresenter.PlaybackView,
+ CallLogAsyncTaskUtil.CallLogAsyncTaskListener {
+
+ private static final String TAG = VoicemailPlaybackLayout.class.getSimpleName();
+ private static final int VOICEMAIL_DELETE_DELAY_MS = 3000;
+
+ private Context mContext;
+ private CallLogListItemViewHolder mViewHolder;
+ private VoicemailPlaybackPresenter mPresenter;
+ /** Click listener to toggle speakerphone. */
+ private final View.OnClickListener mSpeakerphoneListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPresenter != null) {
+ mPresenter.toggleSpeakerphone();
+ }
+ }
+ };
+
+ private Uri mVoicemailUri;
+ private final View.OnClickListener mDeleteButtonListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_DELETE_ENTRY);
+ if (mPresenter == null) {
+ return;
+ }
+
+ // When the undo button is pressed, the viewHolder we have is no longer valid because when
+ // we hide the view it is binded to something else, and the layout is not updated for
+ // hidden items. copy the adapter position so we can update the view upon undo.
+ // TODO: refactor this so the view holder will always be valid.
+ final int adapterPosition = mViewHolder.getAdapterPosition();
+
+ mPresenter.pausePlayback();
+ mPresenter.onVoicemailDeleted(mViewHolder);
+
+ final Uri deleteUri = mVoicemailUri;
+ final Runnable deleteCallback =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (Objects.equals(deleteUri, mVoicemailUri)) {
+ CallLogAsyncTaskUtil.deleteVoicemail(
+ mContext, deleteUri, VoicemailPlaybackLayout.this);
+ }
+ }
+ };
+
+ final Handler handler = new Handler();
+ // Add a little buffer time in case the user clicked "undo" at the end of the delay
+ // window.
+ handler.postDelayed(deleteCallback, VOICEMAIL_DELETE_DELAY_MS + 50);
+
+ Snackbar.make(
+ VoicemailPlaybackLayout.this,
+ R.string.snackbar_voicemail_deleted,
+ Snackbar.LENGTH_LONG)
+ .setDuration(VOICEMAIL_DELETE_DELAY_MS)
+ .setAction(
+ R.string.snackbar_voicemail_deleted_undo,
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mPresenter.onVoicemailDeleteUndo(adapterPosition);
+ handler.removeCallbacks(deleteCallback);
+ }
+ })
+ .setActionTextColor(
+ mContext.getResources().getColor(R.color.dialer_snackbar_action_text_color))
+ .show();
+ }
+ };
+ private boolean mIsPlaying = false;
+ /** Click listener to play or pause voicemail playback. */
+ private final View.OnClickListener mStartStopButtonListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (mPresenter == null) {
+ return;
+ }
+
+ if (mIsPlaying) {
+ mPresenter.pausePlayback();
+ } else {
+ Logger.get(mContext)
+ .logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY);
+ mPresenter.resumePlayback();
+ }
+ }
+ };
+
+ private SeekBar mPlaybackSeek;
+ private ImageButton mStartStopButton;
+ private ImageButton mPlaybackSpeakerphone;
+ private ImageButton mDeleteButton;
+ private TextView mStateText;
+ private TextView mPositionText;
+ private TextView mTotalDurationText;
+ /** Handle state changes when the user manipulates the seek bar. */
+ private final OnSeekBarChangeListener mSeekBarChangeListener =
+ new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (mPresenter != null) {
+ mPresenter.pausePlaybackForSeeking();
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ if (mPresenter != null) {
+ mPresenter.resumePlaybackAfterSeeking(seekBar.getProgress());
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ setClipPosition(progress, seekBar.getMax());
+ // Update the seek position if user manually changed it. This makes sure position gets
+ // updated when user use volume button to seek playback in talkback mode.
+ if (fromUser) {
+ mPresenter.seek(progress);
+ }
+ }
+ };
+
+ private PositionUpdater mPositionUpdater;
+ private Drawable mVoicemailSeekHandleEnabled;
+ private Drawable mVoicemailSeekHandleDisabled;
+
+ public VoicemailPlaybackLayout(Context context) {
+ this(context, null);
+ }
+
+ public VoicemailPlaybackLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.voicemail_playback_layout, this);
+ }
+
+ public void setViewHolder(CallLogListItemViewHolder mViewHolder) {
+ this.mViewHolder = mViewHolder;
+ }
+
+ @Override
+ public void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri) {
+ mPresenter = presenter;
+ mVoicemailUri = voicemailUri;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mPlaybackSeek = (SeekBar) findViewById(R.id.playback_seek);
+ mStartStopButton = (ImageButton) findViewById(R.id.playback_start_stop);
+ mPlaybackSpeakerphone = (ImageButton) findViewById(R.id.playback_speakerphone);
+ mDeleteButton = (ImageButton) findViewById(R.id.delete_voicemail);
+
+ mStateText = (TextView) findViewById(R.id.playback_state_text);
+ mStateText.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);
+ mPositionText = (TextView) findViewById(R.id.playback_position_text);
+ mTotalDurationText = (TextView) findViewById(R.id.total_duration_text);
+
+ mPlaybackSeek.setOnSeekBarChangeListener(mSeekBarChangeListener);
+ mStartStopButton.setOnClickListener(mStartStopButtonListener);
+ mPlaybackSpeakerphone.setOnClickListener(mSpeakerphoneListener);
+ mDeleteButton.setOnClickListener(mDeleteButtonListener);
+
+ mPositionText.setText(formatAsMinutesAndSeconds(0));
+ mTotalDurationText.setText(formatAsMinutesAndSeconds(0));
+
+ mVoicemailSeekHandleEnabled =
+ getResources().getDrawable(R.drawable.ic_voicemail_seek_handle, mContext.getTheme());
+ mVoicemailSeekHandleDisabled =
+ getResources()
+ .getDrawable(R.drawable.ic_voicemail_seek_handle_disabled, mContext.getTheme());
+ }
+
+ @Override
+ public void onPlaybackStarted(int duration, ScheduledExecutorService executorService) {
+ mIsPlaying = true;
+
+ mStartStopButton.setImageResource(R.drawable.ic_pause);
+
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ mPositionUpdater = null;
+ }
+ mPositionUpdater = new PositionUpdater(duration, executorService);
+ mPositionUpdater.startUpdating();
+ }
+
+ @Override
+ public void onPlaybackStopped() {
+ mIsPlaying = false;
+
+ mStartStopButton.setImageResource(R.drawable.ic_play_arrow);
+
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ mPositionUpdater = null;
+ }
+ }
+
+ @Override
+ public void onPlaybackError() {
+ if (mPositionUpdater != null) {
+ mPositionUpdater.stopUpdating();
+ }
+
+ disableUiElements();
+ mStateText.setText(getString(R.string.voicemail_playback_error));
+ }
+
+ @Override
+ public void onSpeakerphoneOn(boolean on) {
+ if (on) {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_up_24dp);
+ // Speaker is now on, tapping button will turn it off.
+ mPlaybackSpeakerphone.setContentDescription(
+ mContext.getString(R.string.voicemail_speaker_off));
+ } else {
+ mPlaybackSpeakerphone.setImageResource(R.drawable.ic_volume_down_24dp);
+ // Speaker is now off, tapping button will turn it on.
+ mPlaybackSpeakerphone.setContentDescription(
+ mContext.getString(R.string.voicemail_speaker_on));
+ }
+ }
+
+ @Override
+ public void setClipPosition(int positionMs, int durationMs) {
+ int seekBarPositionMs = Math.max(0, positionMs);
+ int seekBarMax = Math.max(seekBarPositionMs, durationMs);
+ if (mPlaybackSeek.getMax() != seekBarMax) {
+ mPlaybackSeek.setMax(seekBarMax);
+ }
+
+ mPlaybackSeek.setProgress(seekBarPositionMs);
+
+ mPositionText.setText(formatAsMinutesAndSeconds(seekBarPositionMs));
+ mTotalDurationText.setText(formatAsMinutesAndSeconds(durationMs));
+ }
+
+ @Override
+ public void setSuccess() {
+ mStateText.setText(null);
+ }
+
+ @Override
+ public void setIsFetchingContent() {
+ disableUiElements();
+ mStateText.setText(getString(R.string.voicemail_fetching_content));
+ }
+
+ @Override
+ public void setFetchContentTimeout() {
+ mStartStopButton.setEnabled(true);
+ mStateText.setText(getString(R.string.voicemail_fetching_timout));
+ }
+
+ @Override
+ public int getDesiredClipPosition() {
+ return mPlaybackSeek.getProgress();
+ }
+
+ @Override
+ public void disableUiElements() {
+ mStartStopButton.setEnabled(false);
+ resetSeekBar();
+ }
+
+ @Override
+ public void enableUiElements() {
+ mDeleteButton.setEnabled(true);
+ mStartStopButton.setEnabled(true);
+ mPlaybackSeek.setEnabled(true);
+ mPlaybackSeek.setThumb(mVoicemailSeekHandleEnabled);
+ }
+
+ @Override
+ public void resetSeekBar() {
+ mPlaybackSeek.setProgress(0);
+ mPlaybackSeek.setEnabled(false);
+ mPlaybackSeek.setThumb(mVoicemailSeekHandleDisabled);
+ }
+
+ @Override
+ public void onDeleteCall() {}
+
+ @Override
+ public void onDeleteVoicemail() {
+ mPresenter.onVoicemailDeletedInDatabase();
+ }
+
+ @Override
+ public void onGetCallDetails(PhoneCallDetails[] details) {}
+
+ private String getString(int resId) {
+ return mContext.getString(resId);
+ }
+
+ /**
+ * Formats a number of milliseconds as something that looks like {@code 00:05}.
+ *
+ * <p>We always use four digits, two for minutes two for seconds. In the very unlikely event that
+ * the voicemail duration exceeds 99 minutes, the display is capped at 99 minutes.
+ */
+ private String formatAsMinutesAndSeconds(int millis) {
+ int seconds = millis / 1000;
+ int minutes = seconds / 60;
+ seconds -= minutes * 60;
+ if (minutes > 99) {
+ minutes = 99;
+ }
+ return String.format("%02d:%02d", minutes, seconds);
+ }
+
+ @VisibleForTesting
+ public String getStateText() {
+ return mStateText.getText().toString();
+ }
+
+ /** Controls the animation of the playback slider. */
+ @ThreadSafe
+ private final class PositionUpdater implements Runnable {
+
+ /** Update rate for the slider, 30fps. */
+ private static final int SLIDER_UPDATE_PERIOD_MILLIS = 1000 / 30;
+
+ private final ScheduledExecutorService mExecutorService;
+ private final Object mLock = new Object();
+ private int mDurationMs;
+
+ @GuardedBy("mLock")
+ private ScheduledFuture<?> mScheduledFuture;
+
+ private Runnable mUpdateClipPositionRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ int currentPositionMs = 0;
+ synchronized (mLock) {
+ if (mScheduledFuture == null || mPresenter == null) {
+ // This task has been canceled. Just stop now.
+ return;
+ }
+ currentPositionMs = mPresenter.getMediaPlayerPosition();
+ }
+ setClipPosition(currentPositionMs, mDurationMs);
+ }
+ };
+
+ public PositionUpdater(int durationMs, ScheduledExecutorService executorService) {
+ mDurationMs = durationMs;
+ mExecutorService = executorService;
+ }
+
+ @Override
+ public void run() {
+ post(mUpdateClipPositionRunnable);
+ }
+
+ public void startUpdating() {
+ synchronized (mLock) {
+ cancelPendingRunnables();
+ mScheduledFuture =
+ mExecutorService.scheduleAtFixedRate(
+ this, 0, SLIDER_UPDATE_PERIOD_MILLIS, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ public void stopUpdating() {
+ synchronized (mLock) {
+ cancelPendingRunnables();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void cancelPendingRunnables() {
+ if (mScheduledFuture != null) {
+ mScheduledFuture.cancel(true);
+ mScheduledFuture = null;
+ }
+ removeCallbacks(mUpdateClipPositionRunnable);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
new file mode 100644
index 000000000..657022291
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java
@@ -0,0 +1,1050 @@
+/*
+ * 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.voicemail;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.provider.CallLog;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.content.FileProvider;
+import android.text.TextUtils;
+import android.view.WindowManager.LayoutParams;
+import android.webkit.MimeTypeMap;
+import com.android.common.io.MoreCloseables;
+import com.android.dialer.app.R;
+import com.android.dialer.app.calllog.CallLogListItemViewHolder;
+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.constants.Constants;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.google.common.io.ByteStreams;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.NotThreadSafe;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * Contains the controlling logic for a voicemail playback in the call log. It is closely coupled to
+ * assumptions about the behaviors and lifecycle of the call log, in particular in the {@link
+ * CallLogFragment} and {@link CallLogAdapter}.
+ *
+ * <p>This controls a single {@link com.android.dialer.voicemail.VoicemailPlaybackLayout}. A single
+ * instance can be reused for different such layouts, using {@link #setPlaybackView}. This is to
+ * facilitate reuse across different voicemail call log entries.
+ *
+ * <p>This class is not thread safe. The thread policy for this class is thread-confinement, all
+ * calls into this class from outside must be done from the main UI thread.
+ */
+@NotThreadSafe
+@VisibleForTesting
+@TargetApi(VERSION_CODES.M)
+public class VoicemailPlaybackPresenter
+ implements MediaPlayer.OnPreparedListener,
+ MediaPlayer.OnCompletionListener,
+ MediaPlayer.OnErrorListener {
+
+ public static final int PLAYBACK_REQUEST = 0;
+ private static final int NUMBER_OF_THREADS_IN_POOL = 2;
+ // Time to wait for content to be fetched before timing out.
+ private static final long FETCH_CONTENT_TIMEOUT_MS = 20000;
+ private static final String VOICEMAIL_URI_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".VOICEMAIL_URI";
+ private static final String IS_PREPARED_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_PREPARED";
+ // If present in the saved instance bundle, we should not resume playback on create.
+ private static final String IS_PLAYING_STATE_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_PLAYING_STATE_KEY";
+ // If present in the saved instance bundle, indicates where to set the playback slider.
+ private static final String CLIP_POSITION_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".CLIP_POSITION_KEY";
+ private static final String IS_SPEAKERPHONE_ON_KEY =
+ VoicemailPlaybackPresenter.class.getName() + ".IS_SPEAKER_PHONE_ON";
+ private static final String VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT = "MM-dd-yy_hhmmaa";
+ private static VoicemailPlaybackPresenter sInstance;
+ private static ScheduledExecutorService mScheduledExecutorService;
+ /**
+ * The most recently cached duration. We cache this since we don't want to keep requesting it from
+ * the player, as this can easily lead to throwing {@link IllegalStateException} (any time the
+ * player is released, it's illegal to ask for the duration).
+ */
+ private final AtomicInteger mDuration = new AtomicInteger(0);
+
+ protected Context mContext;
+ private long mRowId;
+ protected Uri mVoicemailUri;
+ protected MediaPlayer mMediaPlayer;
+ // Used to run async tasks that need to interact with the UI.
+ protected AsyncTaskExecutor mAsyncTaskExecutor;
+ private Activity mActivity;
+ private PlaybackView mView;
+ private int mPosition;
+ private boolean mIsPlaying;
+ // MediaPlayer crashes on some method calls if not prepared but does not have a method which
+ // exposes its prepared state. Store this locally, so we can check and prevent crashes.
+ private boolean mIsPrepared;
+ private boolean mIsSpeakerphoneOn;
+
+ private boolean mShouldResumePlaybackAfterSeeking;
+ /**
+ * Used to handle the result of a successful or time-out fetch result.
+ *
+ * <p>This variable is thread-contained, accessed only on the ui thread.
+ */
+ private FetchResultHandler mFetchResultHandler;
+
+ private PowerManager.WakeLock mProximityWakeLock;
+ private VoicemailAudioManager mVoicemailAudioManager;
+ private OnVoicemailDeletedListener mOnVoicemailDeletedListener;
+
+ /** Initialize variables which are activity-independent and state-independent. */
+ protected VoicemailPlaybackPresenter(Activity activity) {
+ Context context = activity.getApplicationContext();
+ mAsyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor();
+ mVoicemailAudioManager = new VoicemailAudioManager(context, this);
+ PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ if (powerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock =
+ powerManager.newWakeLock(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "VoicemailPlaybackPresenter");
+ }
+ }
+
+ /**
+ * Obtain singleton instance of this class. Use a single instance to provide a consistent listener
+ * to the AudioManager when requesting and abandoning audio focus.
+ *
+ * <p>Otherwise, after rotation the previous listener will still be active but a new listener will
+ * be provided to calls to the AudioManager, which is bad. For example, abandoning audio focus
+ * with the new listeners results in an AUDIO_FOCUS_GAIN callback to the previous listener, which
+ * is the opposite of the intended behavior.
+ */
+ @MainThread
+ public static VoicemailPlaybackPresenter getInstance(
+ Activity activity, Bundle savedInstanceState) {
+ if (sInstance == null) {
+ sInstance = new VoicemailPlaybackPresenter(activity);
+ }
+
+ sInstance.init(activity, savedInstanceState);
+ return sInstance;
+ }
+
+ private static synchronized ScheduledExecutorService getScheduledExecutorServiceInstance() {
+ if (mScheduledExecutorService == null) {
+ mScheduledExecutorService = Executors.newScheduledThreadPool(NUMBER_OF_THREADS_IN_POOL);
+ }
+ return mScheduledExecutorService;
+ }
+
+ /** Update variables which are activity-dependent or state-dependent. */
+ @MainThread
+ protected void init(Activity activity, Bundle savedInstanceState) {
+ Assert.isMainThread();
+ mActivity = activity;
+ mContext = activity;
+
+ if (savedInstanceState != null) {
+ // Restores playback state when activity is recreated, such as after rotation.
+ mVoicemailUri = savedInstanceState.getParcelable(VOICEMAIL_URI_KEY);
+ mIsPrepared = savedInstanceState.getBoolean(IS_PREPARED_KEY);
+ mPosition = savedInstanceState.getInt(CLIP_POSITION_KEY, 0);
+ mIsPlaying = savedInstanceState.getBoolean(IS_PLAYING_STATE_KEY, false);
+ mIsSpeakerphoneOn = savedInstanceState.getBoolean(IS_SPEAKERPHONE_ON_KEY, false);
+ }
+
+ if (mMediaPlayer == null) {
+ mIsPrepared = false;
+ mIsPlaying = false;
+ }
+
+ if (mActivity != null) {
+ if (isPlaying()) {
+ mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+ }
+
+ /** Must be invoked when the parent Activity is saving it state. */
+ public void onSaveInstanceState(Bundle outState) {
+ if (mView != null) {
+ outState.putParcelable(VOICEMAIL_URI_KEY, mVoicemailUri);
+ outState.putBoolean(IS_PREPARED_KEY, mIsPrepared);
+ outState.putInt(CLIP_POSITION_KEY, mView.getDesiredClipPosition());
+ outState.putBoolean(IS_PLAYING_STATE_KEY, mIsPlaying);
+ outState.putBoolean(IS_SPEAKERPHONE_ON_KEY, mIsSpeakerphoneOn);
+ }
+ }
+
+ /** Specify the view which this presenter controls and the voicemail to prepare to play. */
+ public void setPlaybackView(
+ PlaybackView view, long rowId, Uri voicemailUri, final boolean startPlayingImmediately) {
+ mRowId = rowId;
+ mView = view;
+ mView.setPresenter(this, voicemailUri);
+ mView.onSpeakerphoneOn(mIsSpeakerphoneOn);
+
+ // Handles cases where the same entry is binded again when scrolling in list, or where
+ // the MediaPlayer was retained after an orientation change.
+ if (mMediaPlayer != null && mIsPrepared && voicemailUri.equals(mVoicemailUri)) {
+ // If the voicemail card was rebinded, we need to set the position to the appropriate
+ // point. Since we retain the media player, we can just set it to the position of the
+ // media player.
+ mPosition = mMediaPlayer.getCurrentPosition();
+ onPrepared(mMediaPlayer);
+ } else {
+ if (!voicemailUri.equals(mVoicemailUri)) {
+ mVoicemailUri = voicemailUri;
+ mPosition = 0;
+ }
+ /*
+ * Check to see if the content field in the DB is set. If set, we proceed to
+ * prepareContent() method. We get the duration of the voicemail from the query and set
+ * it if the content is not available.
+ */
+ checkForContent(
+ new OnContentCheckedListener() {
+ @Override
+ public void onContentChecked(boolean hasContent) {
+ if (hasContent) {
+ prepareContent();
+ } else {
+ if (startPlayingImmediately) {
+ requestContent(PLAYBACK_REQUEST);
+ }
+ if (mView != null) {
+ mView.resetSeekBar();
+ mView.setClipPosition(0, mDuration.get());
+ }
+ }
+ }
+ });
+
+ if (startPlayingImmediately) {
+ // Since setPlaybackView can get called during the view binding process, we don't
+ // want to reset mIsPlaying to false if the user is currently playing the
+ // voicemail and the view is rebound.
+ mIsPlaying = startPlayingImmediately;
+ }
+ }
+ }
+
+ /** Reset the presenter for playback back to its original state. */
+ public void resetAll() {
+ pausePresenter(true);
+
+ mView = null;
+ mVoicemailUri = null;
+ }
+
+ /**
+ * When navigating away from voicemail playback, we need to release the media player, pause the UI
+ * and save the position.
+ *
+ * @param reset {@code true} if we want to reset the position of the playback, {@code false} if we
+ * want to retain the current position (in case we return to the voicemail).
+ */
+ public void pausePresenter(boolean reset) {
+ pausePlayback();
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ disableProximitySensor(false /* waitForFarState */);
+
+ mIsPrepared = false;
+ mIsPlaying = false;
+
+ if (reset) {
+ // We want to reset the position whether or not the view is valid.
+ mPosition = 0;
+ }
+
+ if (mView != null) {
+ mView.onPlaybackStopped();
+ if (reset) {
+ mView.setClipPosition(0, mDuration.get());
+ } else {
+ mPosition = mView.getDesiredClipPosition();
+ }
+ }
+ }
+
+ /** Must be invoked when the parent activity is resumed. */
+ public void onResume() {
+ mVoicemailAudioManager.registerReceivers();
+ }
+
+ /** Must be invoked when the parent activity is paused. */
+ public void onPause() {
+ mVoicemailAudioManager.unregisterReceivers();
+
+ if (mActivity != null && mIsPrepared && mActivity.isChangingConfigurations()) {
+ // If an configuration change triggers the pause, retain the MediaPlayer.
+ LogUtil.d("VoicemailPlaybackPresenter.onPause", "configuration changed.");
+ return;
+ }
+
+ // Release the media player, otherwise there may be failures.
+ pausePresenter(false);
+ }
+
+ /** Must be invoked when the parent activity is destroyed. */
+ public void onDestroy() {
+ // Clear references to avoid leaks from the singleton instance.
+ mActivity = null;
+ mContext = null;
+
+ if (mScheduledExecutorService != null) {
+ mScheduledExecutorService.shutdown();
+ mScheduledExecutorService = null;
+ }
+
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ mFetchResultHandler = null;
+ }
+ }
+
+ /** Checks to see if we have content available for this voicemail. */
+ protected void checkForContent(final OnContentCheckedListener callback) {
+ mAsyncTaskExecutor.submit(
+ Tasks.CHECK_FOR_CONTENT,
+ new AsyncTask<Void, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ callback.onContentChecked(hasContent);
+ }
+ });
+ }
+
+ private boolean queryHasContent(Uri voicemailUri) {
+ if (voicemailUri == null || mContext == null) {
+ return false;
+ }
+
+ ContentResolver contentResolver = mContext.getContentResolver();
+ Cursor cursor = contentResolver.query(voicemailUri, null, null, null, null);
+ try {
+ if (cursor != null && cursor.moveToNext()) {
+ int duration = cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.DURATION));
+ // Convert database duration (seconds) into mDuration (milliseconds)
+ mDuration.set(duration > 0 ? duration * 1000 : 0);
+ return cursor.getInt(cursor.getColumnIndex(VoicemailContract.Voicemails.HAS_CONTENT)) == 1;
+ }
+ } finally {
+ MoreCloseables.closeQuietly(cursor);
+ }
+ return false;
+ }
+
+ /**
+ * Makes a broadcast request to ask that a voicemail source fetch this content.
+ *
+ * <p>This method <b>must be called on the ui thread</b>.
+ *
+ * <p>This method will be called when we realise that we don't have content for this voicemail. It
+ * will trigger a broadcast to request that the content be downloaded. It will add a listener to
+ * the content resolver so that it will be notified when the has_content field changes. It will
+ * also set a timer. If the has_content field changes to true within the allowed time, we will
+ * proceed to {@link #prepareContent()}. If the has_content field does not become true within the
+ * allowed time, we will update the ui to reflect the fact that content was not available.
+ *
+ * @return whether issued request to fetch content
+ */
+ protected boolean requestContent(int code) {
+ if (mContext == null || mVoicemailUri == null) {
+ return false;
+ }
+
+ FetchResultHandler tempFetchResultHandler =
+ new FetchResultHandler(new Handler(), mVoicemailUri, code);
+
+ switch (code) {
+ default:
+ if (mFetchResultHandler != null) {
+ mFetchResultHandler.destroy();
+ }
+ mView.setIsFetchingContent();
+ mFetchResultHandler = tempFetchResultHandler;
+ break;
+ }
+
+ mAsyncTaskExecutor.submit(
+ Tasks.SEND_FETCH_REQUEST,
+ new AsyncTask<Void, Void, Void>() {
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ try (Cursor cursor =
+ mContext
+ .getContentResolver()
+ .query(
+ mVoicemailUri,
+ new String[] {Voicemails.SOURCE_PACKAGE},
+ null,
+ null,
+ null)) {
+ String sourcePackage;
+ if (!hasContent(cursor)) {
+ LogUtil.e(
+ "VoicemailPlaybackPresenter.requestContent",
+ "mVoicemailUri does not return a SOURCE_PACKAGE");
+ sourcePackage = null;
+ } else {
+ sourcePackage = cursor.getString(0);
+ }
+ // Send voicemail fetch request.
+ Intent intent = new Intent(VoicemailContract.ACTION_FETCH_VOICEMAIL, mVoicemailUri);
+ intent.setPackage(sourcePackage);
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.requestContent",
+ "Sending ACTION_FETCH_VOICEMAIL to " + sourcePackage);
+ mContext.sendBroadcast(intent);
+ }
+ return null;
+ }
+ });
+ return true;
+ }
+
+ /**
+ * Prepares the voicemail content for playback.
+ *
+ * <p>This method will be called once we know that our voicemail has content (according to the
+ * content provider). this method asynchronously tries to prepare the data source through the
+ * media player. If preparation is successful, the media player will {@link #onPrepared()}, and it
+ * will call {@link #onError()} otherwise.
+ */
+ protected void prepareContent() {
+ if (mView == null) {
+ return;
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.prepareContent", null);
+
+ // Release the previous media player, otherwise there may be failures.
+ if (mMediaPlayer != null) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ }
+
+ mView.disableUiElements();
+ mIsPrepared = false;
+
+ try {
+ mMediaPlayer = new MediaPlayer();
+ mMediaPlayer.setOnPreparedListener(this);
+ mMediaPlayer.setOnErrorListener(this);
+ mMediaPlayer.setOnCompletionListener(this);
+
+ mMediaPlayer.reset();
+ mMediaPlayer.setDataSource(mContext, mVoicemailUri);
+ mMediaPlayer.setAudioStreamType(VoicemailAudioManager.PLAYBACK_STREAM);
+ mMediaPlayer.prepareAsync();
+ } catch (IOException e) {
+ handleError(e);
+ }
+ }
+
+ /**
+ * Once the media player is prepared, enables the UI and adopts the appropriate playback state.
+ */
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ if (mView == null || mContext == null) {
+ return;
+ }
+ LogUtil.d("VoicemailPlaybackPresenter.onPrepared", null);
+ mIsPrepared = true;
+
+ mDuration.set(mMediaPlayer.getDuration());
+
+ LogUtil.d("VoicemailPlaybackPresenter.onPrepared", "mPosition=" + mPosition);
+ mView.setClipPosition(mPosition, mDuration.get());
+ mView.enableUiElements();
+ mView.setSuccess();
+ mMediaPlayer.seekTo(mPosition);
+
+ if (mIsPlaying) {
+ resumePlayback();
+ } else {
+ pausePlayback();
+ }
+ }
+
+ /**
+ * Invoked if preparing the media player fails, for example, if file is missing or the voicemail
+ * is an unknown file format that can't be played.
+ */
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ handleError(new IllegalStateException("MediaPlayer error listener invoked: " + extra));
+ return true;
+ }
+
+ protected void handleError(Exception e) {
+ LogUtil.e("VoicemailPlaybackPresenter.handlerError", "could not play voicemail", e);
+
+ if (mIsPrepared) {
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ mIsPrepared = false;
+ }
+
+ if (mView != null) {
+ mView.onPlaybackError();
+ }
+
+ mPosition = 0;
+ mIsPlaying = false;
+ }
+
+ /** After done playing the voicemail clip, reset the clip position to the start. */
+ @Override
+ public void onCompletion(MediaPlayer mediaPlayer) {
+ pausePlayback();
+
+ // Reset the seekbar position to the beginning.
+ mPosition = 0;
+ if (mView != null) {
+ mediaPlayer.seekTo(0);
+ mView.setClipPosition(0, mDuration.get());
+ }
+ }
+
+ /**
+ * Only play voicemail when audio focus is granted. When it is lost (usually by another
+ * application requesting focus), pause playback. Audio focus gain/lost only triggers the focus is
+ * requested. Audio focus is requested when the user pressed play and abandoned when the user
+ * pressed pause or the audio has finished. Losing focus should not abandon focus as the voicemail
+ * should resume once the focus is returned.
+ *
+ * @param gainedFocus {@code true} if the audio focus was gained, {@code} false otherwise.
+ */
+ public void onAudioFocusChange(boolean gainedFocus) {
+ if (mIsPlaying == gainedFocus) {
+ // Nothing new here, just exit.
+ return;
+ }
+
+ if (gainedFocus) {
+ resumePlayback();
+ } else {
+ pausePlayback(true);
+ }
+ }
+
+ /**
+ * Resumes voicemail playback at the clip position stored by the presenter. Null-op if already
+ * playing.
+ */
+ public void resumePlayback() {
+ if (mView == null) {
+ return;
+ }
+
+ if (!mIsPrepared) {
+ /*
+ * Check content before requesting content to avoid duplicated requests. It is possible
+ * that the UI doesn't know content has arrived if the fetch took too long causing a
+ * timeout, but succeeded.
+ */
+ checkForContent(
+ new OnContentCheckedListener() {
+ @Override
+ public void onContentChecked(boolean hasContent) {
+ if (!hasContent) {
+ // No local content, download from server. Queue playing if the request was
+ // issued,
+ mIsPlaying = requestContent(PLAYBACK_REQUEST);
+ } else {
+ // Queue playing once the media play loaded the content.
+ mIsPlaying = true;
+ prepareContent();
+ }
+ }
+ });
+ return;
+ }
+
+ mIsPlaying = true;
+
+ mActivity.getWindow().addFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+ if (mMediaPlayer != null && !mMediaPlayer.isPlaying()) {
+ // Clamp the start position between 0 and the duration.
+ mPosition = Math.max(0, Math.min(mPosition, mDuration.get()));
+
+ mMediaPlayer.seekTo(mPosition);
+
+ try {
+ // Grab audio focus.
+ // Can throw RejectedExecutionException.
+ mVoicemailAudioManager.requestAudioFocus();
+ mMediaPlayer.start();
+ setSpeakerphoneOn(mIsSpeakerphoneOn);
+ mVoicemailAudioManager.setSpeakerphoneOn(mIsSpeakerphoneOn);
+ } catch (RejectedExecutionException e) {
+ handleError(e);
+ }
+ }
+
+ LogUtil.d("VoicemailPlaybackPresenter.resumePlayback", "resumed playback at %d.", mPosition);
+ mView.onPlaybackStarted(mDuration.get(), getScheduledExecutorServiceInstance());
+ }
+
+ /** Pauses voicemail playback at the current position. Null-op if already paused. */
+ public void pausePlayback() {
+ pausePlayback(false);
+ }
+
+ private void pausePlayback(boolean keepFocus) {
+ if (!mIsPrepared) {
+ return;
+ }
+
+ mIsPlaying = false;
+
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ }
+
+ mPosition = mMediaPlayer == null ? 0 : mMediaPlayer.getCurrentPosition();
+
+ LogUtil.d("VoicemailPlaybackPresenter.pausePlayback", "paused playback at %d.", mPosition);
+
+ if (mView != null) {
+ mView.onPlaybackStopped();
+ }
+
+ if (!keepFocus) {
+ mVoicemailAudioManager.abandonAudioFocus();
+ }
+ if (mActivity != null) {
+ mActivity.getWindow().clearFlags(LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ disableProximitySensor(true /* waitForFarState */);
+ }
+
+ /**
+ * Pauses playback when the user starts seeking the position, and notes whether the voicemail is
+ * playing to know whether to resume playback once the user selects a new position.
+ */
+ public void pausePlaybackForSeeking() {
+ if (mMediaPlayer != null) {
+ mShouldResumePlaybackAfterSeeking = mMediaPlayer.isPlaying();
+ }
+ pausePlayback(true);
+ }
+
+ public void resumePlaybackAfterSeeking(int desiredPosition) {
+ mPosition = desiredPosition;
+ if (mShouldResumePlaybackAfterSeeking) {
+ mShouldResumePlaybackAfterSeeking = false;
+ resumePlayback();
+ }
+ }
+
+ /**
+ * Seek to position. This is called when user manually seek the playback. It could be either by
+ * touch or volume button while in talkback mode.
+ */
+ public void seek(int position) {
+ mPosition = position;
+ mMediaPlayer.seekTo(mPosition);
+ }
+
+ private void enableProximitySensor() {
+ if (mProximityWakeLock == null
+ || mIsSpeakerphoneOn
+ || !mIsPrepared
+ || mMediaPlayer == null
+ || !mMediaPlayer.isPlaying()) {
+ return;
+ }
+
+ if (!mProximityWakeLock.isHeld()) {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.enableProximitySensor", "acquiring proximity wake lock");
+ mProximityWakeLock.acquire();
+ } else {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.enableProximitySensor",
+ "proximity wake lock already acquired");
+ }
+ }
+
+ private void disableProximitySensor(boolean waitForFarState) {
+ if (mProximityWakeLock == null) {
+ return;
+ }
+ if (mProximityWakeLock.isHeld()) {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.disableProximitySensor", "releasing proximity wake lock");
+ int flags = waitForFarState ? PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY : 0;
+ mProximityWakeLock.release(flags);
+ } else {
+ LogUtil.i(
+ "VoicemailPlaybackPresenter.disableProximitySensor",
+ "proximity wake lock already released");
+ }
+ }
+
+ /** This is for use by UI interactions only. It simplifies UI logic. */
+ public void toggleSpeakerphone() {
+ mVoicemailAudioManager.setSpeakerphoneOn(!mIsSpeakerphoneOn);
+ setSpeakerphoneOn(!mIsSpeakerphoneOn);
+ }
+
+ public void setOnVoicemailDeletedListener(OnVoicemailDeletedListener listener) {
+ mOnVoicemailDeletedListener = listener;
+ }
+
+ public int getMediaPlayerPosition() {
+ return mIsPrepared && mMediaPlayer != null ? mMediaPlayer.getCurrentPosition() : 0;
+ }
+
+ void onVoicemailDeleted(CallLogListItemViewHolder viewHolder) {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeleted(viewHolder, mVoicemailUri);
+ }
+ }
+
+ void onVoicemailDeleteUndo(int adapterPosition) {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeleteUndo(mRowId, adapterPosition, mVoicemailUri);
+ }
+ }
+
+ void onVoicemailDeletedInDatabase() {
+ if (mOnVoicemailDeletedListener != null) {
+ mOnVoicemailDeletedListener.onVoicemailDeletedInDatabase(mRowId, mVoicemailUri);
+ }
+ }
+
+ @VisibleForTesting
+ public boolean isPlaying() {
+ return mIsPlaying;
+ }
+
+ @VisibleForTesting
+ public boolean isSpeakerphoneOn() {
+ return mIsSpeakerphoneOn;
+ }
+
+ /**
+ * This method only handles app-level changes to the speakerphone. Audio layer changes should be
+ * handled separately. This is so that the VoicemailAudioManager can trigger changes to the
+ * presenter without the presenter triggering the audio manager and duplicating actions.
+ */
+ public void setSpeakerphoneOn(boolean on) {
+ if (mView == null) {
+ return;
+ }
+
+ mView.onSpeakerphoneOn(on);
+
+ mIsSpeakerphoneOn = on;
+
+ // This should run even if speakerphone is not being toggled because we may be switching
+ // from earpiece to headphone and vise versa. Also upon initial setup the default audio
+ // source is the earpiece, so we want to trigger the proximity sensor.
+ if (mIsPlaying) {
+ if (on || mVoicemailAudioManager.isWiredHeadsetPluggedIn()) {
+ disableProximitySensor(false /* waitForFarState */);
+ } else {
+ enableProximitySensor();
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void clearInstance() {
+ sInstance = null;
+ }
+
+ /**
+ * Share voicemail to be opened by user selected apps. This method will collect information, copy
+ * voicemail to a temporary file in background and launch a chooser intent to share it.
+ */
+ @TargetApi(VERSION_CODES.M)
+ public void shareVoicemail() {
+ mAsyncTaskExecutor.submit(
+ Tasks.SHARE_VOICEMAIL,
+ new AsyncTask<Void, Void, Uri>() {
+ @Nullable
+ @Override
+ protected Uri doInBackground(Void... params) {
+ ContentResolver contentResolver = mContext.getContentResolver();
+ try (Cursor callLogInfo = getCallLogInfoCursor(contentResolver, mVoicemailUri);
+ Cursor contentInfo = getContentInfoCursor(contentResolver, mVoicemailUri)) {
+
+ if (hasContent(callLogInfo) && hasContent(contentInfo)) {
+ String cachedName = callLogInfo.getString(CallLogQuery.CACHED_NAME);
+ String number =
+ contentInfo.getString(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.NUMBER));
+ long date =
+ contentInfo.getLong(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.DATE));
+ String mimeType =
+ contentInfo.getString(
+ contentInfo.getColumnIndex(VoicemailContract.Voicemails.MIME_TYPE));
+
+ // Copy voicemail content to a new file.
+ // Please see reference in third_party/java_src/android_app/dialer/java/com/android/
+ // dialer/app/res/xml/file_paths.xml for correct cache directory name.
+ File parentDir = new File(mContext.getCacheDir(), "my_cache");
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ File temporaryVoicemailFile =
+ new File(parentDir, getFileName(cachedName, number, mimeType, date));
+
+ try (InputStream inputStream = contentResolver.openInputStream(mVoicemailUri);
+ OutputStream outputStream =
+ contentResolver.openOutputStream(Uri.fromFile(temporaryVoicemailFile))) {
+ if (inputStream != null && outputStream != null) {
+ ByteStreams.copy(inputStream, outputStream);
+ return FileProvider.getUriForFile(
+ mContext,
+ Constants.get().getFileProviderAuthority(),
+ temporaryVoicemailFile);
+ }
+ } catch (IOException e) {
+ LogUtil.e(
+ "VoicemailAsyncTaskUtil.shareVoicemail",
+ "failed to copy voicemail content to new file: ",
+ e);
+ }
+ return null;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Uri uri) {
+ if (uri == null) {
+ LogUtil.e("VoicemailAsyncTaskUtil.shareVoicemail", "failed to get voicemail");
+ } else {
+ mContext.startActivity(
+ Intent.createChooser(
+ getShareIntent(mContext, uri),
+ mContext.getResources().getText(R.string.call_log_action_share_voicemail)));
+ }
+ }
+ });
+ }
+
+ private static String getFileName(String cachedName, String number, String mimeType, long date) {
+ String callerName = TextUtils.isEmpty(cachedName) ? number : cachedName;
+ SimpleDateFormat simpleDateFormat =
+ new SimpleDateFormat(VOICEMAIL_SHARE_FILE_NAME_DATE_FORMAT, Locale.getDefault());
+
+ String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+
+ return callerName
+ + "_"
+ + simpleDateFormat.format(new Date(date))
+ + (TextUtils.isEmpty(fileExtension) ? "" : "." + fileExtension);
+ }
+
+ private static Intent getShareIntent(Context context, Uri voicemailFileUri) {
+ Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_STREAM, voicemailFileUri);
+ shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ shareIntent.setType(context.getContentResolver().getType(voicemailFileUri));
+ return shareIntent;
+ }
+
+ private static boolean hasContent(@Nullable Cursor cursor) {
+ return cursor != null && cursor.moveToFirst();
+ }
+
+ @Nullable
+ private static Cursor getCallLogInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
+ return contentResolver.query(
+ ContentUris.withAppendedId(
+ CallLog.Calls.CONTENT_URI_WITH_VOICEMAIL, ContentUris.parseId(voicemailUri)),
+ CallLogQuery.getProjection(),
+ null,
+ null,
+ null);
+ }
+
+ @Nullable
+ private static Cursor getContentInfoCursor(ContentResolver contentResolver, Uri voicemailUri) {
+ return contentResolver.query(
+ voicemailUri,
+ new String[] {
+ VoicemailContract.Voicemails._ID,
+ VoicemailContract.Voicemails.NUMBER,
+ VoicemailContract.Voicemails.DATE,
+ VoicemailContract.Voicemails.MIME_TYPE,
+ },
+ null,
+ null,
+ null);
+ }
+
+ /** The enumeration of {@link AsyncTask} objects we use in this class. */
+ public enum Tasks {
+ CHECK_FOR_CONTENT,
+ CHECK_CONTENT_AFTER_CHANGE,
+ SHARE_VOICEMAIL,
+ SEND_FETCH_REQUEST
+ }
+
+ /** Contract describing the behaviour we need from the ui we are controlling. */
+ public interface PlaybackView {
+
+ int getDesiredClipPosition();
+
+ void disableUiElements();
+
+ void enableUiElements();
+
+ void onPlaybackError();
+
+ void onPlaybackStarted(int duration, ScheduledExecutorService executorService);
+
+ void onPlaybackStopped();
+
+ void onSpeakerphoneOn(boolean on);
+
+ void setClipPosition(int clipPositionInMillis, int clipLengthInMillis);
+
+ void setSuccess();
+
+ void setFetchContentTimeout();
+
+ void setIsFetchingContent();
+
+ void setPresenter(VoicemailPlaybackPresenter presenter, Uri voicemailUri);
+
+ void resetSeekBar();
+ }
+
+ public interface OnVoicemailDeletedListener {
+
+ void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri);
+
+ void onVoicemailDeleteUndo(long rowId, int adaptorPosition, Uri uri);
+
+ void onVoicemailDeletedInDatabase(long rowId, Uri uri);
+ }
+
+ protected interface OnContentCheckedListener {
+
+ void onContentChecked(boolean hasContent);
+ }
+
+ @ThreadSafe
+ private class FetchResultHandler extends ContentObserver implements Runnable {
+
+ private final Handler mFetchResultHandler;
+ private final Uri mVoicemailUri;
+ private AtomicBoolean mIsWaitingForResult = new AtomicBoolean(true);
+
+ public FetchResultHandler(Handler handler, Uri uri, int code) {
+ super(handler);
+ mFetchResultHandler = handler;
+ mVoicemailUri = uri;
+ if (mContext != null) {
+ mContext.getContentResolver().registerContentObserver(mVoicemailUri, false, this);
+ mFetchResultHandler.postDelayed(this, FETCH_CONTENT_TIMEOUT_MS);
+ }
+ }
+
+ /** Stop waiting for content and notify UI if {@link FETCH_CONTENT_TIMEOUT_MS} has elapsed. */
+ @Override
+ public void run() {
+ if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ if (mView != null) {
+ mView.setFetchContentTimeout();
+ }
+ }
+ }
+
+ public void destroy() {
+ if (mIsWaitingForResult.getAndSet(false) && mContext != null) {
+ mContext.getContentResolver().unregisterContentObserver(this);
+ mFetchResultHandler.removeCallbacks(this);
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ mAsyncTaskExecutor.submit(
+ Tasks.CHECK_CONTENT_AFTER_CHANGE,
+ new AsyncTask<Void, Void, Boolean>() {
+
+ @Override
+ public Boolean doInBackground(Void... params) {
+ return queryHasContent(mVoicemailUri);
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasContent) {
+ if (hasContent && mContext != null && mIsWaitingForResult.getAndSet(false)) {
+ mContext.getContentResolver().unregisterContentObserver(FetchResultHandler.this);
+ prepareContent();
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java b/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java
new file mode 100644
index 000000000..24d4c6ff7
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/WiredHeadsetManager.java
@@ -0,0 +1,88 @@
+/*
+ * 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.voicemail;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.util.Log;
+
+/** Listens for and caches headset state. */
+class WiredHeadsetManager {
+
+ private static final String TAG = WiredHeadsetManager.class.getSimpleName();
+ private final WiredHeadsetBroadcastReceiver mReceiver;
+ private boolean mIsPluggedIn;
+ private Listener mListener;
+ private Context mContext;
+
+ WiredHeadsetManager(Context context) {
+ mContext = context;
+ mReceiver = new WiredHeadsetBroadcastReceiver();
+
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ mIsPluggedIn = audioManager.isWiredHeadsetOn();
+ }
+
+ void setListener(Listener listener) {
+ mListener = listener;
+ }
+
+ boolean isPluggedIn() {
+ return mIsPluggedIn;
+ }
+
+ void registerReceiver() {
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
+ mContext.registerReceiver(mReceiver, intentFilter);
+ }
+
+ void unregisterReceiver() {
+ mContext.unregisterReceiver(mReceiver);
+ }
+
+ private void onHeadsetPluggedInChanged(boolean isPluggedIn) {
+ if (mIsPluggedIn != isPluggedIn) {
+ Log.v(TAG, "onHeadsetPluggedInChanged, mIsPluggedIn: " + mIsPluggedIn + " -> " + isPluggedIn);
+ boolean oldIsPluggedIn = mIsPluggedIn;
+ mIsPluggedIn = isPluggedIn;
+ if (mListener != null) {
+ mListener.onWiredHeadsetPluggedInChanged(oldIsPluggedIn, mIsPluggedIn);
+ }
+ }
+ }
+
+ interface Listener {
+
+ void onWiredHeadsetPluggedInChanged(boolean oldIsPluggedIn, boolean newIsPluggedIn);
+ }
+
+ /** Receiver for wired headset plugged and unplugged events. */
+ private class WiredHeadsetBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (AudioManager.ACTION_HEADSET_PLUG.equals(intent.getAction())) {
+ boolean isPluggedIn = intent.getIntExtra("state", 0) == 1;
+ Log.v(TAG, "ACTION_HEADSET_PLUG event, plugged in: " + isPluggedIn);
+ onHeadsetPluggedInChanged(isPluggedIn);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
new file mode 100644
index 000000000..65d043034
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.app.voicemail.error">
+
+ <uses-permission android:name="android.permission.CALL_PHONE"/>
+</manifest>
diff --git a/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
new file mode 100644
index 000000000..e36406d17
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java
@@ -0,0 +1,177 @@
+/*
+ * 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.voicemail.error;
+
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Create error message from {@link VoicemailStatus} for OMTP visual voicemail. This is also the
+ * default behavior if other message creator does not handle the status.
+ */
+public class OmtpVoicemailMessageCreator {
+
+ private static final float QUOTA_NEAR_FULL_THRESHOLD = 0.9f;
+ private static final float QUOTA_FULL_THRESHOLD = 0.99f;
+
+ @Nullable
+ public static VoicemailErrorMessage create(Context context, VoicemailStatus status) {
+ if (Status.CONFIGURATION_STATE_OK == status.configurationState
+ && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
+ && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
+
+ return checkQuota(context, status);
+ }
+ // Initial state when the source is activating. Other error might be written into data and
+ // notification channel during activation.
+ if (Status.CONFIGURATION_STATE_CONFIGURING == status.configurationState
+ && Status.DATA_CHANNEL_STATE_OK == status.dataChannelState
+ && Status.NOTIFICATION_CHANNEL_STATE_OK == status.notificationChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_activating_title),
+ context.getString(R.string.voicemail_error_activating_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context));
+ }
+
+ if (Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION == status.notificationChannelState) {
+ return createNoSignalMessage(context, status);
+ }
+
+ if (Status.CONFIGURATION_STATE_FAILED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_activation_failed_title),
+ context.getString(R.string.voicemail_error_activation_failed_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_no_data_title),
+ context.getString(R.string.voicemail_error_no_data_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_no_data_title),
+ context.getString(R.string.voicemail_error_no_data_cellular_required_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_bad_config_title),
+ context.getString(R.string.voicemail_error_bad_config_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_communication_title),
+ context.getString(R.string.voicemail_error_communication_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_SERVER_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_server_title),
+ context.getString(R.string.voicemail_error_server_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ if (Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_server_connection_title),
+ context.getString(R.string.voicemail_error_server_connection_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ VoicemailErrorMessage.createRetryAction(context, status));
+ }
+
+ // This should be an assertion error, but there's a bug in NYC-DR (b/31069259) that will
+ // sometimes give status mixed from multiple SIMs. There's no meaningful message to be displayed
+ // from it, so just suppress the message.
+ LogUtil.e("OmtpVoicemailMessageCreator.create", "Unhandled status: " + status);
+ return null;
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage checkQuota(Context context, VoicemailStatus status) {
+ if (status.quotaOccupied != Status.QUOTA_UNAVAILABLE
+ && status.quotaTotal != Status.QUOTA_UNAVAILABLE) {
+ if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_FULL_THRESHOLD) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_inbox_full_title),
+ context.getString(R.string.voicemail_error_inbox_full_message));
+ }
+
+ if ((float) status.quotaOccupied / (float) status.quotaTotal >= QUOTA_NEAR_FULL_THRESHOLD) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_inbox_near_full_title),
+ context.getString(R.string.voicemail_error_inbox_near_full_message));
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage createNoSignalMessage(
+ Context context, VoicemailStatus status) {
+ CharSequence title;
+ CharSequence description;
+ List<Action> actions = new ArrayList<>();
+ if (Status.CONFIGURATION_STATE_OK == status.configurationState) {
+ if (Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED == status.dataChannelState) {
+ title = context.getString(R.string.voicemail_error_no_signal_title);
+ description =
+ context.getString(R.string.voicemail_error_no_signal_cellular_required_message);
+ } else {
+ title = context.getString(R.string.voicemail_error_no_signal_title);
+ if (status.isAirplaneMode) {
+ description = context.getString(R.string.voicemail_error_no_signal_airplane_mode_message);
+ } else {
+ description = context.getString(R.string.voicemail_error_no_signal_message);
+ }
+ actions.add(VoicemailErrorMessage.createSyncAction(context, status));
+ }
+ } else {
+ title = context.getString(R.string.voicemail_error_not_activate_no_signal_title);
+ if (status.isAirplaneMode) {
+ description =
+ context.getString(
+ R.string.voicemail_error_not_activate_no_signal_airplane_mode_message);
+ } else {
+ description = context.getString(R.string.voicemail_error_not_activate_no_signal_message);
+ actions.add(VoicemailErrorMessage.createRetryAction(context, status));
+ }
+ }
+ if (status.isAirplaneMode) {
+ actions.add(VoicemailErrorMessage.createChangeAirplaneModeAction(context));
+ }
+ return new VoicemailErrorMessage(title, description, actions);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java
new file mode 100644
index 000000000..d34a0f3c7
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java
@@ -0,0 +1,165 @@
+/*
+ * 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.voicemail.error;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.view.View;
+import android.widget.TextView;
+import com.android.dialer.app.alert.AlertManager;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.List;
+
+/**
+ * UI for the voicemail error message, which will be inserted to the top of the voicemail tab if any
+ * occurred.
+ */
+public class VoicemailErrorAlert {
+
+ private final Context context;
+ private final AlertManager alertManager;
+ private final VoicemailErrorMessageCreator messageCreator;
+
+ private final View view;
+ private final TextView header;
+ private final TextView details;
+ private final TextView primaryAction;
+ private final TextView secondaryAction;
+ private final TextView primaryActionRaised;
+ private final TextView secondaryActionRaised;
+ private final AlertManager modalAlertManager;
+ private View modalView;
+
+ public VoicemailErrorAlert(
+ Context context,
+ AlertManager alertManager,
+ AlertManager modalAlertManager,
+ VoicemailErrorMessageCreator messageCreator) {
+ this.context = context;
+ this.alertManager = alertManager;
+ this.modalAlertManager = modalAlertManager;
+ this.messageCreator = messageCreator;
+
+ view = alertManager.inflate(R.layout.voicemai_error_message_fragment);
+ header = (TextView) view.findViewById(R.id.error_card_header);
+ details = (TextView) view.findViewById(R.id.error_card_details);
+ primaryAction = (TextView) view.findViewById(R.id.primary_action);
+ secondaryAction = (TextView) view.findViewById(R.id.secondary_action);
+ primaryActionRaised = (TextView) view.findViewById(R.id.primary_action_raised);
+ secondaryActionRaised = (TextView) view.findViewById(R.id.secondary_action_raised);
+ }
+
+ public void updateStatus(List<VoicemailStatus> statuses, VoicemailStatusReader statusReader) {
+ LogUtil.i("VoicemailErrorAlert.updateStatus", "%d status", statuses.size());
+ VoicemailErrorMessage message = null;
+ view.setVisibility(View.VISIBLE);
+ for (VoicemailStatus status : statuses) {
+ message = messageCreator.create(context, status, statusReader);
+ if (message != null) {
+ break;
+ }
+ }
+
+ alertManager.clear();
+ modalAlertManager.clear();
+ if (message != null) {
+ LogUtil.i(
+ "VoicemailErrorAlert.updateStatus",
+ "isModal: %b, %s",
+ message.isModal(),
+ message.getTitle());
+ if (message.isModal()) {
+ if (message instanceof VoicemailTosMessage) {
+ modalView = getTosView(modalAlertManager, (VoicemailTosMessage) message);
+ } else {
+ throw new IllegalArgumentException("Modal message type is undefined!");
+ }
+ modalAlertManager.add(modalView);
+ } else {
+ loadMessage(message);
+ alertManager.add(view);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public View getView() {
+ return view;
+ }
+
+ @VisibleForTesting
+ public View getModalView() {
+ return modalView;
+ }
+
+ void loadMessage(VoicemailErrorMessage message) {
+ header.setText(message.getTitle());
+ details.setText(message.getDescription());
+ bindActions(message);
+ }
+
+ private View getTosView(AlertManager alertManager, VoicemailTosMessage message) {
+ View view = alertManager.inflate(R.layout.voicemail_tos_fragment);
+ TextView tosTitle = (TextView) view.findViewById(R.id.tos_message_title);
+ tosTitle.setText(message.getTitle());
+ TextView tosDetails = (TextView) view.findViewById(R.id.tos_message_details);
+ tosDetails.setText(message.getDescription());
+
+ Assert.checkArgument(message.getActions().size() == 2);
+ Action primaryAction = message.getActions().get(0);
+ TextView primaryButton = (TextView) view.findViewById(R.id.voicemail_tos_button_decline);
+ primaryButton.setText(primaryAction.getText());
+ primaryButton.setOnClickListener(primaryAction.getListener());
+ Action secondaryAction = message.getActions().get(1);
+ TextView secondaryButton = (TextView) view.findViewById(R.id.voicemail_tos_button_accept);
+ secondaryButton.setText(secondaryAction.getText());
+ secondaryButton.setOnClickListener(secondaryAction.getListener());
+ return view;
+ }
+
+ /**
+ * Attach actions to buttons until all buttons are assigned. If there are not enough actions the
+ * rest of the buttons will be removed. If there are more actions then buttons the extra actions
+ * will be dropped. {@link VoicemailErrorMessage#getActions()} will specify what actions should be
+ * shown and in what order.
+ */
+ private void bindActions(VoicemailErrorMessage message) {
+ TextView[] buttons = new TextView[] {primaryAction, secondaryAction};
+ TextView[] raisedButtons = new TextView[] {primaryActionRaised, secondaryActionRaised};
+ for (int i = 0; i < buttons.length; i++) {
+ if (message.getActions() != null && i < message.getActions().size()) {
+ VoicemailErrorMessage.Action action = message.getActions().get(i);
+ TextView button;
+ if (action.isRaised()) {
+ button = raisedButtons[i];
+ buttons[i].setVisibility(View.GONE);
+ } else {
+ button = buttons[i];
+ raisedButtons[i].setVisibility(View.GONE);
+ }
+ button.setText(action.getText());
+ button.setOnClickListener(action.getListener());
+ button.setVisibility(View.VISIBLE);
+ } else {
+ buttons[i].setVisibility(View.GONE);
+ raisedButtons[i].setVisibility(View.GONE);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
new file mode 100644
index 000000000..61572008b
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java
@@ -0,0 +1,178 @@
+/*
+ * 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.voicemail.error;
+
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.VoicemailContract;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.CallUtil;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Represents an error determined from the current {@link
+ * android.provider.VoicemailContract.Status}. The message will contain a title, a description, and
+ * a list of actions that can be performed.
+ */
+public class VoicemailErrorMessage {
+
+ private final CharSequence title;
+ private final CharSequence description;
+ private final List<Action> actions;
+
+ private boolean modal;
+
+ /** Something the user can click on to resolve an error, such as retrying or calling voicemail */
+ public static class Action {
+
+ private final CharSequence text;
+ private final View.OnClickListener listener;
+ private final boolean raised;
+
+ public Action(CharSequence text, View.OnClickListener listener) {
+ this(text, listener, false);
+ }
+
+ public Action(CharSequence text, View.OnClickListener listener, boolean raised) {
+ this.text = text;
+ this.listener = listener;
+ this.raised = raised;
+ }
+
+ public CharSequence getText() {
+ return text;
+ }
+
+ public View.OnClickListener getListener() {
+ return listener;
+ }
+
+ public boolean isRaised() {
+ return raised;
+ }
+ }
+
+ public CharSequence getTitle() {
+ return title;
+ }
+
+ public CharSequence getDescription() {
+ return description;
+ }
+
+ @Nullable
+ public List<Action> getActions() {
+ return actions;
+ }
+
+ public boolean isModal() {
+ return modal;
+ }
+
+ public VoicemailErrorMessage setModal(boolean value) {
+ modal = value;
+ return this;
+ }
+
+ public VoicemailErrorMessage(CharSequence title, CharSequence description, Action... actions) {
+ this(title, description, Arrays.asList(actions));
+ }
+
+ public VoicemailErrorMessage(
+ CharSequence title, CharSequence description, @Nullable List<Action> actions) {
+ this.title = title;
+ this.description = description;
+ this.actions = actions;
+ }
+
+ @NonNull
+ public static Action createChangeAirplaneModeAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_turn_off_airplane_mode),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS);
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createSetPinAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_set_pin),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_ALERT_SET_PIN_CLICKED);
+ Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createCallVoicemailAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_call_voicemail),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(Intent.ACTION_CALL, CallUtil.getVoicemailUri());
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createSyncAction(final Context context, final VoicemailStatus status) {
+ return new Action(
+ context.getString(R.string.voicemail_action_sync),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+
+ @NonNull
+ public static Action createRetryAction(final Context context, final VoicemailStatus status) {
+ return new Action(
+ context.getString(R.string.voicemail_action_retry),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent = new Intent(VoicemailContract.ACTION_SYNC_VOICEMAIL);
+ intent.setPackage(status.sourcePackage);
+ context.sendBroadcast(intent);
+ }
+ });
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java
new file mode 100644
index 000000000..5ebef801d
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.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.dialer.app.voicemail.error;
+
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+
+/**
+ * Given a VoicemailStatus, {@link VoicemailErrorMessageCreator#create(Context, VoicemailStatus)}
+ * will return a {@link VoicemailErrorMessage} representing the message to be shown to the user, or
+ * <code>null</code> if no message should be shown.
+ */
+public class VoicemailErrorMessageCreator {
+
+ @Nullable
+ public VoicemailErrorMessage create(
+ Context context, VoicemailStatus status, VoicemailStatusReader statusReader) {
+ // Never return error message before NMR1. Voicemail status is not supported on those.
+ if (VERSION.SDK_INT < VERSION_CODES.N_MR1) {
+ return null;
+ }
+ switch (status.type) {
+ case Vvm3VoicemailMessageCreator.VVM_TYPE_VVM3:
+ return Vvm3VoicemailMessageCreator.create(context, status, statusReader);
+ default:
+ return OmtpVoicemailMessageCreator.create(context, status);
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
new file mode 100644
index 000000000..a09941de2
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatus.java
@@ -0,0 +1,260 @@
+/*
+ * 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.voicemail.error;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import com.android.dialer.database.VoicemailStatusQuery;
+
+/** Structured data from {@link android.provider.VoicemailContract.Status} */
+public class VoicemailStatus {
+
+ public final String sourcePackage;
+ public final String type;
+
+ public final String phoneAccountComponentName;
+ public final String phoneAccountId;
+
+ @Nullable public final Uri settingsUri;
+ @Nullable public final Uri voicemailAccessUri;
+
+ public final int configurationState;
+ public final int dataChannelState;
+ public final int notificationChannelState;
+
+ public final int quotaOccupied;
+ public final int quotaTotal;
+
+ // System status
+
+ public final boolean isAirplaneMode;
+
+ /** Wraps the row currently pointed by <code>statusCursor</code> */
+ public VoicemailStatus(Context context, Cursor statusCursor) {
+ sourcePackage = getString(statusCursor, VoicemailStatusQuery.SOURCE_PACKAGE_INDEX, "");
+
+ settingsUri = getUri(statusCursor, VoicemailStatusQuery.SETTINGS_URI_INDEX);
+ voicemailAccessUri = getUri(statusCursor, VoicemailStatusQuery.VOICEMAIL_ACCESS_URI_INDEX);
+
+ configurationState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.CONFIGURATION_STATE_INDEX,
+ Status.CONFIGURATION_STATE_NOT_CONFIGURED);
+ dataChannelState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.DATA_CHANNEL_STATE_INDEX,
+ Status.DATA_CHANNEL_STATE_NO_CONNECTION);
+ notificationChannelState =
+ getInt(
+ statusCursor,
+ VoicemailStatusQuery.NOTIFICATION_CHANNEL_STATE_INDEX,
+ Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+
+ isAirplaneMode =
+ Settings.System.getInt(context.getContentResolver(), Global.AIRPLANE_MODE_ON, 0) != 0;
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ quotaOccupied =
+ getInt(statusCursor, VoicemailStatusQuery.QUOTA_OCCUPIED_INDEX, Status.QUOTA_UNAVAILABLE);
+ quotaTotal =
+ getInt(statusCursor, VoicemailStatusQuery.QUOTA_TOTAL_INDEX, Status.QUOTA_UNAVAILABLE);
+ } else {
+ quotaOccupied = Status.QUOTA_UNAVAILABLE;
+ quotaTotal = Status.QUOTA_UNAVAILABLE;
+ }
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ type =
+ getString(
+ statusCursor, VoicemailStatusQuery.SOURCE_TYPE_INDEX, TelephonyManager.VVM_TYPE_OMTP);
+ phoneAccountComponentName =
+ getString(statusCursor, VoicemailStatusQuery.PHONE_ACCOUNT_COMPONENT_NAME, "");
+ phoneAccountId = getString(statusCursor, VoicemailStatusQuery.PHONE_ACCOUNT_ID, "");
+ } else {
+ type = TelephonyManager.VVM_TYPE_OMTP;
+ phoneAccountComponentName = "";
+ phoneAccountId = "";
+ }
+ }
+
+ private VoicemailStatus(Builder builder) {
+ sourcePackage = builder.sourcePackage;
+ phoneAccountComponentName = builder.phoneAccountComponentName;
+ phoneAccountId = builder.phoneAccountId;
+ type = builder.type;
+ settingsUri = builder.settingsUri;
+ voicemailAccessUri = builder.voicemailAccessUri;
+ configurationState = builder.configurationState;
+ dataChannelState = builder.dataChannelState;
+ notificationChannelState = builder.notificationChannelState;
+ quotaOccupied = builder.quotaOccupied;
+ quotaTotal = builder.quotaTotal;
+ isAirplaneMode = builder.isAirplaneMode;
+ }
+
+ static class Builder {
+
+ private String sourcePackage = "";
+ private String type = TelephonyManager.VVM_TYPE_OMTP;
+ private String phoneAccountComponentName = "";
+ private String phoneAccountId = "";
+
+ @Nullable private Uri settingsUri;
+ @Nullable private Uri voicemailAccessUri;
+
+ private int configurationState = Status.CONFIGURATION_STATE_NOT_CONFIGURED;
+ private int dataChannelState = Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+ private int notificationChannelState = Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+
+ private int quotaOccupied = Status.QUOTA_UNAVAILABLE;
+ private int quotaTotal = Status.QUOTA_UNAVAILABLE;
+
+ private boolean isAirplaneMode;
+
+ public VoicemailStatus build() {
+ return new VoicemailStatus(this);
+ }
+
+ public Builder setSourcePackage(String sourcePackage) {
+ this.sourcePackage = sourcePackage;
+ return this;
+ }
+
+ public Builder setType(String type) {
+ this.type = type;
+ return this;
+ }
+
+ public Builder setPhoneAccountComponentName(String name) {
+ this.phoneAccountComponentName = name;
+ return this;
+ }
+
+ public Builder setPhoneAccountId(String id) {
+ this.phoneAccountId = id;
+ return this;
+ }
+
+ public Builder setSettingsUri(Uri settingsUri) {
+ this.settingsUri = settingsUri;
+ return this;
+ }
+
+ public Builder setVoicemailAccessUri(Uri voicemailAccessUri) {
+ this.voicemailAccessUri = voicemailAccessUri;
+ return this;
+ }
+
+ public Builder setConfigurationState(int configurationState) {
+ this.configurationState = configurationState;
+ return this;
+ }
+
+ public Builder setDataChannelState(int dataChannelState) {
+ this.dataChannelState = dataChannelState;
+ return this;
+ }
+
+ public Builder setNotificationChannelState(int notificationChannelState) {
+ this.notificationChannelState = notificationChannelState;
+ return this;
+ }
+
+ public Builder setQuotaOccupied(int quotaOccupied) {
+ this.quotaOccupied = quotaOccupied;
+ return this;
+ }
+
+ public Builder setQuotaTotal(int quotaTotal) {
+ this.quotaTotal = quotaTotal;
+ return this;
+ }
+
+ public Builder setAirplaneMode(boolean isAirplaneMode) {
+ this.isAirplaneMode = isAirplaneMode;
+ return this;
+ }
+ }
+
+ public boolean isActive() {
+ switch (configurationState) {
+ case Status.CONFIGURATION_STATE_NOT_CONFIGURED:
+ case Status.CONFIGURATION_STATE_DISABLED:
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "VoicemailStatus["
+ + "sourcePackage: "
+ + sourcePackage
+ + ", type:"
+ + type
+ + ", settingsUri: "
+ + settingsUri
+ + ", voicemailAccessUri: "
+ + voicemailAccessUri
+ + ", configurationState: "
+ + configurationState
+ + ", dataChannelState: "
+ + dataChannelState
+ + ", notificationChannelState: "
+ + notificationChannelState
+ + ", quotaOccupied: "
+ + quotaOccupied
+ + ", quotaTotal: "
+ + quotaTotal
+ + ", isAirplaneMode: "
+ + isAirplaneMode
+ + "]";
+ }
+
+ @Nullable
+ private static Uri getUri(Cursor cursor, int index) {
+ if (cursor.getString(index) != null) {
+ return Uri.parse(cursor.getString(index));
+ }
+ return null;
+ }
+
+ private static int getInt(Cursor cursor, int index, int defaultValue) {
+ if (cursor.isNull(index)) {
+ return defaultValue;
+ }
+ return cursor.getInt(index);
+ }
+
+ private static String getString(Cursor cursor, int index, String defaultValue) {
+ if (cursor.isNull(index)) {
+ return defaultValue;
+ }
+ return cursor.getString(index);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
new file mode 100644
index 000000000..6f411217c
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java
@@ -0,0 +1,114 @@
+/*
+ * 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.voicemail.error;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+
+/**
+ * This class will detect the corruption in the voicemail status and log it so we can track how many
+ * users are affected.
+ */
+public class VoicemailStatusCorruptionHandler {
+
+ /** Where the check is made so logging can be done. */
+ public enum Source {
+ Activity,
+ Notification
+ }
+
+ private static final String CONFIG_VVM_STATUS_FIX_DISABLED = "vvm_status_fix_disabled";
+
+ public static void maybeFixVoicemailStatus(Context context, Cursor statusCursor, Source source) {
+
+ if (ConfigProviderBindings.get(context).getBoolean(CONFIG_VVM_STATUS_FIX_DISABLED, false)) {
+ return;
+ }
+
+ if (VERSION.SDK_INT != VERSION_CODES.N_MR1) {
+ // This issue is specific to N MR1, it is fixed in future SDK.
+ return;
+ }
+
+ if (statusCursor.getCount() == 0) {
+ return;
+ }
+
+ statusCursor.moveToFirst();
+ VoicemailStatus status = new VoicemailStatus(context, statusCursor);
+ PhoneAccountHandle phoneAccountHandle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(status.phoneAccountComponentName),
+ status.phoneAccountId);
+
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+
+ boolean visualVoicemailEnabled =
+ TelephonyManagerCompat.isVisualVoicemailEnabled(telephonyManager, phoneAccountHandle);
+ LogUtil.i(
+ "VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus",
+ "Source="
+ + source
+ + ", CONFIGURATION_STAIE="
+ + status.configurationState
+ + ", visualVoicemailEnabled="
+ + visualVoicemailEnabled);
+
+ // If visual voicemail is enabled, the CONFIGURATION_STATE should be either OK, PIN_NOT_SET,
+ // or other failure code. CONFIGURATION_STATE_NOT_CONFIGURED means that the client has been
+ // shut down improperly (b/32371710). The client should be reset or the VVM tab will be
+ // missing.
+ if (Status.CONFIGURATION_STATE_NOT_CONFIGURED == status.configurationState
+ && visualVoicemailEnabled) {
+ LogUtil.e(
+ "VoicemailStatusCorruptionHandler.maybeFixVoicemailStatus",
+ "VVM3 voicemail status corrupted");
+
+ switch (source) {
+ case Activity:
+ Logger.get(context)
+ .logImpression(
+ DialerImpression.Type
+ .VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY);
+ break;
+ case Notification:
+ Logger.get(context)
+ .logImpression(
+ DialerImpression.Type
+ .VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION);
+ break;
+ default:
+ Assert.fail("this should never happen");
+ break;
+ }
+ // At this point we could attempt to work around the issue by disabling and re-enabling
+ // voicemail. Unfortunately this work around is buggy so we'll do nothing for now.
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java b/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java
new file mode 100644
index 000000000..fd9e7ef25
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.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.voicemail.error;
+
+/**
+ * A source that is generating the voicemail status to show error messages, used by {@link
+ * VoicemailErrorMessageCreator} to inform the source that the status should be updated
+ */
+public interface VoicemailStatusReader {
+ void refresh();
+}
diff --git a/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java
new file mode 100644
index 000000000..86b124419
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java
@@ -0,0 +1,25 @@
+/*
+ * 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.voicemail.error;
+
+/** Voicemail TOS message. */
+public class VoicemailTosMessage extends VoicemailErrorMessage {
+
+ public VoicemailTosMessage(CharSequence title, CharSequence description, Action... actions) {
+ super(title, description, actions);
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
new file mode 100644
index 000000000..6e9405cbf
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java
@@ -0,0 +1,428 @@
+/*
+ * 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.voicemail.error;
+
+import android.app.AlertDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.app.voicemail.error.VoicemailErrorMessage.Action;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import java.util.Locale;
+
+/**
+ * Create error message from {@link VoicemailStatus} for VVM3 visual voicemail. VVM3 is used only by
+ * Verizon Wireless.
+ */
+@RequiresApi(VERSION_CODES.N_MR1)
+public class Vvm3VoicemailMessageCreator {
+
+ public static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+
+ // Copied from com.android.phone.vvm.omtp.protocol.Vvm3EventHandler
+ // TODO(b/28380841): unbundle VVM client so we can access these values directly
+ public static final int VMS_DNS_FAILURE = -9001;
+ public static final int VMG_DNS_FAILURE = -9002;
+ public static final int SPG_DNS_FAILURE = -9003;
+ public static final int VMS_NO_CELLULAR = -9004;
+ public static final int VMG_NO_CELLULAR = -9005;
+ public static final int SPG_NO_CELLULAR = -9006;
+ public static final int VMS_TIMEOUT = -9007;
+ public static final int VMG_TIMEOUT = -9008;
+ public static final int STATUS_SMS_TIMEOUT = -9009;
+
+ public static final int SUBSCRIBER_BLOCKED = -9990;
+ public static final int UNKNOWN_USER = -9991;
+ public static final int UNKNOWN_DEVICE = -9992;
+ public static final int INVALID_PASSWORD = -9993;
+ public static final int MAILBOX_NOT_INITIALIZED = -9994;
+ public static final int SERVICE_NOT_PROVISIONED = -9995;
+ public static final int SERVICE_NOT_ACTIVATED = -9996;
+ public static final int USER_BLOCKED = -9998;
+ public static final int IMAP_GETQUOTA_ERROR = -9997;
+ public static final int IMAP_SELECT_ERROR = -9989;
+ public static final int IMAP_ERROR = -9999;
+
+ public static final int VMG_INTERNAL_ERROR = -101;
+ public static final int VMG_DB_ERROR = -102;
+ public static final int VMG_COMMUNICATION_ERROR = -103;
+ public static final int SPG_URL_NOT_FOUND = -301;
+
+ // Non VVM3 codes:
+ public static final int VMG_UNKNOWN_ERROR = -1;
+ public static final int PIN_NOT_SET = -100;
+ public static final int SUBSCRIBER_UNKNOWN = -99;
+
+ private static final String ISO639_SPANISH = "es";
+ @VisibleForTesting static final String VVM3_TOS_ACCEPTANCE_FLAG_KEY = "vvm3_tos_acceptance_flag";
+
+ @Nullable
+ public static VoicemailErrorMessage create(
+ final Context context,
+ final VoicemailStatus status,
+ final VoicemailStatusReader statusReader) {
+ VoicemailErrorMessage tosMessage = maybeShowTosMessage(context, status, statusReader);
+ if (tosMessage != null) {
+ return tosMessage;
+ }
+
+ if (VMS_DNS_FAILURE == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_DNS_FAILURE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SPG_DNS_FAILURE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_spg_dns_failure_title),
+ getCustomerSupportString(context, R.string.vvm3_error_spg_dns_failure_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMS_NO_CELLULAR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_NO_CELLULAR == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SPG_NO_CELLULAR == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_spg_no_cellular_title),
+ getCustomerSupportString(context, R.string.vvm3_error_spg_no_cellular_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMS_TIMEOUT == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vms_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vms_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (VMG_TIMEOUT == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_vmg_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_vmg_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (STATUS_SMS_TIMEOUT == status.notificationChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_status_sms_timeout_title),
+ getCustomerSupportString(context, R.string.vvm3_error_status_sms_timeout_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SUBSCRIBER_BLOCKED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_subscriber_blocked_title),
+ getCustomerSupportString(context, R.string.vvm3_error_subscriber_blocked_message),
+ VoicemailErrorMessage.createRetryAction(context, status),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (UNKNOWN_USER == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_unknown_user_title),
+ getCustomerSupportString(context, R.string.vvm3_error_unknown_user_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (UNKNOWN_DEVICE == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_unknown_device_title),
+ getCustomerSupportString(context, R.string.vvm3_error_unknown_device_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (INVALID_PASSWORD == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_invalid_password_title),
+ getCustomerSupportString(context, R.string.vvm3_error_invalid_password_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (MAILBOX_NOT_INITIALIZED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_mailbox_not_initialized_title),
+ getCustomerSupportString(context, R.string.vvm3_error_mailbox_not_initialized_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SERVICE_NOT_PROVISIONED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_service_not_provisioned_title),
+ getCustomerSupportString(context, R.string.vvm3_error_service_not_provisioned_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SERVICE_NOT_ACTIVATED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_service_not_activated_title),
+ getCustomerSupportString(context, R.string.vvm3_error_service_not_activated_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (USER_BLOCKED == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_user_blocked_title),
+ getCustomerSupportString(context, R.string.vvm3_error_user_blocked_message),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (SUBSCRIBER_UNKNOWN == status.configurationState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_subscriber_unknown_title),
+ getCustomerSupportString(context, R.string.vvm3_error_subscriber_unknown_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_GETQUOTA_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_getquota_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_getquota_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_SELECT_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_select_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_select_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (IMAP_ERROR == status.dataChannelState) {
+ return new VoicemailErrorMessage(
+ context.getString(R.string.vvm3_error_imap_error_title),
+ getCustomerSupportString(context, R.string.vvm3_error_imap_error_message),
+ VoicemailErrorMessage.createCallVoicemailAction(context),
+ createCallCustomerSupportAction(context));
+ }
+
+ if (PIN_NOT_SET == status.configurationState) {
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_ALERT_SET_PIN_SHOWN);
+ return new VoicemailErrorMessage(
+ context.getString(R.string.voicemail_error_pin_not_set_title),
+ getCustomerSupportString(context, R.string.voicemail_error_pin_not_set_message),
+ VoicemailErrorMessage.createSetPinAction(context));
+ }
+
+ return OmtpVoicemailMessageCreator.create(context, status);
+ }
+
+ @NonNull
+ private static CharSequence getCustomerSupportString(Context context, int id) {
+ // TODO: get number based on the country the user is currently in.
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ context.getResources(),
+ id,
+ context.getString(R.string.verizon_domestic_customer_support_display_number));
+ }
+
+ @NonNull
+ private static Action createCallCustomerSupportAction(final Context context) {
+ return new Action(
+ context.getString(R.string.voicemail_action_call_customer_support),
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ Intent intent =
+ new Intent(
+ Intent.ACTION_CALL,
+ Uri.parse(
+ "tel:"
+ + context.getString(
+ R.string.verizon_domestic_customer_support_number)));
+ context.startActivity(intent);
+ }
+ });
+ }
+
+ @Nullable
+ private static VoicemailErrorMessage maybeShowTosMessage(
+ final Context context,
+ final VoicemailStatus status,
+ final VoicemailStatusReader statusReader) {
+ final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (preferences.getBoolean(VVM3_TOS_ACCEPTANCE_FLAG_KEY, false)) {
+ return null;
+ }
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_SHOWN);
+
+ CharSequence termsAndConditions;
+ CharSequence acceptText;
+ CharSequence declineText;
+ // TODO(b/29082671): use LocaleList
+ if (Locale.getDefault().getLanguage().equals(new Locale(ISO639_SPANISH).getLanguage())) {
+ // Spanish
+ termsAndConditions = context.getString(R.string.verizon_terms_and_conditions_1_1_spanish);
+ acceptText = context.getString(R.string.verizon_terms_and_conditions_accept_spanish);
+ declineText = context.getString(R.string.verizon_terms_and_conditions_decline_spanish);
+ } else {
+ termsAndConditions = context.getString(R.string.verizon_terms_and_conditions_1_1_english);
+ acceptText = context.getString(R.string.verizon_terms_and_conditions_accept_english);
+ declineText = context.getString(R.string.verizon_terms_and_conditions_decline_english);
+ }
+
+ return new VoicemailTosMessage(
+ context.getString(R.string.verizon_terms_and_conditions_title),
+ context.getString(R.string.verizon_terms_and_conditions_message, termsAndConditions),
+ new Action(
+ declineText,
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtil.i("Vvm3VoicemailMessageCreator.maybeShowTosMessage", "decline clicked");
+ PhoneAccountHandle handle =
+ new PhoneAccountHandle(
+ ComponentName.unflattenFromString(status.phoneAccountComponentName),
+ status.phoneAccountId);
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CLICKED);
+ showDeclineTosDialog(context, handle, status);
+ }
+ }),
+ new Action(
+ acceptText,
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ LogUtil.i("Vvm3VoicemailMessageCreator.maybeShowTosMessage", "accept clicked");
+ preferences.edit().putBoolean(VVM3_TOS_ACCEPTANCE_FLAG_KEY, true).apply();
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_ACCEPTED);
+ statusReader.refresh();
+ }
+ },
+ true /* raised */))
+ .setModal(true);
+ }
+
+ private static void showDeclineTosDialog(
+ final Context context, final PhoneAccountHandle handle, VoicemailStatus status) {
+ if (PIN_NOT_SET == status.configurationState) {
+ LogUtil.i(
+ "Vvm3VoicemailMessageCreator.showDeclineTosDialog",
+ "PIN_NOT_SET, showing set PIN dialog");
+ showSetPinBeforeDeclineDialog(context);
+ return;
+ }
+ LogUtil.i(
+ "Vvm3VoicemailMessageCreator.showDeclineTosDialog",
+ "showing decline ToS dialog, status=" + status);
+ final TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.verizon_terms_and_conditions_decline_dialog_message);
+ builder.setPositiveButton(
+ R.string.verizon_terms_and_conditions_decline_dialog_downgrade,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINED);
+ TelephonyManagerCompat.setVisualVoicemailEnabled(telephonyManager, handle, false);
+ }
+ });
+
+ builder.setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setCancelable(true);
+ builder.show();
+ }
+
+ private static void showSetPinBeforeDeclineDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.verizon_terms_and_conditions_decline_set_pin_dialog_message);
+ builder.setPositiveButton(
+ R.string.verizon_terms_and_conditions_decline_set_pin_dialog_set_pin,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.VOICEMAIL_VVM3_TOS_DECLINE_CHANGE_PIN_SHOWN);
+ Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
+ context.startActivity(intent);
+ }
+ });
+
+ builder.setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+
+ builder.setCancelable(true);
+ builder.show();
+ }
+}
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
new file mode 100644
index 000000000..0dfb1c2fd
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<android.support.v7.widget.CardView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/error_card"
+ style="@style/CallLogCardStyle"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/error_card_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/alert_main_padding"
+ android:layout_marginStart="@dimen/alert_main_padding"
+ android:layout_marginEnd="@dimen/alert_main_padding"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/error_card_header"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/alert_title_padding"
+ android:layout_gravity="center_vertical"
+ android:singleLine="false"
+ android:textColor="@color/primary_text_color"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+
+ <TextView
+ android:id="@+id/error_card_details"
+ android:autoLink="web"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lineSpacingExtra="@dimen/alert_line_spacing"
+ android:singleLine="false"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@+id/error_actions"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="20dp"
+ android:paddingTop="@dimen/alert_action_vertical_padding"
+ android:paddingBottom="@dimen/alert_action_vertical_padding"
+ android:paddingStart="@dimen/alert_action_horizontal_padding"
+ android:paddingEnd="@dimen/alert_action_horizontal_padding"
+ android:gravity="start"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/primary_action_raised"
+ style="@style/RaisedErrorActionStyle"
+ android:nextFocusLeft="@+id/promo_card"
+ android:nextFocusRight="@+id/primary_action"
+ android:clickable="true"
+ />
+
+ <TextView
+ android:id="@+id/primary_action"
+ style="@style/ErrorActionStyle"
+ android:background="?android:attr/selectableItemBackground"
+ android:nextFocusLeft="@+id/promo_card"
+ android:nextFocusRight="@+id/secondary_action"
+ android:clickable="true"
+ />
+
+ <TextView
+ android:id="@+id/secondary_action"
+ style="@style/ErrorActionStyle"
+ android:paddingEnd="@dimen/alert_action_between_padding"
+ android:background="?android:attr/selectableItemBackground"
+ android:nextFocusLeft="@+id/primary_action"
+ android:nextFocusRight="@+id/promo_card"
+ android:clickable="true"/>
+
+ <android.support.v4.widget.Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+
+ <TextView
+ android:id="@+id/secondary_action_raised"
+ style="@style/RaisedErrorActionStyle"
+ android:paddingEnd="@dimen/alert_action_between_padding"
+ android:layout_marginEnd="8dp"
+ android:nextFocusLeft="@+id/primary_action"
+ android:nextFocusRight="@+id/promo_card"
+ android:clickable="true"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</android.support.v7.widget.CardView>
diff --git a/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
new file mode 100644
index 000000000..2b9d17328
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml
@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <ScrollView
+ android:id="@+id/voicemail_tos_message"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/tos_message_title"
+ android:textStyle="bold"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="24dp"
+ android:paddingBottom="12dp"
+ android:text="@string/verizon_terms_and_conditions_title"
+ android:textColor="@color/primary_text_color"
+ android:textSize="@dimen/call_log_primary_text_size"/>
+ <TextView
+ android:id="@+id/tos_message_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="16dp"
+ android:autoLink="web"
+ android:text="@string/verizon_terms_and_conditions_1.1_english"
+ android:textColor="@color/secondary_text_color"
+ android:textSize="@dimen/call_log_detail_text_size"/>
+ </LinearLayout>
+ </ScrollView>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#D2D2D2"/>
+
+ <LinearLayout
+ android:id="@+id/voicemail_tos_button"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="horizontal">
+ <TextView
+ android:id="@+id/voicemail_tos_button_decline"
+ style="@style/ErrorActionStyle"
+ android:background="?android:attr/selectableItemBackground"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/verizon_terms_and_conditions_decline_english"/>
+ <android.support.v4.widget.Space
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"/>
+ <TextView
+ android:id="@+id/voicemail_tos_button_accept"
+ style="@style/RaisedErrorActionStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/verizon_terms_and_conditions_accept_english"/>
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml b/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml
new file mode 100644
index 000000000..20dd40a8f
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="alert_icon_size">24dp</dimen>
+ <dimen name="alert_start_padding">16dp</dimen>
+ <dimen name="alert_top_padding">21dp</dimen>
+ <dimen name="alert_main_padding">24dp</dimen>
+ <dimen name="alert_title_padding">12dp</dimen>
+ <dimen name="alert_action_vertical_padding">4dp</dimen>
+ <dimen name="alert_action_horizontal_padding">4dp</dimen>
+ <dimen name="alert_action_between_padding">11dp</dimen>
+ <dimen name="alert_line_spacing">4dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/strings.xml b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
new file mode 100644
index 000000000..1d39b9dcb
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/strings.xml
@@ -0,0 +1,176 @@
+<!--
+ ~ 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
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="voicemail_error_turn_off_airplane_mode_title">Turn off airplane mode</string>
+
+ <string name="voicemail_error_activating_title">Activating visual voicemail</string>
+ <string name="voicemail_error_activating_message">You might not receive voicemail notifications until visual voicemail is fully activated. Call voicemail to retrieve new messages until voicemail is fully activated.</string>
+
+ <string name="voicemail_error_not_activate_no_signal_title">Can\'t activate visual voicemail</string>
+ <string name="voicemail_error_not_activate_no_signal_message">Make sure your phone has cellular connection and try again.</string>
+ <string name="voicemail_error_not_activate_no_signal_airplane_mode_message">Turn off airplane mode and try again.</string>
+
+ <string name="voicemail_error_no_signal_title">No connection</string>
+ <string name="voicemail_error_no_signal_message">You won\'t be notified for new voicemails. If you\'re on Wi-Fi, you can check for voicemail by syncing now.</string>
+ <string name="voicemail_error_no_signal_airplane_mode_message">You won\'t be notified for new voicemails. Turn off airplane mode to sync your voicemail.</string>
+ <string name="voicemail_error_no_signal_cellular_required_message">Your phone needs a cellular data connection to check voicemail.</string>
+
+ <string name="voicemail_error_activation_failed_title">Can\'t activate visual voicemail</string>
+ <string name="voicemail_error_activation_failed_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_no_data_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_no_data_message">Try again when your Wi-Fi or cellular connection is better. You can still call to check voicemail.</string>
+ <string name="voicemail_error_no_data_cellular_required_message">Try again when your cellular data connection is better. You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_bad_config_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_bad_config_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_communication_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_communication_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_server_connection_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_server_connection_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_server_title">Can\'t update visual voicemail</string>
+ <string name="voicemail_error_server_message">You can still call to check voicemail.</string>
+
+ <string name="voicemail_error_inbox_near_full_title">Inbox almost full</string>
+ <string name="voicemail_error_inbox_near_full_message">You won\'t be able to receive new voicemail if your inbox is full.</string>
+
+ <string name="voicemail_error_inbox_full_title">Can\'t receive new voicemails</string>
+ <string name="voicemail_error_inbox_full_message">Your inbox is full. Try deleting some messages to receive new voicemail.</string>
+
+
+ <string name="voicemail_error_pin_not_set_title">Set your voicemail PIN</string>
+ <string name="voicemail_error_pin_not_set_message">You\'ll need a voicemail PIN anytime you call to access your voicemail.</string>
+
+ <string name="voicemail_error_unknown_title">Unknown error</string>
+
+ <string name="voicemail_action_turn_off_airplane_mode">Airplane Mode Settings</string>
+ <string name="voicemail_action_set_pin">Set PIN</string>
+ <string name="voicemail_action_retry">Try Again</string>
+ <string name="voicemail_action_sync">Sync</string>
+ <string name="voicemail_action_call_voicemail">Call Voicemail</string>
+ <string name="voicemail_action_call_customer_support">Call Customer Support</string>
+
+ <string name="vvm3_error_vms_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_vms_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9001.</string>
+
+ <string name="vvm3_error_vmg_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_vmg_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9002.</string>
+
+ <string name="vvm3_error_spg_dns_failure_title">Something Went Wrong</string>
+ <string name="vvm3_error_spg_dns_failure_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9003.</string>
+
+ <string name="vvm3_error_vms_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_vms_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9004.</string>
+
+ <string name="vvm3_error_vmg_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_vmg_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9005.</string>
+
+ <string name="vvm3_error_spg_no_cellular_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_spg_no_cellular_message">Sorry, we\'re having trouble connecting to your voice mailbox. If you\'re in an area with poor signal strength, wait until you have a strong signal and try again. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9006.</string>
+
+ <string name="vvm3_error_vms_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_vms_timeout_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9007.</string>
+
+ <string name="vvm3_error_vmg_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_vmg_timeout_message">Sorry, we ran into a problem. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9008.</string>
+
+ <string name="vvm3_error_status_sms_timeout_title">Something Went Wrong</string>
+ <string name="vvm3_error_status_sms_timeout_message">Sorry, we\'re having trouble setting up your service. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9009.</string>
+
+ <string name="vvm3_error_subscriber_blocked_title">Can\'t Connect to Your Voice Mailbox</string>
+ <string name="vvm3_error_subscriber_blocked_message">Sorry, we\'re not able to connect to your voice mailbox at this time. Please try again later. If there is still a problem, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9990."</string>
+
+ <string name="vvm3_error_unknown_user_title">Set Up Voice Mail</string>
+ <string name="vvm3_error_unknown_user_message">Voicemail is not set up on your account. Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9991.</string>
+
+ <string name="vvm3_error_unknown_device_title">Voice Mail</string>
+ <string name="vvm3_error_unknown_device_message">Visual Voicemail cannot be used on this device. Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9992.</string>
+
+ <string name="vvm3_error_invalid_password_title">Something Went Wrong</string>
+ <string name="vvm3_error_invalid_password_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9993.</string>
+
+ <string name="vvm3_error_mailbox_not_initialized_title">Visual Voice Mail</string>
+ <string name="vvm3_error_mailbox_not_initialized_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9994.</string>
+
+ <string name="vvm3_error_service_not_provisioned_title">Visual Voice Mail</string>
+ <string name="vvm3_error_service_not_provisioned_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9995.</string>
+
+ <string name="vvm3_error_service_not_activated_title">Visual Voice Mail</string>
+ <string name="vvm3_error_service_not_activated_message">To activate Visual Voice Mail, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9996.</string>
+
+ <string name="vvm3_error_user_blocked_title">Something Went Wrong</string>
+ <string name="vvm3_error_user_blocked_message">To complete Visual Voicemail setup, please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9998.</string>
+
+ <string name="vvm3_error_subscriber_unknown_title">Visual Voicemail is Disabled</string>
+ <string name="vvm3_error_subscriber_unknown_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> to activate visual voicemail.</string>
+
+ <string name="vvm3_error_imap_getquota_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_getquota_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9997.</string>
+
+ <string name="vvm3_error_imap_select_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_select_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9989.</string>
+
+ <string name="vvm3_error_imap_error_title">Something Went Wrong</string>
+ <string name="vvm3_error_imap_error_message">Please contact Customer Service at <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> and tell them the error code is 9999.</string>
+
+ <string translatable="false" name="verizon_domestic_customer_support_number">+18009220204</string>
+ <string translatable="false" name="verizon_domestic_customer_support_display_number">(800) 922–0204</string>
+
+ <string name="verizon_terms_and_conditions_title">Visual Voicemail Terms and Conditions</string>
+ <string name="verizon_terms_and_conditions_message">You must accept Verizon Wireless\'s terms and conditions to use visual voicemail:\n\n%s</string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_1.1_english">
+Visual Voice Mail (VVM) is a service that provides access to voice mail messages directly on the device, without the need to call *86. This service requires traditional Voice Mail but does not support all traditional Voice Mail features, which you can access by dialing *86 from your handset. Use of this feature will be billed on a per-megabyte basis, or according to any data package you have. Mobile to mobile minutes do not apply. Standard rates apply to any calls, emails or messages initiated from Visual Voice Mail.\n
+\n
+You may disable VVM in settings. This will revert you to basic voice mail. In some cases you may need to call customer care to cancel and if you cancel Visual Voice Mail you may lose all stored voice mails and information.\n
+\n
+For the Premium Visual Voice Mail service, some voice messages may not be completely transcribed; incomplete messages will end with [...]. Only the first 45 seconds of each voice message will be transcribed, so for longer messages, you will need to listen to the voice message itself. Any profane or offensive language also will not be transcribed and will appear as [...] in the transcription.\n
+\n
+Speech recordings may be collected and stored for a period of 30 days, solely for the purpose of testing and improving transcription technology and performance, subject to the Verizon Wireless Privacy Policy, which can be found at http://www.verizon.com/about/privacy/policy/\n
+\n
+You understand that by selecting ACCEPT, your messages will be stored and anyone in possession of this device will have access to your voice mail. You further understand that your voice mail messages may be stored in electronic format on this device. To limit unauthorized access to your voice mail, you should consider locking your phone when not in use. Not available in all areas or over Wi-Fi.\n
+\n
+If you do not accept all of these terms and conditions, do not use Visual Voice Mail. </string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_1.1_spanish">
+El buzón de voz visual (VVM) es un servicio que permite acceder a los mensajes del buzón de voz directamente en el dispositivo, sin necesidad de llamar al *86. Este servicio requiere el buzón de voz tradicional, pero no admite todas las funciones del buzón de voz tradicional, a las que se puede acceder marcando *86 en el teléfono. El uso de esta función se factura por megabyte o conforme a cualquier paquete de datos que tenga. No se aplican los minutos de un dispositivo móvil a otro. Se aplican tarifas estándar a todos los correos electrónicos, las llamadas o los mensajes originados en el buzón de voz visual.\n
+\n
+Puede inhabilitar el VVM en la configuración. Esto le permite volver al buzón de voz básico. En algunos casos, es posible que deba llamar al servicio de atención al cliente para cancelar el buzón de voz visual. Si lo cancela, puede perder la información y los mensajes de voz almacenados.\n
+\n
+En el caso del servicio de buzón de voz visual premium, es posible que algunos mensajes no se transcriban totalmente; los mensajes incompletos finalizan con "[…]". Solo se transcriben los primeros 45 segundos de cada mensaje de voz, por lo que debe escuchar los mensajes de voz más largos. Tampoco se transcribe ninguna palabra ofensiva o profana; aparece como "[…]" en la transcripción.\n
+\n
+Es posible que reunamos y almacenemos grabaciones de voz durante 30 días, con el único fin de probar y mejorar el rendimiento y la tecnología de la transcripción, sujeto a la Política de privacidad de Verizon Wireless, disponible en http://www.verizon.com/about/privacy/policy/.\n
+\n
+Entiende que, al seleccionar ACEPTAR, sus mensajes se almacenarán, y cualquier persona que disponga de este dispositivo tendrá acceso al buzón de voz. Entiende, además, que los mensajes de voz pueden almacenarse en formato electrónico en este dispositivo. Para limitar el acceso no autorizado al buzón de voz, debe considerar el bloqueo del teléfono cuando no está en uso. No está disponible en todas las áreas ni mediante Wi-Fi.\n
+\n
+Si no acepta todos estos términos y condiciones, no use el buzón de voz visual.
+ </string>
+
+ <string translatable="false" name="verizon_terms_and_conditions_accept_english">Accept</string>
+ <string translatable="false" name="verizon_terms_and_conditions_accept_spanish">Aceptar</string>
+ <string translatable="false" name="verizon_terms_and_conditions_decline_english">Decline</string>
+ <string translatable="false" name="verizon_terms_and_conditions_decline_spanish">Rechazar</string>
+
+ <string name="verizon_terms_and_conditions_decline_dialog_message">Visual voicemail will be disabled if the terms and conditions are declined.</string>
+ <string name="verizon_terms_and_conditions_decline_dialog_downgrade">Disable visual voicemail</string>
+
+ <string name="verizon_terms_and_conditions_decline_set_pin_dialog_message">Voicemail will only be accessible by calling *86. Set a new voicemail PIN to proceed.</string>
+ <string name="verizon_terms_and_conditions_decline_set_pin_dialog_set_pin">Set PIN</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/app/voicemail/error/res/values/styles.xml b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
new file mode 100644
index 000000000..c4a8542f1
--- /dev/null
+++ b/java/com/android/dialer/app/voicemail/error/res/values/styles.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="ErrorActionStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">48dp</item>
+ <item name="android:gravity">end|center_vertical</item>
+ <item name="android:paddingStart">8dp</item>
+ <item name="android:paddingEnd">8dp</item>
+ <item name="android:layout_marginStart">8dp</item>
+ <item name="android:layout_marginEnd">8dp</item>
+ <item name="android:textColor">@color/dialtacts_theme_color</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textSize">14sp</item>
+ </style>
+
+ <style name="RaisedErrorActionStyle" parent="Widget.AppCompat.Button.Colored">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:colorButtonNormal">@color/dialer_theme_color</item>
+ <item name="android:textSize">14sp</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/app/widget/ActionBarController.java b/java/com/android/dialer/app/widget/ActionBarController.java
new file mode 100644
index 000000000..7fe056c51
--- /dev/null
+++ b/java/com/android/dialer/app/widget/ActionBarController.java
@@ -0,0 +1,247 @@
+/*
+ * 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.widget;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.os.Bundle;
+import android.support.annotation.VisibleForTesting;
+import android.util.Log;
+import com.android.dialer.animation.AnimUtils.AnimationCallback;
+import com.android.dialer.app.DialtactsActivity;
+
+/**
+ * Controls the various animated properties of the actionBar: showing/hiding, fading/revealing, and
+ * collapsing/expanding, and assigns suitable properties to the actionBar based on the current state
+ * of the UI.
+ */
+public class ActionBarController {
+
+ public static final boolean DEBUG = DialtactsActivity.DEBUG;
+ public static final String TAG = "ActionBarController";
+ private static final String KEY_IS_SLID_UP = "key_actionbar_is_slid_up";
+ private static final String KEY_IS_FADED_OUT = "key_actionbar_is_faded_out";
+ private static final String KEY_IS_EXPANDED = "key_actionbar_is_expanded";
+
+ private ActivityUi mActivityUi;
+ private SearchEditTextLayout mSearchBox;
+
+ private boolean mIsActionBarSlidUp;
+
+ private final AnimationCallback mFadeOutCallback =
+ new AnimationCallback() {
+ @Override
+ public void onAnimationEnd() {
+ slideActionBar(true /* slideUp */, false /* animate */);
+ }
+
+ @Override
+ public void onAnimationCancel() {
+ slideActionBar(true /* slideUp */, false /* animate */);
+ }
+ };
+
+ public ActionBarController(ActivityUi activityUi, SearchEditTextLayout searchBox) {
+ mActivityUi = activityUi;
+ mSearchBox = searchBox;
+ }
+
+ /** @return Whether or not the action bar is currently showing (both slid down and visible) */
+ public boolean isActionBarShowing() {
+ return !mIsActionBarSlidUp && !mSearchBox.isFadedOut();
+ }
+
+ /** Called when the user has tapped on the collapsed search box, to start a new search query. */
+ public void onSearchBoxTapped() {
+ if (DEBUG) {
+ Log.d(TAG, "OnSearchBoxTapped: isInSearchUi " + mActivityUi.isInSearchUi());
+ }
+ if (!mActivityUi.isInSearchUi()) {
+ mSearchBox.expand(true /* animate */, true /* requestFocus */);
+ }
+ }
+
+ /** Called when search UI has been exited for some reason. */
+ public void onSearchUiExited() {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "OnSearchUIExited: isExpanded "
+ + mSearchBox.isExpanded()
+ + " isFadedOut: "
+ + mSearchBox.isFadedOut()
+ + " shouldShowActionBar: "
+ + mActivityUi.shouldShowActionBar());
+ }
+ if (mSearchBox.isExpanded()) {
+ mSearchBox.collapse(true /* animate */);
+ }
+ if (mSearchBox.isFadedOut()) {
+ mSearchBox.fadeIn();
+ }
+
+ if (mActivityUi.shouldShowActionBar()) {
+ slideActionBar(false /* slideUp */, false /* animate */);
+ } else {
+ slideActionBar(true /* slideUp */, false /* animate */);
+ }
+ }
+
+ /**
+ * Called to indicate that the user is trying to hide the dialpad. Should be called before any
+ * state changes have actually occurred.
+ */
+ public void onDialpadDown() {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "OnDialpadDown: isInSearchUi "
+ + mActivityUi.isInSearchUi()
+ + " hasSearchQuery: "
+ + mActivityUi.hasSearchQuery()
+ + " isFadedOut: "
+ + mSearchBox.isFadedOut()
+ + " isExpanded: "
+ + mSearchBox.isExpanded());
+ }
+ if (mActivityUi.isInSearchUi()) {
+ if (mActivityUi.hasSearchQuery()) {
+ if (mSearchBox.isFadedOut()) {
+ mSearchBox.setVisible(true);
+ }
+ if (!mSearchBox.isExpanded()) {
+ mSearchBox.expand(false /* animate */, false /* requestFocus */);
+ }
+ slideActionBar(false /* slideUp */, true /* animate */);
+ } else {
+ mSearchBox.fadeIn();
+ }
+ }
+ }
+
+ /**
+ * Called to indicate that the user is trying to show the dialpad. Should be called before any
+ * state changes have actually occurred.
+ */
+ public void onDialpadUp() {
+ if (DEBUG) {
+ Log.d(TAG, "OnDialpadUp: isInSearchUi " + mActivityUi.isInSearchUi());
+ }
+ if (mActivityUi.isInSearchUi()) {
+ slideActionBar(true /* slideUp */, true /* animate */);
+ } else {
+ // From the lists fragment
+ mSearchBox.fadeOut(mFadeOutCallback);
+ }
+ }
+
+ public void slideActionBar(boolean slideUp, boolean animate) {
+ if (DEBUG) {
+ Log.d(TAG, "Sliding actionBar - up: " + slideUp + " animate: " + animate);
+ }
+ if (animate) {
+ ValueAnimator animator = slideUp ? ValueAnimator.ofFloat(0, 1) : ValueAnimator.ofFloat(1, 0);
+ animator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final float value = (float) animation.getAnimatedValue();
+ setHideOffset((int) (mActivityUi.getActionBarHeight() * value));
+ }
+ });
+ animator.start();
+ } else {
+ setHideOffset(slideUp ? mActivityUi.getActionBarHeight() : 0);
+ }
+ mIsActionBarSlidUp = slideUp;
+ }
+
+ public void setAlpha(float alphaValue) {
+ mSearchBox.animate().alpha(alphaValue).start();
+ }
+
+ /** @return The offset the action bar is being translated upwards by */
+ public int getHideOffset() {
+ return mActivityUi.getActionBarHideOffset();
+ }
+
+ public void setHideOffset(int offset) {
+ mIsActionBarSlidUp = offset >= mActivityUi.getActionBarHeight();
+ mActivityUi.setActionBarHideOffset(offset);
+ }
+
+ public int getActionBarHeight() {
+ return mActivityUi.getActionBarHeight();
+ }
+
+ /** Saves the current state of the action bar into a provided {@link Bundle} */
+ public void saveInstanceState(Bundle outState) {
+ outState.putBoolean(KEY_IS_SLID_UP, mIsActionBarSlidUp);
+ outState.putBoolean(KEY_IS_FADED_OUT, mSearchBox.isFadedOut());
+ outState.putBoolean(KEY_IS_EXPANDED, mSearchBox.isExpanded());
+ }
+
+ /** Restores the action bar state from a provided {@link Bundle}. */
+ public void restoreInstanceState(Bundle inState) {
+ mIsActionBarSlidUp = inState.getBoolean(KEY_IS_SLID_UP);
+
+ final boolean isSearchBoxFadedOut = inState.getBoolean(KEY_IS_FADED_OUT);
+ if (isSearchBoxFadedOut) {
+ if (!mSearchBox.isFadedOut()) {
+ mSearchBox.setVisible(false);
+ }
+ } else if (mSearchBox.isFadedOut()) {
+ mSearchBox.setVisible(true);
+ }
+
+ final boolean isSearchBoxExpanded = inState.getBoolean(KEY_IS_EXPANDED);
+ if (isSearchBoxExpanded) {
+ if (!mSearchBox.isExpanded()) {
+ mSearchBox.expand(false, false);
+ }
+ } else if (mSearchBox.isExpanded()) {
+ mSearchBox.collapse(false);
+ }
+ }
+
+ /**
+ * This should be called after onCreateOptionsMenu has been called, when the actionbar has been
+ * laid out and actually has a height.
+ */
+ public void restoreActionBarOffset() {
+ slideActionBar(mIsActionBarSlidUp /* slideUp */, false /* animate */);
+ }
+
+ @VisibleForTesting
+ public boolean getIsActionBarSlidUp() {
+ return mIsActionBarSlidUp;
+ }
+
+ public interface ActivityUi {
+
+ boolean isInSearchUi();
+
+ boolean hasSearchQuery();
+
+ boolean shouldShowActionBar();
+
+ int getActionBarHeight();
+
+ int getActionBarHideOffset();
+
+ void setActionBarHideOffset(int offset);
+ }
+}
diff --git a/java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java b/java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java
new file mode 100644
index 000000000..85fd5ec6a
--- /dev/null
+++ b/java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java
@@ -0,0 +1,43 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+import com.android.dialer.app.R;
+import com.android.dialer.util.OrientationUtil;
+
+/** Empty content view to be shown when dialpad is visible. */
+public class DialpadSearchEmptyContentView extends EmptyContentView {
+
+ public DialpadSearchEmptyContentView(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void inflateLayout() {
+ int orientation =
+ OrientationUtil.isLandscape(getContext()) ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL;
+
+ setOrientation(orientation);
+
+ final LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.empty_content_view_dialpad_search, this);
+ }
+}
diff --git a/java/com/android/dialer/app/widget/EmptyContentView.java b/java/com/android/dialer/app/widget/EmptyContentView.java
new file mode 100644
index 000000000..cfc8665a2
--- /dev/null
+++ b/java/com/android/dialer/app/widget/EmptyContentView.java
@@ -0,0 +1,121 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.app.R;
+
+public class EmptyContentView extends LinearLayout implements View.OnClickListener {
+
+ /** Listener to call when action button is clicked. */
+ public interface OnEmptyViewActionButtonClickedListener {
+ void onEmptyViewActionButtonClicked();
+ }
+
+ public static final int NO_LABEL = 0;
+ public static final int NO_IMAGE = 0;
+
+ private ImageView mImageView;
+ private TextView mDescriptionView;
+ private TextView mActionView;
+ private OnEmptyViewActionButtonClickedListener mOnActionButtonClickedListener;
+
+ public EmptyContentView(Context context) {
+ this(context, null);
+ }
+
+ public EmptyContentView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public EmptyContentView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ inflateLayout();
+
+ // Don't let touches fall through the empty view.
+ setClickable(true);
+ mImageView = (ImageView) findViewById(R.id.emptyListViewImage);
+ mDescriptionView = (TextView) findViewById(R.id.emptyListViewMessage);
+ mActionView = (TextView) findViewById(R.id.emptyListViewAction);
+ mActionView.setOnClickListener(this);
+ }
+
+ public void setDescription(int resourceId) {
+ if (resourceId == NO_LABEL) {
+ mDescriptionView.setText(null);
+ mDescriptionView.setVisibility(View.GONE);
+ } else {
+ mDescriptionView.setText(resourceId);
+ mDescriptionView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setImage(int resourceId) {
+ if (resourceId == NO_LABEL) {
+ mImageView.setImageDrawable(null);
+ mImageView.setVisibility(View.GONE);
+ } else {
+ mImageView.setImageResource(resourceId);
+ mImageView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void setActionLabel(int resourceId) {
+ if (resourceId == NO_LABEL) {
+ mActionView.setText(null);
+ mActionView.setVisibility(View.GONE);
+ } else {
+ mActionView.setText(resourceId);
+ mActionView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public boolean isShowingContent() {
+ return mImageView.getVisibility() == View.VISIBLE
+ || mDescriptionView.getVisibility() == View.VISIBLE
+ || mActionView.getVisibility() == View.VISIBLE;
+ }
+
+ public void setActionClickedListener(OnEmptyViewActionButtonClickedListener listener) {
+ mOnActionButtonClickedListener = listener;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mOnActionButtonClickedListener != null) {
+ mOnActionButtonClickedListener.onEmptyViewActionButtonClicked();
+ }
+ }
+
+ protected void inflateLayout() {
+ setOrientation(LinearLayout.VERTICAL);
+ final LayoutInflater inflater =
+ (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ inflater.inflate(R.layout.empty_content_view, this);
+ }
+
+}
diff --git a/java/com/android/dialer/app/widget/SearchEditTextLayout.java b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
new file mode 100644
index 000000000..be850f9a0
--- /dev/null
+++ b/java/com/android/dialer/app/widget/SearchEditTextLayout.java
@@ -0,0 +1,324 @@
+/*
+ * 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.app.widget;
+
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.FrameLayout;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.app.R;
+import com.android.dialer.util.DialerUtils;
+
+public class SearchEditTextLayout extends FrameLayout {
+
+ private static final float EXPAND_MARGIN_FRACTION_START = 0.8f;
+ private static final int ANIMATION_DURATION = 200;
+ /* Subclass-visible for testing */
+ protected boolean mIsExpanded = false;
+ protected boolean mIsFadedOut = false;
+ private OnKeyListener mPreImeKeyListener;
+ private int mTopMargin;
+ private int mBottomMargin;
+ private int mLeftMargin;
+ private int mRightMargin;
+ private float mCollapsedElevation;
+ private View mCollapsed;
+ private View mExpanded;
+ private EditText mSearchView;
+ private View mSearchIcon;
+ private View mCollapsedSearchBox;
+ private View mVoiceSearchButtonView;
+ private View mOverflowButtonView;
+ private View mBackButtonView;
+ private View mExpandedSearchBox;
+ private View mClearButtonView;
+
+ private ValueAnimator mAnimator;
+
+ private Callback mCallback;
+
+ public SearchEditTextLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setPreImeKeyListener(OnKeyListener listener) {
+ mPreImeKeyListener = listener;
+ }
+
+ public void setCallback(Callback listener) {
+ mCallback = listener;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+ mTopMargin = params.topMargin;
+ mBottomMargin = params.bottomMargin;
+ mLeftMargin = params.leftMargin;
+ mRightMargin = params.rightMargin;
+
+ mCollapsedElevation = getElevation();
+
+ mCollapsed = findViewById(R.id.search_box_collapsed);
+ mExpanded = findViewById(R.id.search_box_expanded);
+ mSearchView = (EditText) mExpanded.findViewById(R.id.search_view);
+
+ mSearchIcon = findViewById(R.id.search_magnifying_glass);
+ mCollapsedSearchBox = findViewById(R.id.search_box_start_search);
+ mVoiceSearchButtonView = findViewById(R.id.voice_search_button);
+ mOverflowButtonView = findViewById(R.id.dialtacts_options_menu_button);
+ mBackButtonView = findViewById(R.id.search_back_button);
+ mExpandedSearchBox = findViewById(R.id.search_box_expanded);
+ mClearButtonView = findViewById(R.id.search_close_button);
+
+ // Convert a long click into a click to expand the search box, and then long click on the
+ // search view. This accelerates the long-press scenario for copy/paste.
+ mCollapsedSearchBox.setOnLongClickListener(
+ new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ mCollapsedSearchBox.performClick();
+ mSearchView.performLongClick();
+ return false;
+ }
+ });
+
+ mSearchView.setOnFocusChangeListener(
+ new OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ DialerUtils.showInputMethod(v);
+ } else {
+ DialerUtils.hideInputMethod(v);
+ }
+ }
+ });
+
+ mSearchView.setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCallback != null) {
+ mCallback.onSearchViewClicked();
+ }
+ }
+ });
+
+ mSearchView.addTextChangedListener(
+ 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) {
+ mClearButtonView.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+
+ findViewById(R.id.search_close_button)
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mSearchView.setText(null);
+ }
+ });
+
+ findViewById(R.id.search_back_button)
+ .setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mCallback != null) {
+ mCallback.onBackButtonClicked();
+ }
+ }
+ });
+
+ super.onFinishInflate();
+ }
+
+ @Override
+ public boolean dispatchKeyEventPreIme(KeyEvent event) {
+ if (mPreImeKeyListener != null) {
+ if (mPreImeKeyListener.onKey(this, event.getKeyCode(), event)) {
+ return true;
+ }
+ }
+ return super.dispatchKeyEventPreIme(event);
+ }
+
+ public void fadeOut() {
+ fadeOut(null);
+ }
+
+ public void fadeOut(AnimUtils.AnimationCallback callback) {
+ AnimUtils.fadeOut(this, ANIMATION_DURATION, callback);
+ mIsFadedOut = true;
+ }
+
+ public void fadeIn() {
+ AnimUtils.fadeIn(this, ANIMATION_DURATION);
+ mIsFadedOut = false;
+ }
+
+ public void setVisible(boolean visible) {
+ if (visible) {
+ setAlpha(1);
+ setVisibility(View.VISIBLE);
+ mIsFadedOut = false;
+ } else {
+ setAlpha(0);
+ setVisibility(View.GONE);
+ mIsFadedOut = true;
+ }
+ }
+
+ public void expand(boolean animate, boolean requestFocus) {
+ updateVisibility(true /* isExpand */);
+
+ if (animate) {
+ AnimUtils.crossFadeViews(mExpanded, mCollapsed, ANIMATION_DURATION);
+ mAnimator = ValueAnimator.ofFloat(EXPAND_MARGIN_FRACTION_START, 0f);
+ setMargins(EXPAND_MARGIN_FRACTION_START);
+ prepareAnimator(true);
+ } else {
+ mExpanded.setVisibility(View.VISIBLE);
+ mExpanded.setAlpha(1);
+ setMargins(0f);
+ mCollapsed.setVisibility(View.GONE);
+ }
+
+ // Set 9-patch background. This owns the padding, so we need to restore the original values.
+ int paddingTop = this.getPaddingTop();
+ int paddingStart = this.getPaddingStart();
+ int paddingBottom = this.getPaddingBottom();
+ int paddingEnd = this.getPaddingEnd();
+ setBackgroundResource(R.drawable.search_shadow);
+ setElevation(0);
+ setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom);
+
+ if (requestFocus) {
+ mSearchView.requestFocus();
+ }
+ mIsExpanded = true;
+ }
+
+ public void collapse(boolean animate) {
+ updateVisibility(false /* isExpand */);
+
+ if (animate) {
+ AnimUtils.crossFadeViews(mCollapsed, mExpanded, ANIMATION_DURATION);
+ mAnimator = ValueAnimator.ofFloat(0f, 1f);
+ prepareAnimator(false);
+ } else {
+ mCollapsed.setVisibility(View.VISIBLE);
+ mCollapsed.setAlpha(1);
+ setMargins(1f);
+ mExpanded.setVisibility(View.GONE);
+ }
+
+ mIsExpanded = false;
+ setElevation(mCollapsedElevation);
+ setBackgroundResource(R.drawable.rounded_corner);
+ }
+
+ /**
+ * Updates the visibility of views depending on whether we will show the expanded or collapsed
+ * search view. This helps prevent some jank with the crossfading if we are animating.
+ *
+ * @param isExpand Whether we are about to show the expanded search box.
+ */
+ private void updateVisibility(boolean isExpand) {
+ int collapsedViewVisibility = isExpand ? View.GONE : View.VISIBLE;
+ int expandedViewVisibility = isExpand ? View.VISIBLE : View.GONE;
+
+ mSearchIcon.setVisibility(collapsedViewVisibility);
+ mCollapsedSearchBox.setVisibility(collapsedViewVisibility);
+ mVoiceSearchButtonView.setVisibility(collapsedViewVisibility);
+ mOverflowButtonView.setVisibility(collapsedViewVisibility);
+ mBackButtonView.setVisibility(expandedViewVisibility);
+ // TODO: Prevents keyboard from jumping up in landscape mode after exiting the
+ // SearchFragment when the query string is empty. More elegant fix?
+ //mExpandedSearchBox.setVisibility(expandedViewVisibility);
+ if (TextUtils.isEmpty(mSearchView.getText())) {
+ mClearButtonView.setVisibility(View.GONE);
+ } else {
+ mClearButtonView.setVisibility(expandedViewVisibility);
+ }
+ }
+
+ private void prepareAnimator(final boolean expand) {
+ if (mAnimator != null) {
+ mAnimator.cancel();
+ }
+
+ mAnimator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ final Float fraction = (Float) animation.getAnimatedValue();
+ setMargins(fraction);
+ }
+ });
+
+ mAnimator.setDuration(ANIMATION_DURATION);
+ mAnimator.start();
+ }
+
+ public boolean isExpanded() {
+ return mIsExpanded;
+ }
+
+ public boolean isFadedOut() {
+ return mIsFadedOut;
+ }
+
+ /**
+ * Assigns margins to the search box as a fraction of its maximum margin size
+ *
+ * @param fraction How large the margins should be as a fraction of their full size
+ */
+ private void setMargins(float fraction) {
+ MarginLayoutParams params = (MarginLayoutParams) getLayoutParams();
+ params.topMargin = (int) (mTopMargin * fraction);
+ params.bottomMargin = (int) (mBottomMargin * fraction);
+ params.leftMargin = (int) (mLeftMargin * fraction);
+ params.rightMargin = (int) (mRightMargin * fraction);
+ requestLayout();
+ }
+
+ /** Listener for the back button next to the search view being pressed */
+ public interface Callback {
+
+ void onBackButtonClicked();
+
+ void onSearchViewClicked();
+ }
+}
diff --git a/java/com/android/dialer/backup/AndroidManifest.xml b/java/com/android/dialer/backup/AndroidManifest.xml
new file mode 100644
index 000000000..cfdb3d93d
--- /dev/null
+++ b/java/com/android/dialer/backup/AndroidManifest.xml
@@ -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
+ -->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.backup">
+
+ <application
+ android:backupAgent="com.android.dialer.backup.DialerBackupAgent"
+ android:fullBackupOnly="true"
+ android:restoreAnyVersion="true"
+ android:name="com.android.dialer.app.DialerApplication"
+ />
+
+</manifest> \ No newline at end of file
diff --git a/java/com/android/dialer/backup/DialerBackupAgent.java b/java/com/android/dialer/backup/DialerBackupAgent.java
new file mode 100644
index 000000000..391a93f29
--- /dev/null
+++ b/java/com/android/dialer/backup/DialerBackupAgent.java
@@ -0,0 +1,276 @@
+/*
+ * 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.backup;
+
+import android.annotation.TargetApi;
+import android.app.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.app.backup.FullBackupDataOutput;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.ParcelFileDescriptor;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.util.Pair;
+import com.android.dialer.backup.nano.VoicemailInfo;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.telecom.TelecomUtil;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Locale;
+
+/**
+ * The Dialer backup agent to backup voicemails, and files under files, shared prefs and databases
+ */
+public class DialerBackupAgent extends BackupAgent {
+ // File names suffix for backup/restore.
+ private static final String VOICEMAIL_BACKUP_FILE_SUFFIX = "_voicemail_backup.proto";
+ // File name formats for backup. It looks like 000000_voicemail_backup.proto, 0000001...
+ private static final String VOICEMAIL_BACKUP_FILE_FORMAT = "%06d" + VOICEMAIL_BACKUP_FILE_SUFFIX;
+ // Order by Date entries from database. We start backup from the newest.
+ private static final String ORDER_BY_DATE = "date DESC";
+ // Voicemail Uri Column
+ public static final String VOICEMAIL_URI = "voicemail_uri";
+ // Voicemail packages to backup
+ public static final String VOICEMAIL_SOURCE_PACKAGE = "com.android.phone";
+
+ private long voicemailsBackedupSoFar = 0;
+ private long sizeOfVoicemailsBackedupSoFar = 0;
+ private boolean maxVoicemailBackupReached = false;
+
+ /**
+ * onBackup is used for Key/Value backup. Since we are using Dolly/Android Auto backup, we do not
+ * need to implement this method and Dolly should not be calling this. Instead Dolly will be
+ * calling onFullBackup.
+ */
+ @Override
+ public void onBackup(
+ ParcelFileDescriptor parcelFileDescriptor,
+ BackupDataOutput backupDataOutput,
+ ParcelFileDescriptor parcelFileDescriptor1)
+ throws IOException {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_BACKUP);
+ Assert.fail("Android Backup should not call DialerBackupAgent.onBackup");
+ }
+
+ /**
+ * onRestore is used for Key/Value restore. Since we are using Dolly/Android Auto backup/restore,
+ * we do not need to implement this method as Dolly should not be calling this method. Instead
+ * onFileRestore will be called by Dolly.
+ */
+ @Override
+ public void onRestore(
+ BackupDataInput backupDataInput, int i, ParcelFileDescriptor parcelFileDescriptor)
+ throws IOException {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE);
+ Assert.fail("Android Backup should not call DialerBackupAgent.onRestore");
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ @Override
+ public void onFullBackup(FullBackupDataOutput data) throws IOException {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_FULL_BACKUP);
+ LogUtil.i("DialerBackupAgent.onFullBackup", "performing dialer backup");
+ boolean autoBackupEnabled =
+ ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true);
+ boolean vmBackupEnabled =
+ ConfigProviderBindings.get(this).getBoolean("enable_vm_backup", false);
+
+ if (autoBackupEnabled) {
+ if (!maxVoicemailBackupReached && vmBackupEnabled) {
+ voicemailsBackedupSoFar = 0;
+ sizeOfVoicemailsBackedupSoFar = 0;
+
+ LogUtil.i("DialerBackupAgent.onFullBackup", "autoBackup is enabled");
+ ContentResolver contentResolver = getContentResolver();
+ int limit = 1000;
+
+ Uri uri =
+ TelecomUtil.getCallLogUri(this)
+ .buildUpon()
+ .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
+ .build();
+
+ LogUtil.i("DialerBackupAgent.onFullBackup", "backing up from: " + uri);
+
+ try (Cursor cursor =
+ contentResolver.query(
+ uri,
+ null,
+ String.format(
+ "(%s = ? AND deleted = 0 AND %s = ?)", Calls.TYPE, Voicemails.SOURCE_PACKAGE),
+ new String[] {
+ Integer.toString(CallLog.Calls.VOICEMAIL_TYPE), VOICEMAIL_SOURCE_PACKAGE
+ },
+ ORDER_BY_DATE,
+ null)) {
+
+ if (cursor == null) {
+ LogUtil.i("DialerBackupAgent.onFullBackup", "cursor was null");
+ return;
+ }
+
+ LogUtil.i("DialerBackupAgent.onFullBackup", "cursor count: " + cursor.getCount());
+ if (cursor.moveToFirst()) {
+ int fileNum = 0;
+ do {
+ backupRow(
+ data, cursor, String.format(Locale.US, VOICEMAIL_BACKUP_FILE_FORMAT, fileNum++));
+ } while (cursor.moveToNext() && !maxVoicemailBackupReached);
+ } else {
+ LogUtil.i("DialerBackupAgent.onFullBackup", "cursor.moveToFirst failed");
+ }
+ }
+ }
+ LogUtil.i(
+ "DialerBackupAgent.onFullBackup",
+ "vm files backed up: %d, vm size backed up:%d, "
+ + "max vm backup reached:%b, vm backup enabled:%b",
+ voicemailsBackedupSoFar,
+ sizeOfVoicemailsBackedupSoFar,
+ maxVoicemailBackupReached,
+ vmBackupEnabled);
+ super.onFullBackup(data);
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_FULL_BACKED_UP);
+ } else {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_BACKUP_DISABLED);
+ LogUtil.i("DialerBackupAgent.onFullBackup", "autoBackup is disabled");
+ }
+ }
+
+ private void backupRow(FullBackupDataOutput data, Cursor cursor, String fileName)
+ throws IOException {
+
+ VoicemailInfo cursorRowInProto =
+ DialerBackupUtils.convertVoicemailCursorRowToProto(cursor, getContentResolver());
+
+ File file = new File(getFilesDir(), fileName);
+ DialerBackupUtils.writeProtoToFile(file, cursorRowInProto);
+
+ if (sizeOfVoicemailsBackedupSoFar + file.length()
+ > DialerBackupUtils.maxVoicemailSizeToBackup) {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_MAX_VM_BACKUP_REACHED);
+ maxVoicemailBackupReached = true;
+ file.delete();
+ return;
+ }
+
+ backupFile(file, data);
+ }
+
+ // TODO: Write to FullBackupDataOutput directly (b/33849960)
+ private void backupFile(File file, FullBackupDataOutput data) throws IOException {
+ try {
+ super.fullBackupFile(file, data);
+ sizeOfVoicemailsBackedupSoFar = sizeOfVoicemailsBackedupSoFar + file.length();
+ voicemailsBackedupSoFar++;
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_VOICEMAIL_BACKED_UP);
+ LogUtil.i("DialerBackupAgent.backupFile", "file backed up:" + file.getAbsolutePath());
+ } finally {
+ file.delete();
+ }
+ }
+
+ // Being tracked in b/33839952
+ @Override
+ public void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_QUOTA_EXCEEDED);
+ LogUtil.i("DialerBackupAgent.onQuotaExceeded", "does nothing");
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ @Override
+ public void onRestoreFile(
+ ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime)
+ throws IOException {
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "size:" + size + " destination: " + destination);
+
+ String fileName = destination.getName();
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "file name: " + fileName);
+
+ if (ConfigProviderBindings.get(this).getBoolean("enable_autobackup", true)) {
+ if (fileName.endsWith(VOICEMAIL_BACKUP_FILE_SUFFIX)
+ && ConfigProviderBindings.get(this).getBoolean("enable_vm_restore", true)) {
+ if (DialerBackupUtils.canRestoreVoicemails(getContentResolver(), this)) {
+ try {
+ super.onRestoreFile(data, size, destination, type, mode, mtime);
+ restoreVoicemail(destination);
+ destination.delete();
+ } catch (IOException e) {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_IO_EXCEPTION);
+ LogUtil.e(
+ "DialerBackupAgent.onRestoreFile",
+ "could not restore voicemail - IOException: ",
+ e);
+ }
+ } else {
+ LogUtil.i(
+ "DialerBackupAgent.onRestoreFile", "build does not support restoring voicemails");
+ }
+
+ } else {
+ super.onRestoreFile(data, size, destination, type, mode, mtime);
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "restored: " + fileName);
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_RESTORED_FILE);
+ }
+ } else {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_DISABLED);
+ LogUtil.i("DialerBackupAgent.onRestoreFile", "autoBackup is disabled");
+ }
+ }
+
+ @Override
+ public void onRestoreFinished() {
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_FINISHED);
+ LogUtil.i("DialerBackupAgent.onRestoreFinished", "do nothing");
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ private void restoreVoicemail(File file) throws IOException {
+ Pair<ContentValues, byte[]> pair =
+ DialerBackupUtils.convertVoicemailProtoFileToContentValueAndAudioBytes(
+ file, getApplicationContext());
+
+ if (pair == null) {
+ LogUtil.i("DialerBackupAgent.restoreVoicemail", "not restoring VM due to duplicate");
+ Logger.get(this)
+ .logImpression(DialerImpression.Type.BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING);
+ return;
+ }
+
+ // TODO: Uniquely identify backup agent as the creator of this voicemail b/34084298
+ try (OutputStream restoreStream =
+ getContentResolver()
+ .openOutputStream(
+ getContentResolver()
+ .insert(VoicemailContract.Voicemails.CONTENT_URI, pair.first))) {
+ DialerBackupUtils.copyAudioBytesToContentUri(pair.second, restoreStream);
+ Logger.get(this).logImpression(DialerImpression.Type.BACKUP_RESTORED_VOICEMAIL);
+ }
+ }
+}
diff --git a/java/com/android/dialer/backup/DialerBackupUtils.java b/java/com/android/dialer/backup/DialerBackupUtils.java
new file mode 100644
index 000000000..ff0cb4f7c
--- /dev/null
+++ b/java/com/android/dialer/backup/DialerBackupUtils.java
@@ -0,0 +1,320 @@
+/*
+ * 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.backup;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Pair;
+import com.android.dialer.backup.nano.VoicemailInfo;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
+import com.google.protobuf.nano.MessageNano;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Helper functions for DialerBackupAgent */
+public class DialerBackupUtils {
+ // Backup voicemails up to 20MB
+ static long maxVoicemailSizeToBackup = 20000000L;
+ static final String RESTORED_COLUMN = "restored";
+
+ private DialerBackupUtils() {}
+
+ public static void copyAudioBytesToContentUri(
+ @NonNull byte[] audioBytesArray, @NonNull OutputStream restoreStream) throws IOException {
+ LogUtil.i("DialerBackupUtils.copyStream", "audioByteArray length: " + audioBytesArray.length);
+
+ ByteArrayInputStream decodedStream = new ByteArrayInputStream(audioBytesArray);
+ LogUtil.i(
+ "DialerBackupUtils.copyStream", "decodedStream.available: " + decodedStream.available());
+
+ ByteStreams.copy(decodedStream, restoreStream);
+ }
+
+ public static @Nullable byte[] audioStreamToByteArray(@NonNull InputStream stream)
+ throws IOException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ if (stream.available() > 0) {
+ ByteStreams.copy(stream, buffer);
+ } else {
+ LogUtil.i("DialerBackupUtils.audioStreamToByteArray", "no audio stream to backup");
+ }
+ return buffer.toByteArray();
+ }
+
+ public static void writeProtoToFile(@NonNull File file, @NonNull VoicemailInfo voicemailInfo)
+ throws IOException {
+ LogUtil.i(
+ "DialerBackupUtils.writeProtoToFile",
+ "backup " + voicemailInfo + " to " + file.getAbsolutePath());
+
+ byte[] bytes = MessageNano.toByteArray(voicemailInfo);
+ Files.write(bytes, file);
+ }
+
+ /** Only restore voicemails that have the restored column in calllog (NMR2+ builds) */
+ @TargetApi(VERSION_CODES.M)
+ public static boolean canRestoreVoicemails(ContentResolver contentResolver, Context context) {
+ try (Cursor cursor = contentResolver.query(Voicemails.CONTENT_URI, null, null, null, null)) {
+ // Restored column only exists in NMR2 and above builds.
+ if (cursor.getColumnIndex(RESTORED_COLUMN) != -1) {
+ LogUtil.i("DialerBackupUtils.canRestoreVoicemails", "Build supports restore");
+ return true;
+ } else {
+ LogUtil.i("DialerBackupUtils.canRestoreVoicemails", "Build does not support restore");
+ return false;
+ }
+ }
+ }
+
+ public static VoicemailInfo protoFileToVoicemailInfo(@NonNull File file) throws IOException {
+ byte[] byteArray = Files.toByteArray(file);
+ return VoicemailInfo.parseFrom(byteArray);
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ public static VoicemailInfo convertVoicemailCursorRowToProto(
+ @NonNull Cursor cursor, @NonNull ContentResolver contentResolver) throws IOException {
+
+ VoicemailInfo voicemailInfo = new VoicemailInfo();
+
+ for (int i = 0; i < cursor.getColumnCount(); ++i) {
+ String name = cursor.getColumnName(i);
+ String value = cursor.getString(i);
+
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailCursorRowToProto",
+ "column index: %d, column name: %s, column value: %s",
+ i,
+ name,
+ value);
+
+ switch (name) {
+ case Voicemails.DATE:
+ voicemailInfo.date = value;
+ break;
+ case Voicemails.DELETED:
+ voicemailInfo.deleted = value;
+ break;
+ case Voicemails.DIRTY:
+ voicemailInfo.dirty = value;
+ break;
+ case Voicemails.DIR_TYPE:
+ voicemailInfo.dirType = value;
+ break;
+ case Voicemails.DURATION:
+ voicemailInfo.duration = value;
+ break;
+ case Voicemails.HAS_CONTENT:
+ voicemailInfo.hasContent = value;
+ break;
+ case Voicemails.IS_READ:
+ voicemailInfo.isRead = value;
+ break;
+ case Voicemails.ITEM_TYPE:
+ voicemailInfo.itemType = value;
+ break;
+ case Voicemails.LAST_MODIFIED:
+ voicemailInfo.lastModified = value;
+ break;
+ case Voicemails.MIME_TYPE:
+ voicemailInfo.mimeType = value;
+ break;
+ case Voicemails.NUMBER:
+ voicemailInfo.number = value;
+ break;
+ case Voicemails.PHONE_ACCOUNT_COMPONENT_NAME:
+ voicemailInfo.phoneAccountComponentName = value;
+ break;
+ case Voicemails.PHONE_ACCOUNT_ID:
+ voicemailInfo.phoneAccountId = value;
+ break;
+ case Voicemails.SOURCE_DATA:
+ voicemailInfo.sourceData = value;
+ break;
+ case Voicemails.SOURCE_PACKAGE:
+ voicemailInfo.sourcePackage = value;
+ break;
+ case Voicemails.TRANSCRIPTION:
+ voicemailInfo.transcription = value;
+ break;
+ case DialerBackupAgent.VOICEMAIL_URI:
+ try (InputStream audioStream = contentResolver.openInputStream(Uri.parse(value))) {
+ voicemailInfo.encodedVoicemailKey = audioStreamToByteArray(audioStream);
+ }
+ break;
+ default:
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailCursorRowToProto",
+ "Not backing up column: %s, with value: %s",
+ name,
+ value);
+ break;
+ }
+ }
+ return voicemailInfo;
+ }
+
+ public static Pair<ContentValues, byte[]> convertVoicemailProtoFileToContentValueAndAudioBytes(
+ @NonNull File file, Context context) throws IOException {
+
+ VoicemailInfo voicemailInfo = DialerBackupUtils.protoFileToVoicemailInfo(file);
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
+ "file name: "
+ + file.getName()
+ + " voicemailInfo size: "
+ + voicemailInfo.getSerializedSize());
+
+ if (isDuplicate(context, voicemailInfo)) {
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
+ "voicemail already exists");
+ return null;
+ } else {
+ ContentValues contentValues = new ContentValues();
+
+ if (!voicemailInfo.date.isEmpty()) {
+ contentValues.put(Voicemails.DATE, voicemailInfo.date);
+ }
+ if (!voicemailInfo.deleted.isEmpty()) {
+ contentValues.put(Voicemails.DELETED, voicemailInfo.deleted);
+ }
+ if (!voicemailInfo.dirty.isEmpty()) {
+ contentValues.put(Voicemails.DIRTY, voicemailInfo.dirty);
+ }
+ if (!voicemailInfo.duration.isEmpty()) {
+ contentValues.put(Voicemails.DURATION, voicemailInfo.duration);
+ }
+ if (!voicemailInfo.isRead.isEmpty()) {
+ contentValues.put(Voicemails.IS_READ, voicemailInfo.isRead);
+ }
+ if (!voicemailInfo.lastModified.isEmpty()) {
+ contentValues.put(Voicemails.LAST_MODIFIED, voicemailInfo.lastModified);
+ }
+ if (!voicemailInfo.mimeType.isEmpty()) {
+ contentValues.put(Voicemails.MIME_TYPE, voicemailInfo.mimeType);
+ }
+ if (!voicemailInfo.number.isEmpty()) {
+ contentValues.put(Voicemails.NUMBER, voicemailInfo.number);
+ }
+ if (!voicemailInfo.phoneAccountComponentName.isEmpty()) {
+ contentValues.put(
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, voicemailInfo.phoneAccountComponentName);
+ }
+ if (!voicemailInfo.phoneAccountId.isEmpty()) {
+ contentValues.put(Voicemails.PHONE_ACCOUNT_ID, voicemailInfo.phoneAccountId);
+ }
+ if (!voicemailInfo.sourceData.isEmpty()) {
+ contentValues.put(Voicemails.SOURCE_DATA, voicemailInfo.sourceData);
+ }
+ if (!voicemailInfo.sourcePackage.isEmpty()) {
+ contentValues.put(Voicemails.SOURCE_PACKAGE, voicemailInfo.sourcePackage);
+ }
+ if (!voicemailInfo.transcription.isEmpty()) {
+ contentValues.put(Voicemails.TRANSCRIPTION, voicemailInfo.transcription);
+ }
+ contentValues.put(VoicemailContract.Voicemails.HAS_CONTENT, 1);
+ contentValues.put(RESTORED_COLUMN, "1");
+ contentValues.put(Voicemails.SOURCE_PACKAGE, getSourcePackage(context, voicemailInfo));
+
+ LogUtil.i(
+ "DialerBackupUtils.convertVoicemailProtoFileToContentValueAndEncodedAudio",
+ "cv: " + contentValues);
+
+ return Pair.create(contentValues, voicemailInfo.encodedVoicemailKey);
+ }
+ }
+
+ /**
+ * We should be using the system package name as the source package if there is no endless VM/VM
+ * archive present on the device. This is to separate pre-O (no endless VM) and O+ (endless VM)
+ * devices. This ensures that the source of truth for VMs is the VM server when endless VM is not
+ * enabled, and when endless VM/archived VMs is present, the source of truth for VMs is the device
+ * itself.
+ */
+ private static String getSourcePackage(Context context, VoicemailInfo voicemailInfo) {
+ if (ConfigProviderBindings.get(context)
+ .getBoolean("voicemail_restore_force_system_source_package", false)) {
+ LogUtil.i("DialerBackupUtils.getSourcePackage", "forcing system source package");
+ return "com.android.phone";
+ }
+ if (ConfigProviderBindings.get(context)
+ .getBoolean("voicemail_restore_check_archive_for_source_package", true)) {
+ if ("1".equals(voicemailInfo.archived)) {
+ LogUtil.i(
+ "DialerBackupUtils.getSourcePackage",
+ "voicemail was archived, using app source package");
+ // Using our app's source package will prevent the archived voicemail from being deleted by
+ // the system when it syncs with the voicemail server. In most cases the user will not see
+ // duplicate voicemails because this voicemail was archived and likely deleted from the
+ // voicemail server.
+ return context.getPackageName();
+ } else {
+ // Use the system source package. This means that if the voicemail is not present on the
+ // voicemail server then the system will delete it when it syncs.
+ LogUtil.i(
+ "DialerBackupUtils.getSourcePackage",
+ "voicemail was not archived, using system source package");
+ return "com.android.phone";
+ }
+ }
+ // Use our app's source package. This means that if the system syncs voicemail from the server
+ // the user could potentially get duplicate voicemails.
+ LogUtil.i("DialerBackupUtils.getSourcePackage", "defaulting to using app source package");
+ return context.getPackageName();
+ }
+
+ @TargetApi(VERSION_CODES.M)
+ private static boolean isDuplicate(Context context, VoicemailInfo voicemailInfo) {
+ // This checks for VM that might already exist, and doesn't restore them
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ VoicemailContract.Voicemails.CONTENT_URI,
+ null,
+ String.format(
+ "(%s = ? AND %s = ? AND %s = ?)",
+ Voicemails.NUMBER, Voicemails.DATE, Voicemails.DURATION),
+ new String[] {voicemailInfo.number, voicemailInfo.date, voicemailInfo.duration},
+ null,
+ null)) {
+ if (cursor.moveToFirst()
+ && ConfigProviderBindings.get(context)
+ .getBoolean("enable_vm_restore_no_duplicate", true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/backup/proto/VoicemailInfo.java b/java/com/android/dialer/backup/proto/VoicemailInfo.java
new file mode 100644
index 000000000..9ff8423f3
--- /dev/null
+++ b/java/com/android/dialer/backup/proto/VoicemailInfo.java
@@ -0,0 +1,377 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.backup.nano;
+
+@SuppressWarnings("hiding")
+public final class VoicemailInfo extends
+ com.google.protobuf.nano.ExtendableMessageNano<VoicemailInfo> {
+
+ private static volatile VoicemailInfo[] _emptyArray;
+ public static VoicemailInfo[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new VoicemailInfo[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional string date = 1;
+ public java.lang.String date;
+
+ // optional string deleted = 2;
+ public java.lang.String deleted;
+
+ // optional string dirty = 3;
+ public java.lang.String dirty;
+
+ // optional string dir_type = 4;
+ public java.lang.String dirType;
+
+ // optional string duration = 5;
+ public java.lang.String duration;
+
+ // optional string has_content = 6;
+ public java.lang.String hasContent;
+
+ // optional string is_read = 7;
+ public java.lang.String isRead;
+
+ // optional string item_type = 8;
+ public java.lang.String itemType;
+
+ // optional string last_modified = 9;
+ public java.lang.String lastModified;
+
+ // optional string mime_type = 10;
+ public java.lang.String mimeType;
+
+ // optional string number = 11;
+ public java.lang.String number;
+
+ // optional string phone_account_component_name = 12;
+ public java.lang.String phoneAccountComponentName;
+
+ // optional string phone_account_id = 13;
+ public java.lang.String phoneAccountId;
+
+ // optional string source_data = 14;
+ public java.lang.String sourceData;
+
+ // optional string source_package = 15;
+ public java.lang.String sourcePackage;
+
+ // optional string transcription = 16;
+ public java.lang.String transcription;
+
+ // optional string voicemail_uri = 17;
+ public java.lang.String voicemailUri;
+
+ // optional bytes encoded_voicemail_key = 18;
+ public byte[] encodedVoicemailKey;
+
+ // optional string archived = 19;
+ public java.lang.String archived;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.backup.VoicemailInfo)
+
+ public VoicemailInfo() {
+ clear();
+ }
+
+ public VoicemailInfo clear() {
+ date = "";
+ deleted = "";
+ dirty = "";
+ dirType = "";
+ duration = "";
+ hasContent = "";
+ isRead = "";
+ itemType = "";
+ lastModified = "";
+ mimeType = "";
+ number = "";
+ phoneAccountComponentName = "";
+ phoneAccountId = "";
+ sourceData = "";
+ sourcePackage = "";
+ transcription = "";
+ voicemailUri = "";
+ encodedVoicemailKey = com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES;
+ archived = "";
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.date != null && !this.date.equals("")) {
+ output.writeString(1, this.date);
+ }
+ if (this.deleted != null && !this.deleted.equals("")) {
+ output.writeString(2, this.deleted);
+ }
+ if (this.dirty != null && !this.dirty.equals("")) {
+ output.writeString(3, this.dirty);
+ }
+ if (this.dirType != null && !this.dirType.equals("")) {
+ output.writeString(4, this.dirType);
+ }
+ if (this.duration != null && !this.duration.equals("")) {
+ output.writeString(5, this.duration);
+ }
+ if (this.hasContent != null && !this.hasContent.equals("")) {
+ output.writeString(6, this.hasContent);
+ }
+ if (this.isRead != null && !this.isRead.equals("")) {
+ output.writeString(7, this.isRead);
+ }
+ if (this.itemType != null && !this.itemType.equals("")) {
+ output.writeString(8, this.itemType);
+ }
+ if (this.lastModified != null && !this.lastModified.equals("")) {
+ output.writeString(9, this.lastModified);
+ }
+ if (this.mimeType != null && !this.mimeType.equals("")) {
+ output.writeString(10, this.mimeType);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ output.writeString(11, this.number);
+ }
+ if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) {
+ output.writeString(12, this.phoneAccountComponentName);
+ }
+ if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) {
+ output.writeString(13, this.phoneAccountId);
+ }
+ if (this.sourceData != null && !this.sourceData.equals("")) {
+ output.writeString(14, this.sourceData);
+ }
+ if (this.sourcePackage != null && !this.sourcePackage.equals("")) {
+ output.writeString(15, this.sourcePackage);
+ }
+ if (this.transcription != null && !this.transcription.equals("")) {
+ output.writeString(16, this.transcription);
+ }
+ if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
+ output.writeString(17, this.voicemailUri);
+ }
+ if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+ output.writeBytes(18, this.encodedVoicemailKey);
+ }
+ if (this.archived != null && !this.archived.equals("")) {
+ output.writeString(19, this.archived);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.date != null && !this.date.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(1, this.date);
+ }
+ if (this.deleted != null && !this.deleted.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(2, this.deleted);
+ }
+ if (this.dirty != null && !this.dirty.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(3, this.dirty);
+ }
+ if (this.dirType != null && !this.dirType.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(4, this.dirType);
+ }
+ if (this.duration != null && !this.duration.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(5, this.duration);
+ }
+ if (this.hasContent != null && !this.hasContent.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(6, this.hasContent);
+ }
+ if (this.isRead != null && !this.isRead.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(7, this.isRead);
+ }
+ if (this.itemType != null && !this.itemType.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(8, this.itemType);
+ }
+ if (this.lastModified != null && !this.lastModified.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(9, this.lastModified);
+ }
+ if (this.mimeType != null && !this.mimeType.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(10, this.mimeType);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(11, this.number);
+ }
+ if (this.phoneAccountComponentName != null && !this.phoneAccountComponentName.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(12, this.phoneAccountComponentName);
+ }
+ if (this.phoneAccountId != null && !this.phoneAccountId.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(13, this.phoneAccountId);
+ }
+ if (this.sourceData != null && !this.sourceData.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(14, this.sourceData);
+ }
+ if (this.sourcePackage != null && !this.sourcePackage.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(15, this.sourcePackage);
+ }
+ if (this.transcription != null && !this.transcription.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(16, this.transcription);
+ }
+ if (this.voicemailUri != null && !this.voicemailUri.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(17, this.voicemailUri);
+ }
+ if (!java.util.Arrays.equals(this.encodedVoicemailKey, com.google.protobuf.nano.WireFormatNano.EMPTY_BYTES)) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeBytesSize(18, this.encodedVoicemailKey);
+ }
+ if (this.archived != null && !this.archived.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano
+ .computeStringSize(19, this.archived);
+ }
+ return size;
+ }
+
+ @Override
+ public VoicemailInfo mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 10: {
+ this.date = input.readString();
+ break;
+ }
+ case 18: {
+ this.deleted = input.readString();
+ break;
+ }
+ case 26: {
+ this.dirty = input.readString();
+ break;
+ }
+ case 34: {
+ this.dirType = input.readString();
+ break;
+ }
+ case 42: {
+ this.duration = input.readString();
+ break;
+ }
+ case 50: {
+ this.hasContent = input.readString();
+ break;
+ }
+ case 58: {
+ this.isRead = input.readString();
+ break;
+ }
+ case 66: {
+ this.itemType = input.readString();
+ break;
+ }
+ case 74: {
+ this.lastModified = input.readString();
+ break;
+ }
+ case 82: {
+ this.mimeType = input.readString();
+ break;
+ }
+ case 90: {
+ this.number = input.readString();
+ break;
+ }
+ case 98: {
+ this.phoneAccountComponentName = input.readString();
+ break;
+ }
+ case 106: {
+ this.phoneAccountId = input.readString();
+ break;
+ }
+ case 114: {
+ this.sourceData = input.readString();
+ break;
+ }
+ case 122: {
+ this.sourcePackage = input.readString();
+ break;
+ }
+ case 130: {
+ this.transcription = input.readString();
+ break;
+ }
+ case 138: {
+ this.voicemailUri = input.readString();
+ break;
+ }
+ case 146: {
+ this.encodedVoicemailKey = input.readBytes();
+ break;
+ }
+ case 154: {
+ this.archived = input.readString();
+ break;
+ }
+ }
+ }
+ }
+
+ public static VoicemailInfo parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new VoicemailInfo(), data);
+ }
+
+ public static VoicemailInfo parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new VoicemailInfo().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/blocking/AndroidManifest.xml b/java/com/android/dialer/blocking/AndroidManifest.xml
new file mode 100644
index 000000000..08d243988
--- /dev/null
+++ b/java/com/android/dialer/blocking/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.blocking">
+
+ <application android:theme="@style/Theme.AppCompat">
+
+ <provider
+ android:authorities="com.android.dialer.blocking.filterednumberprovider"
+ android:exported="false"
+ android:multiprocess="false"
+ android:name="com.android.dialer.blocking.FilteredNumberProvider"/>
+
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/blocking/BlockNumberDialogFragment.java b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
new file mode 100644
index 000000000..c405b2fe7
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockNumberDialogFragment.java
@@ -0,0 +1,328 @@
+/*
+ * 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.blocking;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.design.widget.Snackbar;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Toast;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnBlockNumberListener;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnUnblockNumberListener;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.voicemailstatus.VisualVoicemailEnabledChecker;
+
+/**
+ * Fragment for confirming and enacting blocking/unblocking a number. Also invokes snackbar
+ * providing undo functionality.
+ */
+public class BlockNumberDialogFragment extends DialogFragment {
+
+ private static final String BLOCK_DIALOG_FRAGMENT = "BlockNumberDialog";
+ private static final String ARG_BLOCK_ID = "argBlockId";
+ private static final String ARG_NUMBER = "argNumber";
+ private static final String ARG_COUNTRY_ISO = "argCountryIso";
+ private static final String ARG_DISPLAY_NUMBER = "argDisplayNumber";
+ private static final String ARG_PARENT_VIEW_ID = "parentViewId";
+ private String mNumber;
+ private String mDisplayNumber;
+ private String mCountryIso;
+ private FilteredNumberAsyncQueryHandler mHandler;
+ private View mParentView;
+ private VisualVoicemailEnabledChecker mVoicemailEnabledChecker;
+ private Callback mCallback;
+
+ public static BlockNumberDialogFragment show(
+ Integer blockId,
+ String number,
+ String countryIso,
+ String displayNumber,
+ Integer parentViewId,
+ FragmentManager fragmentManager,
+ Callback callback) {
+ final BlockNumberDialogFragment newFragment =
+ BlockNumberDialogFragment.newInstance(
+ blockId, number, countryIso, displayNumber, parentViewId);
+
+ newFragment.setCallback(callback);
+ newFragment.show(fragmentManager, BlockNumberDialogFragment.BLOCK_DIALOG_FRAGMENT);
+ return newFragment;
+ }
+
+ private static BlockNumberDialogFragment newInstance(
+ Integer blockId,
+ String number,
+ String countryIso,
+ String displayNumber,
+ Integer parentViewId) {
+ final BlockNumberDialogFragment fragment = new BlockNumberDialogFragment();
+ final Bundle args = new Bundle();
+ if (blockId != null) {
+ args.putInt(ARG_BLOCK_ID, blockId.intValue());
+ }
+ if (parentViewId != null) {
+ args.putInt(ARG_PARENT_VIEW_ID, parentViewId.intValue());
+ }
+ args.putString(ARG_NUMBER, number);
+ args.putString(ARG_COUNTRY_ISO, countryIso);
+ args.putString(ARG_DISPLAY_NUMBER, displayNumber);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ public void setFilteredNumberAsyncQueryHandlerForTesting(
+ FilteredNumberAsyncQueryHandler handler) {
+ mHandler = handler;
+ }
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ final boolean isBlocked = getArguments().containsKey(ARG_BLOCK_ID);
+
+ mNumber = getArguments().getString(ARG_NUMBER);
+ mDisplayNumber = getArguments().getString(ARG_DISPLAY_NUMBER);
+ mCountryIso = getArguments().getString(ARG_COUNTRY_ISO);
+
+ if (TextUtils.isEmpty(mDisplayNumber)) {
+ mDisplayNumber = mNumber;
+ }
+
+ mHandler = new FilteredNumberAsyncQueryHandler(getContext());
+ mVoicemailEnabledChecker = new VisualVoicemailEnabledChecker(getActivity(), null);
+ // Choose not to update VoicemailEnabledChecker, as checks should already been done in
+ // all current use cases.
+ mParentView = getActivity().findViewById(getArguments().getInt(ARG_PARENT_VIEW_ID));
+
+ CharSequence title;
+ String okText;
+ String message;
+ if (isBlocked) {
+ title = null;
+ okText = getString(R.string.unblock_number_ok);
+ message =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.unblock_number_confirmation_title, mDisplayNumber)
+ .toString();
+ } else {
+ title =
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.block_number_confirmation_title, mDisplayNumber);
+ okText = getString(R.string.block_number_ok);
+ if (FilteredNumberCompat.useNewFiltering(getContext())) {
+ message = getString(R.string.block_number_confirmation_message_new_filtering);
+ } else if (mVoicemailEnabledChecker.isVisualVoicemailEnabled()) {
+ message = getString(R.string.block_number_confirmation_message_vvm);
+ } else {
+ message = getString(R.string.block_number_confirmation_message_no_vvm);
+ }
+ }
+
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(getActivity())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(
+ okText,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ if (isBlocked) {
+ unblockNumber();
+ } else {
+ blockNumber();
+ }
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null);
+ return builder.create();
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ String e164Number = PhoneNumberUtils.formatNumberToE164(mNumber, mCountryIso);
+ if (!FilteredNumbersUtil.canBlockNumber(getContext(), e164Number, mNumber)) {
+ dismiss();
+ Toast.makeText(
+ getContext(),
+ ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.invalidNumber, mDisplayNumber),
+ Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ // Dismiss on rotation.
+ dismiss();
+ mCallback = null;
+
+ super.onPause();
+ }
+
+ public void setCallback(Callback callback) {
+ mCallback = callback;
+ }
+
+ private CharSequence getBlockedMessage() {
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.snackbar_number_blocked, mDisplayNumber);
+ }
+
+ private CharSequence getUnblockedMessage() {
+ return ContactDisplayUtils.getTtsSpannedPhoneNumber(
+ getResources(), R.string.snackbar_number_unblocked, mDisplayNumber);
+ }
+
+ private int getActionTextColor() {
+ return getContext().getResources().getColor(R.color.dialer_snackbar_action_text_color);
+ }
+
+ private void blockNumber() {
+ final CharSequence message = getBlockedMessage();
+ final CharSequence undoMessage = getUnblockedMessage();
+ final Callback callback = mCallback;
+ final int actionTextColor = getActionTextColor();
+ final Context applicationContext = getContext().getApplicationContext();
+
+ final OnUnblockNumberListener onUndoListener =
+ new OnUnblockNumberListener() {
+ @Override
+ public void onUnblockComplete(int rows, ContentValues values) {
+ Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
+ if (callback != null) {
+ callback.onChangeFilteredNumberUndo();
+ }
+ }
+ };
+
+ final OnBlockNumberListener onBlockNumberListener =
+ new OnBlockNumberListener() {
+ @Override
+ public void onBlockComplete(final Uri uri) {
+ final View.OnClickListener undoListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Delete the newly created row on 'undo'.
+ Logger.get(applicationContext)
+ .logInteraction(InteractionEvent.Type.UNDO_BLOCK_NUMBER);
+ mHandler.unblock(onUndoListener, uri);
+ }
+ };
+
+ Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.block_number_undo, undoListener)
+ .setActionTextColor(actionTextColor)
+ .show();
+
+ if (callback != null) {
+ callback.onFilterNumberSuccess();
+ }
+
+ if (FilteredNumbersUtil.hasRecentEmergencyCall(applicationContext)) {
+ FilteredNumbersUtil.maybeNotifyCallBlockingDisabled(applicationContext);
+ }
+ }
+ };
+
+ mHandler.blockNumber(onBlockNumberListener, mNumber, mCountryIso);
+ }
+
+ private void unblockNumber() {
+ final CharSequence message = getUnblockedMessage();
+ final CharSequence undoMessage = getBlockedMessage();
+ final Callback callback = mCallback;
+ final int actionTextColor = getActionTextColor();
+ final Context applicationContext = getContext().getApplicationContext();
+
+ final OnBlockNumberListener onUndoListener =
+ new OnBlockNumberListener() {
+ @Override
+ public void onBlockComplete(final Uri uri) {
+ Snackbar.make(mParentView, undoMessage, Snackbar.LENGTH_LONG).show();
+ if (callback != null) {
+ callback.onChangeFilteredNumberUndo();
+ }
+ }
+ };
+
+ mHandler.unblock(
+ new OnUnblockNumberListener() {
+ @Override
+ public void onUnblockComplete(int rows, final ContentValues values) {
+ final View.OnClickListener undoListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ // Re-insert the row on 'undo', with a new ID.
+ Logger.get(applicationContext)
+ .logInteraction(InteractionEvent.Type.UNDO_UNBLOCK_NUMBER);
+ mHandler.blockNumber(onUndoListener, values);
+ }
+ };
+
+ Snackbar.make(mParentView, message, Snackbar.LENGTH_LONG)
+ .setAction(R.string.block_number_undo, undoListener)
+ .setActionTextColor(actionTextColor)
+ .show();
+
+ if (callback != null) {
+ callback.onUnfilterNumberSuccess();
+ }
+ }
+ },
+ getArguments().getInt(ARG_BLOCK_ID));
+ }
+
+ /**
+ * Use a callback interface to update UI after success/undo. Favor this approach over other more
+ * standard paradigms because of the variety of scenarios in which the DialogFragment can be
+ * invoked (by an Activity, by a fragment, by an adapter, by an adapter list item). Because of
+ * this, we do NOT support retaining state on rotation, and will dismiss the dialog upon rotation
+ * instead.
+ */
+ public interface Callback {
+
+ /** Called when a number is successfully added to the set of filtered numbers */
+ void onFilterNumberSuccess();
+
+ /** Called when a number is successfully removed from the set of filtered numbers */
+ void onUnfilterNumberSuccess();
+
+ /** Called when the action of filtering or unfiltering a number is undone */
+ void onChangeFilteredNumberUndo();
+ }
+}
diff --git a/java/com/android/dialer/blocking/BlockReportSpamDialogs.java b/java/com/android/dialer/blocking/BlockReportSpamDialogs.java
new file mode 100644
index 000000000..b5f81fcc5
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockReportSpamDialogs.java
@@ -0,0 +1,305 @@
+/*
+ * 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.blocking;
+
+import android.app.Activity;
+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.support.annotation.Nullable;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+/** Helper class for creating block/report dialog fragments. */
+public class BlockReportSpamDialogs {
+
+ public static final String BLOCK_REPORT_SPAM_DIALOG_TAG = "BlockReportSpamDialog";
+ public static final String BLOCK_DIALOG_TAG = "BlockDialog";
+ public static final String UNBLOCK_DIALOG_TAG = "UnblockDialog";
+ public static final String NOT_SPAM_DIALOG_TAG = "NotSpamDialog";
+
+ /** Creates a dialog with the default cancel button listener (dismisses dialog). */
+ private static AlertDialog.Builder createDialogBuilder(
+ Activity activity, final DialogFragment fragment) {
+ return new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
+ .setCancelable(true)
+ .setNegativeButton(
+ android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ fragment.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Creates a generic click listener which dismisses the fragment and then calls the actual
+ * listener.
+ */
+ private static DialogInterface.OnClickListener createGenericOnClickListener(
+ final DialogFragment fragment, final OnConfirmListener listener) {
+ return new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ fragment.dismiss();
+ listener.onClick();
+ }
+ };
+ }
+
+ private static String getBlockMessage(Context context) {
+ String message;
+ if (FilteredNumberCompat.useNewFiltering(context)) {
+ message = context.getString(R.string.block_number_confirmation_message_new_filtering);
+ } else {
+ message = context.getString(R.string.block_report_number_alert_details);
+ }
+ return message;
+ }
+
+ /**
+ * Listener passed to block/report spam dialog for positive click in {@link
+ * BlockReportSpamDialogFragment}.
+ */
+ public interface OnSpamDialogClickListener {
+
+ /**
+ * Called when user clicks on positive button in block/report spam dialog.
+ *
+ * @param isSpamChecked Whether the spam checkbox is checked.
+ */
+ void onClick(boolean isSpamChecked);
+ }
+
+ /** Listener passed to all dialogs except the block/report spam dialog for positive click. */
+ public interface OnConfirmListener {
+
+ /** Called when user clicks on positive button in the dialog. */
+ void onClick();
+ }
+
+ /** Contains the common attributes between all block/unblock/report dialog fragments. */
+ private static class CommonDialogsFragment extends DialogFragment {
+
+ /** The number to display in the dialog title. */
+ protected String mDisplayNumber;
+
+ /** Called when dialog positive button is pressed. */
+ protected OnConfirmListener mPositiveListener;
+
+ /** Called when dialog is dismissed. */
+ @Nullable protected DialogInterface.OnDismissListener mDismissListener;
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (mDismissListener != null) {
+ mDismissListener.onDismiss(dialog);
+ }
+ super.onDismiss(dialog);
+ }
+
+ @Override
+ public void onPause() {
+ // The dialog is dismissed onPause, i.e. rotation.
+ dismiss();
+ mDismissListener = null;
+ mPositiveListener = null;
+ mDisplayNumber = null;
+ super.onPause();
+ }
+ }
+
+ /** Dialog for block/report spam with the mark as spam checkbox. */
+ public static class BlockReportSpamDialogFragment extends CommonDialogsFragment {
+
+ /** Called when dialog positive button is pressed. */
+ private OnSpamDialogClickListener mPositiveListener;
+
+ /** Whether the mark as spam checkbox is checked before displaying the dialog. */
+ private boolean mSpamChecked;
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ boolean spamChecked,
+ OnSpamDialogClickListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ BlockReportSpamDialogFragment fragment = new BlockReportSpamDialogFragment();
+ fragment.mSpamChecked = spamChecked;
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ View dialogView = View.inflate(getActivity(), R.layout.block_report_spam_dialog, null);
+ final CheckBox isSpamCheckbox =
+ (CheckBox) dialogView.findViewById(R.id.report_number_as_spam_action);
+ // Listen for changes on the checkbox and update if orientation changes
+ isSpamCheckbox.setChecked(mSpamChecked);
+ isSpamCheckbox.setOnCheckedChangeListener(
+ new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mSpamChecked = isChecked;
+ }
+ });
+
+ TextView details = (TextView) dialogView.findViewById(R.id.block_details);
+ details.setText(getBlockMessage(getContext()));
+
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ Dialog dialog =
+ alertDialogBuilder
+ .setView(dialogView)
+ .setTitle(getString(R.string.block_report_number_alert_title, mDisplayNumber))
+ .setPositiveButton(
+ R.string.block_number_ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ mPositiveListener.onClick(isSpamCheckbox.isChecked());
+ }
+ })
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+
+ /** Dialog for blocking a number. */
+ public static class BlockDialogFragment extends CommonDialogsFragment {
+
+ private boolean isSpamEnabled;
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ boolean isSpamEnabled,
+ OnConfirmListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ BlockDialogFragment fragment = new BlockDialogFragment();
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ fragment.isSpamEnabled = isSpamEnabled;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ // Return the newly created dialog
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ Dialog dialog =
+ alertDialogBuilder
+ .setTitle(getString(R.string.block_number_confirmation_title, mDisplayNumber))
+ .setMessage(
+ isSpamEnabled
+ ? getString(
+ R.string.block_number_alert_details, getBlockMessage(getContext()))
+ : getString(R.string.block_report_number_alert_details))
+ .setPositiveButton(
+ R.string.block_number_ok, createGenericOnClickListener(this, mPositiveListener))
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+
+ /** Dialog for unblocking a number. */
+ public static class UnblockDialogFragment extends CommonDialogsFragment {
+
+ /** Whether or not the number is spam. */
+ private boolean mIsSpam;
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ boolean isSpam,
+ OnConfirmListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ UnblockDialogFragment fragment = new UnblockDialogFragment();
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mIsSpam = isSpam;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ // Return the newly created dialog
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ if (mIsSpam) {
+ alertDialogBuilder
+ .setMessage(R.string.unblock_number_alert_details)
+ .setTitle(getString(R.string.unblock_report_number_alert_title, mDisplayNumber));
+ } else {
+ alertDialogBuilder.setMessage(
+ getString(R.string.unblock_report_number_alert_title, mDisplayNumber));
+ }
+ Dialog dialog =
+ alertDialogBuilder
+ .setPositiveButton(
+ R.string.unblock_number_ok, createGenericOnClickListener(this, mPositiveListener))
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+
+ /** Dialog for reporting a number as not spam. */
+ public static class ReportNotSpamDialogFragment extends CommonDialogsFragment {
+
+ public static DialogFragment newInstance(
+ String displayNumber,
+ OnConfirmListener positiveListener,
+ @Nullable DialogInterface.OnDismissListener dismissListener) {
+ ReportNotSpamDialogFragment fragment = new ReportNotSpamDialogFragment();
+ fragment.mDisplayNumber = displayNumber;
+ fragment.mPositiveListener = positiveListener;
+ fragment.mDismissListener = dismissListener;
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ // Return the newly created dialog
+ AlertDialog.Builder alertDialogBuilder = createDialogBuilder(getActivity(), this);
+ Dialog dialog =
+ alertDialogBuilder
+ .setTitle(R.string.report_not_spam_alert_title)
+ .setMessage(getString(R.string.report_not_spam_alert_details, mDisplayNumber))
+ .setPositiveButton(
+ R.string.report_not_spam_alert_button,
+ createGenericOnClickListener(this, mPositiveListener))
+ .create();
+ dialog.setCanceledOnTouchOutside(true);
+ return dialog;
+ }
+ }
+}
diff --git a/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
new file mode 100644
index 000000000..1773e9b84
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java
@@ -0,0 +1,110 @@
+/*
+ * 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.blocking;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener;
+import com.android.dialer.common.LogUtil;
+import java.util.Objects;
+
+/**
+ * Class responsible for checking if the user can be auto-migrated to {@link
+ * android.provider.BlockedNumberContract} blocking. In order for this to happen, the user cannot
+ * have any numbers that are blocked in the Dialer solution.
+ */
+public class BlockedNumbersAutoMigrator {
+
+ static final String HAS_CHECKED_AUTO_MIGRATE_KEY = "checkedAutoMigrate";
+
+ @NonNull private final Context context;
+ @NonNull private final SharedPreferences sharedPreferences;
+ @NonNull private final FilteredNumberAsyncQueryHandler queryHandler;
+
+ /**
+ * Constructs the BlockedNumbersAutoMigrator with the given {@link SharedPreferences} and {@link
+ * FilteredNumberAsyncQueryHandler}.
+ *
+ * @param sharedPreferences The SharedPreferences used to persist information.
+ * @param queryHandler The FilteredNumberAsyncQueryHandler used to determine if there are blocked
+ * numbers.
+ * @throws NullPointerException if sharedPreferences or queryHandler are null.
+ */
+ public BlockedNumbersAutoMigrator(
+ @NonNull Context context,
+ @NonNull SharedPreferences sharedPreferences,
+ @NonNull FilteredNumberAsyncQueryHandler queryHandler) {
+ this.context = Objects.requireNonNull(context);
+ this.sharedPreferences = Objects.requireNonNull(sharedPreferences);
+ this.queryHandler = Objects.requireNonNull(queryHandler);
+ }
+
+ /**
+ * Attempts to perform the auto-migration. Auto-migration will only be attempted once and can be
+ * performed only when the user has no blocked numbers. As a result of this method, the user will
+ * be migrated to the framework blocking solution, as determined by {@link
+ * FilteredNumberCompat#hasMigratedToNewBlocking()}.
+ */
+ public void autoMigrate() {
+ if (!shouldAttemptAutoMigrate()) {
+ return;
+ }
+
+ LogUtil.i("BlockedNumbersAutoMigrator", "attempting to auto-migrate.");
+ queryHandler.hasBlockedNumbers(
+ new OnHasBlockedNumbersListener() {
+ @Override
+ public void onHasBlockedNumbers(boolean hasBlockedNumbers) {
+ if (hasBlockedNumbers) {
+ LogUtil.i("BlockedNumbersAutoMigrator", "not auto-migrating: blocked numbers exist.");
+ return;
+ }
+ LogUtil.i("BlockedNumbersAutoMigrator", "auto-migrating: no blocked numbers.");
+ FilteredNumberCompat.setHasMigratedToNewBlocking(context, true);
+ }
+ });
+ }
+
+ private boolean shouldAttemptAutoMigrate() {
+ if (sharedPreferences.contains(HAS_CHECKED_AUTO_MIGRATE_KEY)) {
+ LogUtil.v("BlockedNumbersAutoMigrator", "not attempting auto-migrate: already checked once.");
+ return false;
+ }
+
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ // This may be the case where the user is on the lock screen, so we shouldn't record that the
+ // migration status was checked.
+ LogUtil.i(
+ "BlockedNumbersAutoMigrator", "not attempting auto-migrate: current user can't block");
+ return false;
+ }
+ LogUtil.i("BlockedNumbersAutoMigrator", "updating state as already checked for auto-migrate.");
+ sharedPreferences.edit().putBoolean(HAS_CHECKED_AUTO_MIGRATE_KEY, true).apply();
+
+ if (!FilteredNumberCompat.canUseNewFiltering()) {
+ LogUtil.i("BlockedNumbersAutoMigrator", "not attempting auto-migrate: not available.");
+ return false;
+ }
+
+ if (FilteredNumberCompat.hasMigratedToNewBlocking(context)) {
+ LogUtil.i("BlockedNumbersAutoMigrator", "not attempting auto-migrate: already migrated.");
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/dialer/blocking/BlockedNumbersMigrator.java b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
new file mode 100644
index 000000000..88f474a84
--- /dev/null
+++ b/java/com/android/dialer/blocking/BlockedNumbersMigrator.java
@@ -0,0 +1,159 @@
+/*
+ * 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.blocking;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.support.annotation.RequiresApi;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import java.util.Objects;
+
+/**
+ * Class which should be used to migrate numbers from {@link FilteredNumberContract} blocking to
+ * {@link android.provider.BlockedNumberContract} blocking.
+ */
+@TargetApi(VERSION_CODES.M)
+public class BlockedNumbersMigrator {
+
+ private final Context context;
+
+ /**
+ * Creates a new BlockedNumbersMigrate, using the given {@link ContentResolver} to perform queries
+ * against the blocked numbers tables.
+ */
+ public BlockedNumbersMigrator(Context context) {
+ this.context = Objects.requireNonNull(context);
+ }
+
+ @RequiresApi(VERSION_CODES.N)
+ @TargetApi(VERSION_CODES.N)
+ private static boolean migrateToNewBlockingInBackground(ContentResolver resolver) {
+ try (Cursor cursor =
+ resolver.query(
+ FilteredNumber.CONTENT_URI,
+ new String[] {FilteredNumberColumns.NUMBER},
+ null,
+ null,
+ null)) {
+ if (cursor == null) {
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground", "migrate - cursor was null");
+ return false;
+ }
+
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground",
+ "migrate - attempting to migrate " + cursor.getCount() + "numbers");
+
+ int numMigrated = 0;
+ while (cursor.moveToNext()) {
+ String originalNumber =
+ cursor.getString(cursor.getColumnIndex(FilteredNumberColumns.NUMBER));
+ if (isNumberInNewBlocking(resolver, originalNumber)) {
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground",
+ "migrate - number was already blocked in new blocking");
+ continue;
+ }
+ ContentValues values = new ContentValues();
+ values.put(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, originalNumber);
+ resolver.insert(BlockedNumbers.CONTENT_URI, values);
+ ++numMigrated;
+ }
+ LogUtil.i(
+ "BlockedNumbersMigrator.migrateToNewBlockingInBackground",
+ "migrate - migration complete. " + numMigrated + " numbers migrated.");
+ return true;
+ }
+ }
+
+ @RequiresApi(VERSION_CODES.N)
+ @TargetApi(VERSION_CODES.N)
+ private static boolean isNumberInNewBlocking(ContentResolver resolver, String originalNumber) {
+ try (Cursor cursor =
+ resolver.query(
+ BlockedNumbers.CONTENT_URI,
+ new String[] {BlockedNumbers.COLUMN_ID},
+ BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " = ?",
+ new String[] {originalNumber},
+ null)) {
+ return cursor != null && cursor.getCount() != 0;
+ }
+ }
+
+ /**
+ * Copies all of the numbers in the {@link FilteredNumberContract} block list to the {@link
+ * android.provider.BlockedNumberContract} block list.
+ *
+ * @param listener {@link Listener} called once the migration is complete.
+ * @return {@code true} if the migrate can be attempted, {@code false} otherwise.
+ * @throws NullPointerException if listener is null
+ */
+ public boolean migrate(final Listener listener) {
+ LogUtil.i("BlockedNumbersMigrator.migrate", "migrate - start");
+ if (!FilteredNumberCompat.canUseNewFiltering()) {
+ LogUtil.i("BlockedNumbersMigrator.migrate", "migrate - can't use new filtering");
+ return false;
+ }
+ Objects.requireNonNull(listener);
+ new MigratorTask(listener).execute();
+ return true;
+ }
+
+ /**
+ * Listener for the operation to migrate from {@link FilteredNumberContract} blocking to {@link
+ * android.provider.BlockedNumberContract} blocking.
+ */
+ public interface Listener {
+
+ /** Called when the migration operation is finished. */
+ void onComplete();
+ }
+
+ @TargetApi(VERSION_CODES.N)
+ private class MigratorTask extends AsyncTask<Void, Void, Boolean> {
+
+ private final Listener listener;
+
+ public MigratorTask(Listener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ LogUtil.i("BlockedNumbersMigrator.doInBackground", "migrate - start background migration");
+ return migrateToNewBlockingInBackground(context.getContentResolver());
+ }
+
+ @Override
+ protected void onPostExecute(Boolean isSuccessful) {
+ LogUtil.i("BlockedNumbersMigrator.onPostExecute", "migrate - marking migration complete");
+ FilteredNumberCompat.setHasMigratedToNewBlocking(context, isSuccessful);
+ LogUtil.i("BlockedNumbersMigrator.onPostExecute", "migrate - calling listener");
+ listener.onComplete();
+ }
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
new file mode 100644
index 000000000..852e7a0ed
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java
@@ -0,0 +1,428 @@
+/*
+ * 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.blocking;
+
+import android.annotation.TargetApi;
+import android.content.AsyncQueryHandler;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.UserManagerCompat;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class FilteredNumberAsyncQueryHandler extends AsyncQueryHandler {
+
+ public static final int INVALID_ID = -1;
+ // Id used to replace null for blocked id since ConcurrentHashMap doesn't allow null key/value.
+ @VisibleForTesting static final int BLOCKED_NUMBER_CACHE_NULL_ID = -1;
+
+ @VisibleForTesting
+ static final Map<String, Integer> blockedNumberCache = new ConcurrentHashMap<>();
+
+ private static final int NO_TOKEN = 0;
+ private final Context context;
+
+ public FilteredNumberAsyncQueryHandler(Context context) {
+ super(context.getContentResolver());
+ this.context = context;
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cookie != null) {
+ ((Listener) cookie).onQueryComplete(token, cookie, cursor);
+ }
+ }
+
+ @Override
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {
+ if (cookie != null) {
+ ((Listener) cookie).onInsertComplete(token, cookie, uri);
+ }
+ }
+
+ @Override
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ if (cookie != null) {
+ ((Listener) cookie).onUpdateComplete(token, cookie, result);
+ }
+ }
+
+ @Override
+ protected void onDeleteComplete(int token, Object cookie, int result) {
+ if (cookie != null) {
+ ((Listener) cookie).onDeleteComplete(token, cookie, result);
+ }
+ }
+
+ public void hasBlockedNumbers(final OnHasBlockedNumbersListener listener) {
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ listener.onHasBlockedNumbers(false);
+ return;
+ }
+ startQuery(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ listener.onHasBlockedNumbers(cursor != null && cursor.getCount() > 0);
+ }
+ },
+ FilteredNumberCompat.getContentUri(context, null),
+ new String[] {FilteredNumberCompat.getIdColumnName(context)},
+ FilteredNumberCompat.useNewFiltering(context)
+ ? null
+ : FilteredNumberColumns.TYPE + "=" + FilteredNumberTypes.BLOCKED_NUMBER,
+ null,
+ null);
+ }
+
+ /**
+ * Checks if the given number is blocked, calling the given {@link OnCheckBlockedListener} with
+ * the id for the blocked number, {@link #INVALID_ID}, or {@code null} based on the result of the
+ * check.
+ */
+ public void isBlockedNumber(
+ final OnCheckBlockedListener listener, @Nullable final String number, String countryIso) {
+ if (number == null) {
+ listener.onCheckComplete(INVALID_ID);
+ return;
+ }
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ listener.onCheckComplete(null);
+ return;
+ }
+ Integer cachedId = blockedNumberCache.get(number);
+ if (cachedId != null) {
+ if (listener == null) {
+ return;
+ }
+ if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
+ cachedId = null;
+ }
+ listener.onCheckComplete(cachedId);
+ return;
+ }
+
+ String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
+ if (TextUtils.isEmpty(formattedNumber)) {
+ listener.onCheckComplete(INVALID_ID);
+ blockedNumberCache.put(number, INVALID_ID);
+ return;
+ }
+
+ if (!UserManagerCompat.isUserUnlocked(context)) {
+ LogUtil.i(
+ "FilteredNumberAsyncQueryHandler.isBlockedNumber",
+ "Device locked in FBE mode, cannot access blocked number database");
+ listener.onCheckComplete(INVALID_ID);
+ return;
+ }
+
+ startQuery(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ /*
+ * In the frameworking blocking, numbers can be blocked in both e164 format
+ * and not, resulting in multiple rows being returned for this query. For
+ * example, both '16502530000' and '6502530000' can exist at the same time
+ * and will be returned by this query.
+ */
+ if (cursor == null || cursor.getCount() == 0) {
+ blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
+ listener.onCheckComplete(null);
+ return;
+ }
+ cursor.moveToFirst();
+ // New filtering doesn't have a concept of type
+ if (!FilteredNumberCompat.useNewFiltering(context)
+ && cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns.TYPE))
+ != FilteredNumberTypes.BLOCKED_NUMBER) {
+ blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
+ listener.onCheckComplete(null);
+ return;
+ }
+ Integer blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+ blockedNumberCache.put(number, blockedId);
+ listener.onCheckComplete(blockedId);
+ }
+ },
+ FilteredNumberCompat.getContentUri(context, null),
+ FilteredNumberCompat.filter(
+ new String[] {
+ FilteredNumberCompat.getIdColumnName(context),
+ FilteredNumberCompat.getTypeColumnName(context)
+ }),
+ getIsBlockedNumberSelection(e164Number != null) + " = ?",
+ new String[] {formattedNumber},
+ null);
+ }
+
+ /**
+ * Synchronously check if this number has been blocked.
+ *
+ * @return blocked id.
+ */
+ @TargetApi(VERSION_CODES.M)
+ @Nullable
+ public Integer getBlockedIdSynchronousForCalllogOnly(@Nullable String number, String countryIso) {
+ Assert.isWorkerThread();
+ if (number == null) {
+ return null;
+ }
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ return null;
+ }
+ Integer cachedId = blockedNumberCache.get(number);
+ if (cachedId != null) {
+ if (cachedId == BLOCKED_NUMBER_CACHE_NULL_ID) {
+ cachedId = null;
+ }
+ return cachedId;
+ }
+
+ String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ String formattedNumber = FilteredNumbersUtil.getBlockableNumber(context, e164Number, number);
+ if (TextUtils.isEmpty(formattedNumber)) {
+ return null;
+ }
+
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ FilteredNumberCompat.getContentUri(context, null),
+ FilteredNumberCompat.filter(
+ new String[] {
+ FilteredNumberCompat.getIdColumnName(context),
+ FilteredNumberCompat.getTypeColumnName(context)
+ }),
+ getIsBlockedNumberSelection(e164Number != null) + " = ?",
+ new String[] {formattedNumber},
+ null)) {
+ /*
+ * In the frameworking blocking, numbers can be blocked in both e164 format
+ * and not, resulting in multiple rows being returned for this query. For
+ * example, both '16502530000' and '6502530000' can exist at the same time
+ * and will be returned by this query.
+ */
+ if (cursor == null || cursor.getCount() == 0) {
+ blockedNumberCache.put(number, BLOCKED_NUMBER_CACHE_NULL_ID);
+ return null;
+ }
+ cursor.moveToFirst();
+ int blockedId = cursor.getInt(cursor.getColumnIndex(FilteredNumberColumns._ID));
+ blockedNumberCache.put(number, blockedId);
+ return blockedId;
+ } catch (SecurityException e) {
+ LogUtil.e("FilteredNumberAsyncQueryHandler.getBlockedIdSynchronousForCalllogOnly", null, e);
+ return null;
+ }
+ }
+
+ @VisibleForTesting
+ public void clearCache() {
+ blockedNumberCache.clear();
+ }
+
+ /*
+ * TODO: b/27779827, non-e164 numbers can be blocked in the new form of blocking. As a
+ * temporary workaround, determine which column of the database to query based on whether the
+ * number is e164 or not.
+ */
+ private String getIsBlockedNumberSelection(boolean isE164Number) {
+ if (FilteredNumberCompat.useNewFiltering(context) && !isE164Number) {
+ return FilteredNumberCompat.getOriginalNumberColumnName(context);
+ }
+ return FilteredNumberCompat.getE164NumberColumnName(context);
+ }
+
+ public void blockNumber(
+ final OnBlockNumberListener listener, String number, @Nullable String countryIso) {
+ blockNumber(listener, null, number, countryIso);
+ }
+
+ /** Add a number manually blocked by the user. */
+ public void blockNumber(
+ final OnBlockNumberListener listener,
+ @Nullable String normalizedNumber,
+ String number,
+ @Nullable String countryIso) {
+ blockNumber(
+ listener,
+ FilteredNumberCompat.newBlockNumberContentValues(
+ context, number, normalizedNumber, countryIso));
+ }
+
+ /**
+ * Block a number with specified ContentValues. Can be manually added or a restored row from
+ * performing the 'undo' action after unblocking.
+ */
+ public void blockNumber(final OnBlockNumberListener listener, ContentValues values) {
+ blockedNumberCache.clear();
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ listener.onBlockComplete(null);
+ return;
+ }
+ startInsert(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ public void onInsertComplete(int token, Object cookie, Uri uri) {
+ if (listener != null) {
+ listener.onBlockComplete(uri);
+ }
+ }
+ },
+ FilteredNumberCompat.getContentUri(context, null),
+ values);
+ }
+
+ /**
+ * Unblocks the number with the given id.
+ *
+ * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
+ * unblocked.
+ * @param id The id of the number to unblock.
+ */
+ public void unblock(@Nullable final OnUnblockNumberListener listener, Integer id) {
+ if (id == null) {
+ throw new IllegalArgumentException("Null id passed into unblock");
+ }
+ unblock(listener, FilteredNumberCompat.getContentUri(context, id));
+ }
+
+ /**
+ * Removes row from database.
+ *
+ * @param listener (optional) The {@link OnUnblockNumberListener} called after the number is
+ * unblocked.
+ * @param uri The uri of row to remove, from {@link FilteredNumberAsyncQueryHandler#blockNumber}.
+ */
+ public void unblock(@Nullable final OnUnblockNumberListener listener, final Uri uri) {
+ blockedNumberCache.clear();
+ if (!FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ if (listener != null) {
+ listener.onUnblockComplete(0, null);
+ }
+ return;
+ }
+ startQuery(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ public void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ int rowsReturned = cursor == null ? 0 : cursor.getCount();
+ if (rowsReturned != 1) {
+ throw new SQLiteDatabaseCorruptException(
+ "Returned " + rowsReturned + " rows for uri " + uri + "where 1 expected.");
+ }
+ cursor.moveToFirst();
+ final ContentValues values = new ContentValues();
+ DatabaseUtils.cursorRowToContentValues(cursor, values);
+ values.remove(FilteredNumberCompat.getIdColumnName(context));
+
+ startDelete(
+ NO_TOKEN,
+ new Listener() {
+ @Override
+ public void onDeleteComplete(int token, Object cookie, int result) {
+ if (listener != null) {
+ listener.onUnblockComplete(result, values);
+ }
+ }
+ },
+ uri,
+ null,
+ null);
+ }
+ },
+ uri,
+ null,
+ null,
+ null,
+ null);
+ }
+
+ public interface OnCheckBlockedListener {
+
+ /**
+ * Invoked after querying if a number is blocked.
+ *
+ * @param id The ID of the row if blocked, null otherwise.
+ */
+ void onCheckComplete(Integer id);
+ }
+
+ public interface OnBlockNumberListener {
+
+ /**
+ * Invoked after inserting a blocked number.
+ *
+ * @param uri The uri of the newly created row.
+ */
+ void onBlockComplete(Uri uri);
+ }
+
+ public interface OnUnblockNumberListener {
+
+ /**
+ * Invoked after removing a blocked number
+ *
+ * @param rows The number of rows affected (expected value 1).
+ * @param values The deleted data (used for restoration).
+ */
+ void onUnblockComplete(int rows, ContentValues values);
+ }
+
+ public interface OnHasBlockedNumbersListener {
+
+ /**
+ * @param hasBlockedNumbers {@code true} if any blocked numbers are stored. {@code false}
+ * otherwise.
+ */
+ void onHasBlockedNumbers(boolean hasBlockedNumbers);
+ }
+
+ /** Methods for FilteredNumberAsyncQueryHandler result returns. */
+ private abstract static class Listener {
+
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {}
+
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {}
+
+ protected void onUpdateComplete(int token, Object cookie, int result) {}
+
+ protected void onDeleteComplete(int token, Object cookie, int result) {}
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberCompat.java b/java/com/android/dialer/blocking/FilteredNumberCompat.java
new file mode 100644
index 000000000..0ee85d897
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumberCompat.java
@@ -0,0 +1,320 @@
+/*
+ * 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.blocking;
+
+import android.annotation.TargetApi;
+import android.app.FragmentManager;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.UserManager;
+import android.preference.PreferenceManager;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.TelecomManager;
+import android.telephony.PhoneNumberUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Compatibility class to encapsulate logic to switch between call blocking using {@link
+ * com.android.dialer.database.FilteredNumberContract} and using {@link
+ * android.provider.BlockedNumberContract}. This class should be used rather than explicitly
+ * referencing columns from either contract class in situations where both blocking solutions may be
+ * used.
+ */
+public class FilteredNumberCompat {
+
+ private static Boolean canAttemptBlockOperationsForTest;
+
+ @VisibleForTesting
+ public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking";
+
+ /** @return The column name for ID in the filtered number database. */
+ public static String getIdColumnName(Context context) {
+ return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID;
+ }
+
+ /**
+ * @return The column name for type in the filtered number database. Will be {@code null} for the
+ * framework blocking implementation.
+ */
+ @Nullable
+ public static String getTypeColumnName(Context context) {
+ return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE;
+ }
+
+ /**
+ * @return The column name for source in the filtered number database. Will be {@code null} for
+ * the framework blocking implementation
+ */
+ @Nullable
+ public static String getSourceColumnName(Context context) {
+ return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE;
+ }
+
+ /** @return The column name for the original number in the filtered number database. */
+ public static String getOriginalNumberColumnName(Context context) {
+ return useNewFiltering(context)
+ ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER
+ : FilteredNumberColumns.NUMBER;
+ }
+
+ /**
+ * @return The column name for country iso in the filtered number database. Will be {@code null}
+ * the framework blocking implementation
+ */
+ @Nullable
+ public static String getCountryIsoColumnName(Context context) {
+ return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO;
+ }
+
+ /** @return The column name for the e164 formatted number in the filtered number database. */
+ public static String getE164NumberColumnName(Context context) {
+ return useNewFiltering(context)
+ ? BlockedNumbers.COLUMN_E164_NUMBER
+ : FilteredNumberColumns.NORMALIZED_NUMBER;
+ }
+
+ /**
+ * @return {@code true} if the current SDK version supports using new filtering, {@code false}
+ * otherwise.
+ */
+ public static boolean canUseNewFiltering() {
+ return VERSION.SDK_INT >= VERSION_CODES.N;
+ }
+
+ /**
+ * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary
+ * migration has been performed, {@code false} otherwise.
+ */
+ public static boolean useNewFiltering(Context context) {
+ return canUseNewFiltering() && hasMigratedToNewBlocking(context);
+ }
+
+ /**
+ * @return {@code true} if the user has migrated to use {@link
+ * android.provider.BlockedNumberContract} blocking, {@code false} otherwise.
+ */
+ public static boolean hasMigratedToNewBlocking(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false);
+ }
+
+ /**
+ * Called to inform this class whether the user has fully migrated to use {@link
+ * android.provider.BlockedNumberContract} blocking or not.
+ *
+ * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise.
+ */
+ public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) {
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated)
+ .apply();
+ }
+
+ /**
+ * Gets the content {@link Uri} for number filtering.
+ *
+ * @param id The optional id to append with the base content uri.
+ * @return The Uri for number filtering.
+ */
+ public static Uri getContentUri(Context context, @Nullable Integer id) {
+ if (id == null) {
+ return getBaseUri(context);
+ }
+ return ContentUris.withAppendedId(getBaseUri(context), id);
+ }
+
+ private static Uri getBaseUri(Context context) {
+ // Explicit version check to aid static analysis
+ return useNewFiltering(context) && VERSION.SDK_INT >= VERSION_CODES.N
+ ? BlockedNumbers.CONTENT_URI
+ : FilteredNumber.CONTENT_URI;
+ }
+
+ /**
+ * Removes any null column names from the given projection array. This method is intended to be
+ * used to strip out any column names that aren't available in every version of number blocking.
+ * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that
+ * no non-existant columns are queried FilteredNumberCompat.filter(new String[]
+ * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()},
+ * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); }
+ *
+ * @param projection The projection array.
+ * @return The filtered projection array.
+ */
+ @Nullable
+ public static String[] filter(@Nullable String[] projection) {
+ if (projection == null) {
+ return null;
+ }
+ List<String> filtered = new ArrayList<>();
+ for (String column : projection) {
+ if (column != null) {
+ filtered.add(column);
+ }
+ }
+ return filtered.toArray(new String[filtered.size()]);
+ }
+
+ /**
+ * Creates a new {@link ContentValues} suitable for inserting in the filtered number table.
+ *
+ * @param number The unformatted number to insert.
+ * @param e164Number (optional) The number to insert formatted to E164 standard.
+ * @param countryIso (optional) The country iso to use to format the number.
+ * @return The ContentValues to insert.
+ * @throws NullPointerException If number is null.
+ */
+ public static ContentValues newBlockNumberContentValues(
+ Context context, String number, @Nullable String e164Number, @Nullable String countryIso) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number));
+ if (!useNewFiltering(context)) {
+ if (e164Number == null) {
+ e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ }
+ contentValues.put(getE164NumberColumnName(context), e164Number);
+ contentValues.put(getCountryIsoColumnName(context), countryIso);
+ contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER);
+ contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER);
+ }
+ return contentValues;
+ }
+
+ /**
+ * Shows block number migration dialog if necessary.
+ *
+ * @param fragmentManager The {@link FragmentManager} used to show fragments.
+ * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete.
+ * @return boolean True if migration dialog is shown.
+ */
+ public static boolean maybeShowBlockNumberMigrationDialog(
+ Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) {
+ if (shouldShowMigrationDialog(context)) {
+ LogUtil.i(
+ "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog",
+ "maybeShowBlockNumberMigrationDialog - showing migration dialog");
+ MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener)
+ .show(fragmentManager, "MigrateBlockedNumbers");
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean shouldShowMigrationDialog(Context context) {
+ return canUseNewFiltering() && !hasMigratedToNewBlocking(context);
+ }
+
+ /**
+ * Creates the {@link Intent} which opens the blocked numbers management interface.
+ *
+ * @param context The {@link Context}.
+ * @return The intent.
+ */
+ public static Intent createManageBlockedNumbersIntent(Context context) {
+ // Explicit version check to aid static analysis
+ if (canUseNewFiltering()
+ && hasMigratedToNewBlocking(context)
+ && VERSION.SDK_INT >= VERSION_CODES.N) {
+ return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent();
+ }
+ Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS");
+ intent.setPackage(context.getPackageName());
+ return intent;
+ }
+
+ /**
+ * Method used to determine if block operations are possible.
+ *
+ * @param context The {@link Context}.
+ * @return {@code true} if the app and user can block numbers, {@code false} otherwise.
+ */
+ public static boolean canAttemptBlockOperations(Context context) {
+ if (canAttemptBlockOperationsForTest != null) {
+ return canAttemptBlockOperationsForTest;
+ }
+
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ // Dialer blocking, must be primary user
+ return context.getSystemService(UserManager.class).isSystemUser();
+ }
+
+ // Great Wall blocking, must be primary user and the default or system dialer
+ // TODO: check that we're the system Dialer
+ return TelecomUtil.isDefaultDialer(context)
+ && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
+ }
+
+ static void setCanAttemptBlockOperationsForTest(boolean canAttempt) {
+ canAttemptBlockOperationsForTest = canAttempt;
+ }
+
+ /**
+ * Used to determine if the call blocking settings can be opened.
+ *
+ * @param context The {@link Context}.
+ * @return {@code true} if the current user can open the call blocking settings, {@code false}
+ * otherwise.
+ */
+ public static boolean canCurrentUserOpenBlockSettings(Context context) {
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ // Dialer blocking, must be primary user
+ return context.getSystemService(UserManager.class).isSystemUser();
+ }
+ // BlockedNumberContract blocking, verify through Contract API
+ return TelecomUtil.isDefaultDialer(context)
+ && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
+ }
+
+ /**
+ * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it
+ * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't
+ * available, using this method ensures that the Dialer doesn't crash when on that screen.
+ *
+ * @param context The {@link Context}.
+ * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an
+ * exception was thrown.
+ */
+ @TargetApi(VERSION_CODES.N)
+ private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) {
+ try {
+ return BlockedNumberContract.canCurrentUserBlockNumbers(context);
+ } catch (Exception e) {
+ LogUtil.e(
+ "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers",
+ "Exception while querying BlockedNumberContract",
+ e);
+ return false;
+ }
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumberProvider.java b/java/com/android/dialer/blocking/FilteredNumberProvider.java
new file mode 100644
index 000000000..5d369038c
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumberProvider.java
@@ -0,0 +1,176 @@
+/*
+ * 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.blocking;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.database.Database;
+import com.android.dialer.database.DialerDatabaseHelper;
+import com.android.dialer.database.FilteredNumberContract;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+
+/** Filtered number content provider. */
+public class FilteredNumberProvider extends ContentProvider {
+
+ private static final int FILTERED_NUMBERS_TABLE = 1;
+ private static final int FILTERED_NUMBERS_TABLE_ID = 2;
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+ private static final String TAG = FilteredNumberProvider.class.getSimpleName();
+ private DialerDatabaseHelper mDialerDatabaseHelper;
+
+ @Override
+ public boolean onCreate() {
+ mDialerDatabaseHelper = Database.get(getContext()).getDatabaseHelper(getContext());
+ if (mDialerDatabaseHelper == null) {
+ return false;
+ }
+ sUriMatcher.addURI(
+ FilteredNumberContract.AUTHORITY,
+ FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE,
+ FILTERED_NUMBERS_TABLE);
+ sUriMatcher.addURI(
+ FilteredNumberContract.AUTHORITY,
+ FilteredNumberContract.FilteredNumber.FILTERED_NUMBERS_TABLE + "/#",
+ FILTERED_NUMBERS_TABLE_ID);
+ return true;
+ }
+
+ @Override
+ public Cursor query(
+ Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
+ final SQLiteDatabase db = mDialerDatabaseHelper.getReadableDatabase();
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setTables(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE);
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case FILTERED_NUMBERS_TABLE:
+ break;
+ case FILTERED_NUMBERS_TABLE_ID:
+ qb.appendWhere(FilteredNumberColumns._ID + "=" + ContentUris.parseId(uri));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ final Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, null);
+ if (c != null) {
+ c.setNotificationUri(
+ getContext().getContentResolver(), FilteredNumberContract.FilteredNumber.CONTENT_URI);
+ } else {
+ Log.d(TAG, "CURSOR WAS NULL");
+ }
+ return c;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return FilteredNumberContract.FilteredNumber.CONTENT_ITEM_TYPE;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+ setDefaultValues(values);
+ long id = db.insert(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, null, values);
+ if (id < 0) {
+ return null;
+ }
+ notifyChange(uri);
+ return ContentUris.withAppendedId(uri, id);
+ }
+
+ @VisibleForTesting
+ protected long getCurrentTimeMs() {
+ return System.currentTimeMillis();
+ }
+
+ private void setDefaultValues(ContentValues values) {
+ if (values.getAsString(FilteredNumberColumns.COUNTRY_ISO) == null) {
+ values.put(FilteredNumberColumns.COUNTRY_ISO, GeoUtil.getCurrentCountryIso(getContext()));
+ }
+ if (values.getAsInteger(FilteredNumberColumns.TIMES_FILTERED) == null) {
+ values.put(FilteredNumberContract.FilteredNumberColumns.TIMES_FILTERED, 0);
+ }
+ if (values.getAsLong(FilteredNumberColumns.CREATION_TIME) == null) {
+ values.put(FilteredNumberColumns.CREATION_TIME, getCurrentTimeMs());
+ }
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case FILTERED_NUMBERS_TABLE:
+ break;
+ case FILTERED_NUMBERS_TABLE_ID:
+ selection = getSelectionWithId(selection, ContentUris.parseId(uri));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ int rows =
+ db.delete(DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, selection, selectionArgs);
+ if (rows > 0) {
+ notifyChange(uri);
+ }
+ return rows;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ SQLiteDatabase db = mDialerDatabaseHelper.getWritableDatabase();
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case FILTERED_NUMBERS_TABLE:
+ break;
+ case FILTERED_NUMBERS_TABLE_ID:
+ selection = getSelectionWithId(selection, ContentUris.parseId(uri));
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown uri: " + uri);
+ }
+ int rows =
+ db.update(
+ DialerDatabaseHelper.Tables.FILTERED_NUMBER_TABLE, values, selection, selectionArgs);
+ if (rows > 0) {
+ notifyChange(uri);
+ }
+ return rows;
+ }
+
+ private String getSelectionWithId(String selection, long id) {
+ if (TextUtils.isEmpty(selection)) {
+ return FilteredNumberContract.FilteredNumberColumns._ID + "=" + id;
+ } else {
+ return selection + "AND " + FilteredNumberContract.FilteredNumberColumns._ID + "=" + id;
+ }
+ }
+
+ private void notifyChange(Uri uri) {
+ getContext().getContentResolver().notifyChange(uri, null);
+ }
+}
diff --git a/java/com/android/dialer/blocking/FilteredNumbersUtil.java b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
new file mode 100644
index 000000000..61ecf1886
--- /dev/null
+++ b/java/com/android/dialer/blocking/FilteredNumbersUtil.java
@@ -0,0 +1,380 @@
+/*
+ * 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.blocking;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.Settings;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.concurrent.TimeUnit;
+
+/** Utility to help with tasks related to filtered numbers. */
+public class FilteredNumbersUtil {
+
+ public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking";
+ public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10;
+ // Pref key for storing the time of end of the last emergency call in milliseconds after epoch.
+ protected static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms";
+ // Pref key for storing whether a notification has been dispatched to notify the user that call
+ // blocking has been disabled because of a recent emergency call.
+ protected static final String NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY =
+ "notified_call_blocking_disabled_by_emergency_call";
+ // Disable incoming call blocking if there was a call within the past 2 days.
+ private static final long RECENT_EMERGENCY_CALL_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 2;
+
+ /**
+ * Used for testing to specify the custom threshold value, in milliseconds for whether an
+ * emergency call is "recent". The default value will be used if this custom threshold is less
+ * than zero. For example, to set this threshold to 60 seconds:
+ *
+ * <p>adb shell settings put system dialer_emergency_call_threshold_ms 60000
+ */
+ private static final String RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY =
+ "dialer_emergency_call_threshold_ms";
+
+ /** Checks if there exists a contact with {@code Contacts.SEND_TO_VOICEMAIL} set to true. */
+ public static void checkForSendToVoicemailContact(
+ final Context context, final CheckForSendToVoicemailContactListener listener) {
+ final AsyncTask task =
+ new AsyncTask<Object, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Object... params) {
+ if (context == null || !PermissionsUtil.hasContactsPermissions(context)) {
+ return false;
+ }
+
+ final Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ Contacts.CONTENT_URI,
+ ContactsQuery.PROJECTION,
+ ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null,
+ null);
+
+ boolean hasSendToVoicemailContacts = false;
+ if (cursor != null) {
+ try {
+ hasSendToVoicemailContacts = cursor.getCount() > 0;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return hasSendToVoicemailContacts;
+ }
+
+ @Override
+ public void onPostExecute(Boolean hasSendToVoicemailContact) {
+ if (listener != null) {
+ listener.onComplete(hasSendToVoicemailContact);
+ }
+ }
+ };
+ task.execute();
+ }
+
+ /**
+ * Blocks all the phone numbers of any contacts marked as SEND_TO_VOICEMAIL, then clears the
+ * SEND_TO_VOICEMAIL flag on those contacts.
+ */
+ public static void importSendToVoicemailContacts(
+ final Context context, final ImportSendToVoicemailContactsListener listener) {
+ Logger.get(context).logInteraction(InteractionEvent.Type.IMPORT_SEND_TO_VOICEMAIL);
+ final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler =
+ new FilteredNumberAsyncQueryHandler(context);
+
+ final AsyncTask<Object, Void, Boolean> task =
+ new AsyncTask<Object, Void, Boolean>() {
+ @Override
+ public Boolean doInBackground(Object... params) {
+ if (context == null) {
+ return false;
+ }
+
+ // Get the phone number of contacts marked as SEND_TO_VOICEMAIL.
+ final Cursor phoneCursor =
+ context
+ .getContentResolver()
+ .query(
+ Phone.CONTENT_URI,
+ PhoneQuery.PROJECTION,
+ PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null,
+ null);
+
+ if (phoneCursor == null) {
+ return false;
+ }
+
+ try {
+ while (phoneCursor.moveToNext()) {
+ final String normalizedNumber =
+ phoneCursor.getString(PhoneQuery.NORMALIZED_NUMBER_COLUMN_INDEX);
+ final String number = phoneCursor.getString(PhoneQuery.NUMBER_COLUMN_INDEX);
+ if (normalizedNumber != null) {
+ // Block the phone number of the contact.
+ mFilteredNumberAsyncQueryHandler.blockNumber(
+ null, normalizedNumber, number, null);
+ }
+ }
+ } finally {
+ phoneCursor.close();
+ }
+
+ // Clear SEND_TO_VOICEMAIL on all contacts. The setting has been imported to Dialer.
+ ContentValues newValues = new ContentValues();
+ newValues.put(Contacts.SEND_TO_VOICEMAIL, 0);
+ context
+ .getContentResolver()
+ .update(
+ Contacts.CONTENT_URI,
+ newValues,
+ ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
+ null);
+
+ return true;
+ }
+
+ @Override
+ public void onPostExecute(Boolean success) {
+ if (success) {
+ if (listener != null) {
+ listener.onImportComplete();
+ }
+ } else if (context != null) {
+ String toastStr = context.getString(R.string.send_to_voicemail_import_failed);
+ Toast.makeText(context, toastStr, Toast.LENGTH_SHORT).show();
+ }
+ }
+ };
+ task.execute();
+ }
+
+ /**
+ * WARNING: This method should NOT be executed on the UI thread. Use {@code
+ * FilteredNumberAsyncQueryHandler} to asynchronously check if a number is blocked.
+ */
+ public static boolean shouldBlockVoicemail(
+ Context context, String number, String countryIso, long voicemailDateMs) {
+ final String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ if (TextUtils.isEmpty(normalizedNumber)) {
+ return false;
+ }
+
+ if (hasRecentEmergencyCall(context)) {
+ return false;
+ }
+
+ final Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ FilteredNumber.CONTENT_URI,
+ new String[] {FilteredNumberColumns.CREATION_TIME},
+ FilteredNumberColumns.NORMALIZED_NUMBER + "=?",
+ new String[] {normalizedNumber},
+ null);
+ if (cursor == null) {
+ return false;
+ }
+ try {
+ /*
+ * Block if number is found and it was added before this voicemail was received.
+ * The VVM's date is reported with precision to the minute, even though its
+ * magnitude is in milliseconds, so we perform the comparison in minutes.
+ */
+ return cursor.moveToFirst()
+ && TimeUnit.MINUTES.convert(voicemailDateMs, TimeUnit.MILLISECONDS)
+ >= TimeUnit.MINUTES.convert(cursor.getLong(0), TimeUnit.MILLISECONDS);
+ } finally {
+ cursor.close();
+ }
+ }
+
+ public static long getLastEmergencyCallTimeMillis(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context)
+ .getLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, 0);
+ }
+
+ public static boolean hasRecentEmergencyCall(Context context) {
+ if (context == null) {
+ return false;
+ }
+
+ Long lastEmergencyCallTime = getLastEmergencyCallTimeMillis(context);
+ if (lastEmergencyCallTime == 0) {
+ return false;
+ }
+
+ return (System.currentTimeMillis() - lastEmergencyCallTime)
+ < getRecentEmergencyCallThresholdMs(context);
+ }
+
+ public static void recordLastEmergencyCallTime(Context context) {
+ if (context == null) {
+ return;
+ }
+
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, System.currentTimeMillis())
+ .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)
+ .apply();
+
+ maybeNotifyCallBlockingDisabled(context);
+ }
+
+ public static void maybeNotifyCallBlockingDisabled(final Context context) {
+ // The Dialer is not responsible for this notification after migrating
+ if (FilteredNumberCompat.useNewFiltering(context)) {
+ return;
+ }
+ // Skip if the user has already received a notification for the most recent emergency call.
+ if (PreferenceManager.getDefaultSharedPreferences(context)
+ .getBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)) {
+ return;
+ }
+
+ // If the user has blocked numbers, notify that call blocking is temporarily disabled.
+ FilteredNumberAsyncQueryHandler queryHandler = new FilteredNumberAsyncQueryHandler(context);
+ queryHandler.hasBlockedNumbers(
+ new OnHasBlockedNumbersListener() {
+ @Override
+ public void onHasBlockedNumbers(boolean hasBlockedNumbers) {
+ if (context == null || !hasBlockedNumbers) {
+ return;
+ }
+
+ NotificationManager notificationManager =
+ (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ Notification.Builder builder =
+ new Notification.Builder(context)
+ .setSmallIcon(R.drawable.ic_block_24dp)
+ .setContentTitle(
+ context.getString(R.string.call_blocking_disabled_notification_title))
+ .setContentText(
+ context.getString(R.string.call_blocking_disabled_notification_text))
+ .setAutoCancel(true);
+
+ builder.setContentIntent(
+ PendingIntent.getActivity(
+ context,
+ 0,
+ FilteredNumberCompat.createManageBlockedNumbersIntent(context),
+ PendingIntent.FLAG_UPDATE_CURRENT));
+
+ notificationManager.notify(
+ CALL_BLOCKING_NOTIFICATION_TAG,
+ CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID,
+ builder.build());
+
+ // Record that the user has been notified for this emergency call.
+ PreferenceManager.getDefaultSharedPreferences(context)
+ .edit()
+ .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, true)
+ .apply();
+ }
+ });
+ }
+
+ /**
+ * @param e164Number The e164 formatted version of the number, or {@code null} if such a format
+ * doesn't exist.
+ * @param number The number to attempt blocking.
+ * @return {@code true} if the number can be blocked, {@code false} otherwise.
+ */
+ public static boolean canBlockNumber(Context context, String e164Number, String number) {
+ String blockableNumber = getBlockableNumber(context, e164Number, number);
+ return !TextUtils.isEmpty(blockableNumber)
+ && !PhoneNumberUtils.isEmergencyNumber(blockableNumber);
+ }
+
+ /**
+ * @param e164Number The e164 formatted version of the number, or {@code null} if such a format
+ * doesn't exist..
+ * @param number The number to attempt blocking.
+ * @return The version of the given number that can be blocked with the current blocking solution.
+ */
+ @Nullable
+ public static String getBlockableNumber(
+ Context context, @Nullable String e164Number, String number) {
+ if (!FilteredNumberCompat.useNewFiltering(context)) {
+ return e164Number;
+ }
+ return TextUtils.isEmpty(e164Number) ? number : e164Number;
+ }
+
+ private static long getRecentEmergencyCallThresholdMs(Context context) {
+ if (LogUtil.isVerboseEnabled()) {
+ long thresholdMs =
+ Settings.System.getLong(
+ context.getContentResolver(), RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY, 0);
+ return thresholdMs > 0 ? thresholdMs : RECENT_EMERGENCY_CALL_THRESHOLD_MS;
+ } else {
+ return RECENT_EMERGENCY_CALL_THRESHOLD_MS;
+ }
+ }
+
+ public interface CheckForSendToVoicemailContactListener {
+
+ void onComplete(boolean hasSendToVoicemailContact);
+ }
+
+ public interface ImportSendToVoicemailContactsListener {
+
+ void onImportComplete();
+ }
+
+ private static class ContactsQuery {
+
+ static final String[] PROJECTION = {Contacts._ID};
+
+ static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
+
+ static final int ID_COLUMN_INDEX = 0;
+ }
+
+ public static class PhoneQuery {
+
+ public static final String[] PROJECTION = {Contacts._ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER};
+
+ public static final int ID_COLUMN_INDEX = 0;
+ public static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1;
+ public static final int NUMBER_COLUMN_INDEX = 2;
+
+ public static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
+ }
+}
diff --git a/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
new file mode 100644
index 000000000..76e50b38e
--- /dev/null
+++ b/java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java
@@ -0,0 +1,113 @@
+/*
+ * 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.blocking;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnShowListener;
+import android.os.Bundle;
+import android.view.View;
+import com.android.dialer.blocking.BlockedNumbersMigrator.Listener;
+import java.util.Objects;
+
+/**
+ * Dialog fragment shown to users when they need to migrate to use {@link
+ * android.provider.BlockedNumberContract} for blocking.
+ */
+public class MigrateBlockedNumbersDialogFragment extends DialogFragment {
+
+ private BlockedNumbersMigrator mBlockedNumbersMigrator;
+ private BlockedNumbersMigrator.Listener mMigrationListener;
+
+ /**
+ * Creates a new MigrateBlockedNumbersDialogFragment.
+ *
+ * @param blockedNumbersMigrator The {@link BlockedNumbersMigrator} which will be used to migrate
+ * the numbers.
+ * @param migrationListener The {@link BlockedNumbersMigrator.Listener} to call when the migration
+ * is complete.
+ * @return The new MigrateBlockedNumbersDialogFragment.
+ * @throws NullPointerException if blockedNumbersMigrator or migrationListener are {@code null}.
+ */
+ public static DialogFragment newInstance(
+ BlockedNumbersMigrator blockedNumbersMigrator,
+ BlockedNumbersMigrator.Listener migrationListener) {
+ MigrateBlockedNumbersDialogFragment fragment = new MigrateBlockedNumbersDialogFragment();
+ fragment.mBlockedNumbersMigrator = Objects.requireNonNull(blockedNumbersMigrator);
+ fragment.mMigrationListener = Objects.requireNonNull(migrationListener);
+ return fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ AlertDialog dialog =
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.migrate_blocked_numbers_dialog_title)
+ .setMessage(R.string.migrate_blocked_numbers_dialog_message)
+ .setPositiveButton(R.string.migrate_blocked_numbers_dialog_allow_button, null)
+ .setNegativeButton(R.string.migrate_blocked_numbers_dialog_cancel_button, null)
+ .create();
+ // The Dialog's buttons aren't available until show is called, so an OnShowListener
+ // is used to set the positive button callback.
+ dialog.setOnShowListener(
+ new OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialog) {
+ final AlertDialog alertDialog = (AlertDialog) dialog;
+ alertDialog
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .setOnClickListener(newPositiveButtonOnClickListener(alertDialog));
+ }
+ });
+ return dialog;
+ }
+
+ /*
+ * Creates a new View.OnClickListener to be used as the positive button in this dialog. The
+ * OnClickListener will grey out the dialog's positive and negative buttons while the migration
+ * is underway, and close the dialog once the migrate is complete.
+ */
+ private View.OnClickListener newPositiveButtonOnClickListener(final AlertDialog alertDialog) {
+ return new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setEnabled(false);
+ mBlockedNumbersMigrator.migrate(
+ new Listener() {
+ @Override
+ public void onComplete() {
+ alertDialog.dismiss();
+ mMigrationListener.onComplete();
+ }
+ });
+ }
+ };
+ }
+
+ @Override
+ public void onPause() {
+ // The dialog is dismissed and state is cleaned up onPause, i.e. rotation.
+ dismiss();
+ mBlockedNumbersMigrator = null;
+ mMigrationListener = null;
+ super.onPause();
+ }
+}
diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png
new file mode 100644
index 000000000..2ccc89d24
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png
new file mode 100644
index 000000000..dc0c995c1
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png
new file mode 100644
index 000000000..919a872e0
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png
new file mode 100644
index 000000000..ec1b33f0e
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png
new file mode 100644
index 000000000..70b82d6c1
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png
new file mode 100644
index 000000000..dc0c995c1
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png
new file mode 100644
index 000000000..7aba97b65
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png
new file mode 100644
index 000000000..18e7764ab
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png
new file mode 100644
index 000000000..aed766804
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png
new file mode 100644
index 000000000..fddfa54b8
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png
new file mode 100644
index 000000000..aed766804
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png
new file mode 100644
index 000000000..f7cfacbd4
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png
new file mode 100644
index 000000000..0378d1bed
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png
new file mode 100644
index 000000000..855e59015
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png
new file mode 100644
index 000000000..7ef0d7afc
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png
Binary files differ
diff --git a/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml b/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml
new file mode 100644
index 000000000..09d7989e8
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/drawable/blocked_contact.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/blocked_contact_background"/>
+ <size
+ android:height="24dp"
+ android:width="24dp"/>
+ </shape>
+ </item>
+
+ <item
+ android:drawable="@drawable/ic_report_24dp"
+ android:gravity="center"
+ android:height="18dp"
+ android:width="18dp"/>
+
+</layer-list>
diff --git a/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml b/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml
new file mode 100644
index 000000000..82e8d80b3
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="25dp"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/block_details"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="10dp"
+ android:text="@string/block_report_number_alert_details"
+ android:textColor="@color/block_report_spam_primary_text_color"
+ android:textSize="@dimen/blocked_report_spam_primary_text_size"/>
+
+ <CheckBox
+ android:id="@+id/report_number_as_spam_action"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/checkbox_report_as_spam_action"
+ android:textSize="@dimen/blocked_report_spam_primary_text_size"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/blocking/res/values/colors.xml b/java/com/android/dialer/blocking/res/values/colors.xml
new file mode 100644
index 000000000..d1a567d9e
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/values/colors.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+-->
+<resources>
+
+ <!-- 87% black -->
+ <color name="block_report_spam_primary_text_color">#de000000</color>
+
+ <!-- Note, this is also used by InCallUi. -->
+ <color name="blocked_contact_background">#A52714</color>
+
+</resources>
diff --git a/java/com/android/dialer/blocking/res/values/dimens.xml b/java/com/android/dialer/blocking/res/values/dimens.xml
new file mode 100644
index 000000000..cd7cfe2fd
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/values/dimens.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+-->
+<resources>
+ <dimen name="blocked_report_spam_primary_text_size">16sp</dimen>
+</resources>
diff --git a/java/com/android/dialer/blocking/res/values/strings.xml b/java/com/android/dialer/blocking/res/values/strings.xml
new file mode 100644
index 000000000..8abff4561
--- /dev/null
+++ b/java/com/android/dialer/blocking/res/values/strings.xml
@@ -0,0 +1,122 @@
+<!--
+ ~ 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
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Title for dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=30]-->
+ <string name="migrate_blocked_numbers_dialog_title">New, simplified blocking</string>
+
+ <!-- Body text for dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=NONE]-->
+ <string name="migrate_blocked_numbers_dialog_message">To better protect you, Phone needs to change how blocking works. Your blocked numbers will now stop both calls and texts and may be shared with other apps.</string>
+
+ <!-- Positive confirmation button for the dialog which opens when the user needs to migrate to the framework blocking implementation [CHAR LIMIT=NONE]-->
+ <string name="migrate_blocked_numbers_dialog_allow_button">Allow</string>
+
+ <!-- Do not translate -->
+ <string name="migrate_blocked_numbers_dialog_cancel_button">@android:string/cancel</string>
+
+ <!-- Confirmation dialog title for blocking a number. [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_title">Block
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>?</string>
+
+ <!-- Confirmation dialog message for blocking a number with visual voicemail active.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_message_vvm">
+ Calls from this number will be blocked and voicemails will be automatically deleted.
+ </string>
+
+ <!-- Confirmation dialog message for blocking a number with no visual voicemail.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_message_no_vvm">
+ Calls from this number will be blocked, but the caller may still be able to leave you voicemails.
+ </string>
+
+ <!-- Confirmation dialog message for blocking a number with new filtering enabled.
+ [CHAR LIMIT=NONE] -->
+ <string name="block_number_confirmation_message_new_filtering">
+ You will no longer receive calls or texts from this number.
+ </string>
+
+ <!-- Block number alert dialog button [CHAR LIMIT=32] -->
+ <string name="block_number_ok">BLOCK</string>
+
+ <!-- Confirmation dialog for unblocking a number. [CHAR LIMIT=NONE] -->
+ <string name="unblock_number_confirmation_title">Unblock
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>?</string>
+
+ <!-- Unblock number alert dialog button [CHAR LIMIT=32] -->
+ <string name="unblock_number_ok">UNBLOCK</string>
+
+ <!-- Error message shown when user tries to add invalid number to the block list.
+ [CHAR LIMIT=64] -->
+ <string name="invalidNumber"><xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>
+ is invalid.</string>
+
+ <!-- Text for snackbar to undo blocking a number. [CHAR LIMIT=64] -->
+ <string name="snackbar_number_blocked">
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g> blocked</string>
+
+ <!-- Text for snackbar to undo unblocking a number. [CHAR LIMIT=64] -->
+ <string name="snackbar_number_unblocked">
+ <xliff:g example="(555) 555-5555" id="number">%1$s</xliff:g>
+ unblocked</string>
+
+ <!-- Text for undo button in snackbar for blocking/unblocking number. [CHAR LIMIT=10] -->
+ <string name="block_number_undo">UNDO</string>
+
+ <!-- Error toast message for when send to voicemail import fails. [CHAR LIMIT=40] -->
+ <string name="send_to_voicemail_import_failed">Import failed</string>
+
+ <!-- Title of notification telling the user that call blocking has been temporarily disabled.
+ [CHAR LIMIT=56] -->
+ <string name="call_blocking_disabled_notification_title">
+ Call blocking disabled for 48 hours
+ </string>
+
+ <!-- Text for notification which provides the reason that call blocking has been temporarily
+ disabled. Namely, we disable call blocking after an emergency call in case of return
+ phone calls made by emergency services. [CHAR LIMIT=64] -->
+ <string name="call_blocking_disabled_notification_text">
+ Disabled because an emergency call was made.
+ </string>
+
+ <!-- Title of alert dialog after clicking on Block/report as spam. [CHAR LIMIT=100] -->
+ <string name="block_report_number_alert_title">Block <xliff:g id="number">%1$s</xliff:g>?</string>
+
+ <!-- Text in alert dialog after clicking on Block/report as spam. [CHAR LIMIT=100] -->
+ <string name="block_report_number_alert_details">You will no longer receive calls from this number.</string>
+
+ <!-- Text in alert dialog after clicking on Block. [CHAR LIMIT=100] -->
+ <string name="block_number_alert_details"><xliff:g id="text">%1$s</xliff:g> This call will be reported as spam.</string>
+
+ <!-- Text in alert dialog after clicking on Unblock. [CHAR LIMIT=100] -->
+ <string name="unblock_number_alert_details">This number will be unblocked and reported as not spam. Future calls won\'t be identified as spam.</string>
+
+ <!-- Title of alert dialog after clicking on Unblock. [CHAR LIMIT=100] -->
+ <string name="unblock_report_number_alert_title">Unblock <xliff:g id="number">%1$s</xliff:g>?</string>
+
+ <!-- Report not spam number alert dialog button [CHAR LIMIT=32] -->
+ <string name="report_not_spam_alert_button">Report</string>
+
+ <!-- Title of alert dialog after clicking on Report as not spam. [CHAR LIMIT=100] -->
+ <string name="report_not_spam_alert_title">Report a mistake?</string>
+
+ <!-- Text in alert dialog after clicking on Report as not spam. [CHAR LIMIT=100] -->
+ <string name="report_not_spam_alert_details">Future calls from <xliff:g id="number">%1$s</xliff:g> will no longer be identified as spam.</string>
+
+ <!-- Label for checkbox in the Alert dialog to allow the user to report the number as spam as well. [CHAR LIMIT=30] -->
+ <string name="checkbox_report_as_spam_action">Report call as spam</string>
+
+</resources>
diff --git a/java/com/android/dialer/buildtype/BuildType.java b/java/com/android/dialer/buildtype/BuildType.java
new file mode 100644
index 000000000..c0a54a519
--- /dev/null
+++ b/java/com/android/dialer/buildtype/BuildType.java
@@ -0,0 +1,62 @@
+/*
+ * 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.buildtype;
+
+import android.support.annotation.IntDef;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Utility to find out which build type the app is running as. */
+public class BuildType {
+
+ /** The type of build. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ BUGFOOD, FISHFOOD, DOGFOOD, RELEASE,
+ })
+ public @interface Type {}
+
+ public static final int BUGFOOD = 1;
+ public static final int FISHFOOD = 2;
+ public static final int DOGFOOD = 3;
+ public static final int RELEASE = 4;
+
+ private static int cachedBuildType;
+ private static boolean didInitializeBuildType;
+
+ @Type
+ public static synchronized int get() {
+ if (!didInitializeBuildType) {
+ didInitializeBuildType = true;
+ try {
+ Class<?> clazz = Class.forName(BuildTypeAccessor.class.getName() + "Impl");
+ BuildTypeAccessor accessorImpl = (BuildTypeAccessor) clazz.getConstructor().newInstance();
+ cachedBuildType = accessorImpl.getBuildType();
+ } catch (ReflectiveOperationException e) {
+ LogUtil.e("BuildType.get", "error creating BuildTypeAccessorImpl", e);
+ Assert.fail(
+ "Unable to get build type. To fix this error include one of the build type "
+ + "modules (bugfood, etc...) in your target.");
+ }
+ }
+ return cachedBuildType;
+ }
+
+ private BuildType() {}
+}
diff --git a/java/com/android/dialer/buildtype/BuildTypeAccessor.java b/java/com/android/dialer/buildtype/BuildTypeAccessor.java
new file mode 100644
index 000000000..940cf817c
--- /dev/null
+++ b/java/com/android/dialer/buildtype/BuildTypeAccessor.java
@@ -0,0 +1,31 @@
+/*
+ * 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.buildtype;
+
+import com.android.dialer.proguard.UsedByReflection;
+
+/**
+ * Gets the build type. The functionality depends on a an implementation being present in the app
+ * that has the same package and the class name ending in "Impl". For example,
+ * com.android.dialer.buildtype.BuildTypeAccessorImpl. This class is found by the module using
+ * reflection.
+ */
+@UsedByReflection(value = "BuildType.java")
+/* package */ interface BuildTypeAccessor {
+ @BuildType.Type
+ int getBuildType();
+}
diff --git a/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java b/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
new file mode 100644
index 000000000..e1f2cdc79
--- /dev/null
+++ b/java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java
@@ -0,0 +1,30 @@
+/*
+ * 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.buildtype;
+
+import com.android.dialer.proguard.UsedByReflection;
+
+/** Gets the build type. */
+@UsedByReflection(value = "BuildType.java")
+public class BuildTypeAccessorImpl implements BuildTypeAccessor {
+
+ @Override
+ @BuildType.Type
+ public int getBuildType() {
+ return BuildType.DOGFOOD;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/AndroidManifest.xml b/java/com/android/dialer/callcomposer/AndroidManifest.xml
new file mode 100644
index 000000000..c99f22b90
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/AndroidManifest.xml
@@ -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
+ -->
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.callcomposer">
+
+ <application>
+ <activity
+ android:name="com.android.dialer.callcomposer.CallComposerActivity"
+ android:exported="false"
+ android:theme="@style/Theme.AppCompat.CallComposer"
+ android:windowSoftInputMode="adjustResize"
+ android:screenOrientation="portrait"/>
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/callcomposer/CallComposerActivity.java b/java/com/android/dialer/callcomposer/CallComposerActivity.java
new file mode 100644
index 000000000..eef3d210d
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CallComposerActivity.java
@@ -0,0 +1,728 @@
+/*
+ * 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.callcomposer;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ArgbEvaluator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.FileProvider;
+import android.support.v4.view.ViewPager;
+import android.support.v4.view.ViewPager.OnPageChangeListener;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLayoutChangeListener;
+import android.view.ViewAnimationUtils;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toolbar;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.callcomposer.CallComposerFragment.CallComposerListener;
+import com.android.dialer.callcomposer.nano.CallComposerContact;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask.Callback;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.UiUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.State;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.enrichedcall.extensions.StateExtension;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.protos.ProtoParsers;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.ViewUtil;
+import java.io.File;
+
+/**
+ * Implements an activity which prompts for a call with additional media for an outgoing call. The
+ * activity includes a pop up with:
+ *
+ * <ul>
+ * <li>Contact galleryIcon
+ * <li>Name
+ * <li>Number
+ * <li>Media options to attach a gallery image, camera image or a message
+ * </ul>
+ */
+public class CallComposerActivity extends AppCompatActivity
+ implements OnClickListener,
+ OnPageChangeListener,
+ CallComposerListener,
+ OnLayoutChangeListener,
+ AnimatorListener,
+ EnrichedCallManager.StateChangedListener {
+
+ private static final int VIEW_PAGER_ANIMATION_DURATION_MILLIS = 300;
+ private static final int ENTRANCE_ANIMATION_DURATION_MILLIS = 500;
+
+ private static final String ARG_CALL_COMPOSER_CONTACT = "CALL_COMPOSER_CONTACT";
+
+ private static final String ENTRANCE_ANIMATION_KEY = "entrance_animation_key";
+ private static final String CURRENT_INDEX_KEY = "current_index_key";
+ private static final String VIEW_PAGER_STATE_KEY = "view_pager_state_key";
+ private static final String LOCATIONS_KEY = "locations_key";
+ private static final String SESSION_ID_KEY = "session_id_key";
+
+ private CallComposerContact contact;
+ private Long sessionId = Session.NO_SESSION_ID;
+
+ private TextView nameView;
+ private TextView numberView;
+ private QuickContactBadge contactPhoto;
+ private RelativeLayout contactContainer;
+ private Toolbar toolbar;
+ private View sendAndCall;
+
+ private ImageView cameraIcon;
+ private ImageView galleryIcon;
+ private ImageView messageIcon;
+ private ViewPager pager;
+ private CallComposerPagerAdapter adapter;
+
+ private FrameLayout background;
+ private LinearLayout windowContainer;
+
+ private FastOutSlowInInterpolator interpolator;
+ private boolean shouldAnimateEntrance = true;
+ private boolean inFullscreenMode;
+ private boolean isSendAndCallHidingOrHidden = true;
+ private boolean isAnimatingContactBar;
+ private boolean layoutChanged;
+ private int currentIndex;
+ private int[] locations;
+ private int currentLocation;
+
+ @NonNull private EnrichedCallManager enrichedCallManager;
+
+ public static Intent newIntent(Context context, CallComposerContact contact) {
+ Intent intent = new Intent(context, CallComposerActivity.class);
+ ProtoParsers.put(intent, ARG_CALL_COMPOSER_CONTACT, contact);
+ return intent;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.call_composer_activity);
+
+ nameView = (TextView) findViewById(R.id.contact_name);
+ numberView = (TextView) findViewById(R.id.phone_number);
+ contactPhoto = (QuickContactBadge) findViewById(R.id.contact_photo);
+ cameraIcon = (ImageView) findViewById(R.id.call_composer_camera);
+ galleryIcon = (ImageView) findViewById(R.id.call_composer_photo);
+ messageIcon = (ImageView) findViewById(R.id.call_composer_message);
+ contactContainer = (RelativeLayout) findViewById(R.id.contact_bar);
+ pager = (ViewPager) findViewById(R.id.call_composer_view_pager);
+ background = (FrameLayout) findViewById(R.id.background);
+ windowContainer = (LinearLayout) findViewById(R.id.call_composer_container);
+ toolbar = (Toolbar) findViewById(R.id.toolbar);
+ sendAndCall = findViewById(R.id.send_and_call_button);
+
+ interpolator = new FastOutSlowInInterpolator();
+ adapter =
+ new CallComposerPagerAdapter(
+ getSupportFragmentManager(),
+ getResources().getInteger(R.integer.call_composer_message_limit));
+ pager.setAdapter(adapter);
+ pager.addOnPageChangeListener(this);
+
+ setActionBar(toolbar);
+ toolbar.setNavigationIcon(R.drawable.quantum_ic_arrow_back_white_24);
+ toolbar.setNavigationOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ background.addOnLayoutChangeListener(this);
+ cameraIcon.setOnClickListener(this);
+ galleryIcon.setOnClickListener(this);
+ messageIcon.setOnClickListener(this);
+ sendAndCall.setOnClickListener(this);
+
+ onHandleIntent(getIntent());
+
+ enrichedCallManager = EnrichedCallManager.Accessor.getInstance(getApplication());
+ if (savedInstanceState != null) {
+ shouldAnimateEntrance = savedInstanceState.getBoolean(ENTRANCE_ANIMATION_KEY);
+ locations = savedInstanceState.getIntArray(LOCATIONS_KEY);
+ pager.onRestoreInstanceState(savedInstanceState.getParcelable(VIEW_PAGER_STATE_KEY));
+ currentIndex = savedInstanceState.getInt(CURRENT_INDEX_KEY);
+ sessionId = savedInstanceState.getLong(SESSION_ID_KEY);
+ onPageSelected(currentIndex);
+ } else {
+ locations = new int[adapter.getCount()];
+ for (int i = 0; i < locations.length; i++) {
+ locations[i] = CallComposerFragment.CONTENT_TOP_UNSET;
+ }
+ sessionId = enrichedCallManager.startCallComposerSession(contact.number);
+ }
+
+ // Since we can't animate the views until they are ready to be drawn, we use this listener to
+ // track that and animate the call compose UI as soon as it's ready.
+ ViewUtil.doOnPreDraw(
+ windowContainer,
+ false,
+ new Runnable() {
+ @Override
+ public void run() {
+ runEntranceAnimation();
+ }
+ });
+
+ setMediaIconSelected(0);
+
+ // This activity is started using startActivityForResult. By default, mark this as succeeded
+ // and flip this to RESULT_CANCELED if something goes wrong.
+ setResult(RESULT_OK);
+
+ if (sessionId == Session.NO_SESSION_ID) {
+ LogUtil.w("CallComposerActivity.onCreate", "failed to create call composer session");
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ enrichedCallManager.registerStateChangedListener(this);
+ refreshUiForCallComposerState();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ enrichedCallManager.unregisterStateChangedListener(this);
+ }
+
+ @Override
+ public void onEnrichedCallStateChanged() {
+ refreshUiForCallComposerState();
+ }
+
+ private void refreshUiForCallComposerState() {
+ Session session = enrichedCallManager.getSession(sessionId);
+ if (session == null) {
+ return;
+ }
+
+ @State int state = session.getState();
+ LogUtil.i(
+ "CallComposerActivity.refreshUiForCallComposerState",
+ "state: %s",
+ StateExtension.toString(state));
+
+ if (state == EnrichedCallManager.STATE_START_FAILED
+ || state == EnrichedCallManager.STATE_CLOSED) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ onHandleIntent(intent);
+ }
+
+ @Override
+ public void onClick(View view) {
+ LogUtil.enterBlock("CallComposerActivity.onClick");
+ if (view == cameraIcon) {
+ pager.setCurrentItem(CallComposerPagerAdapter.INDEX_CAMERA, true /* animate */);
+ } else if (view == galleryIcon) {
+ pager.setCurrentItem(CallComposerPagerAdapter.INDEX_GALLERY, true /* animate */);
+ } else if (view == messageIcon) {
+ pager.setCurrentItem(CallComposerPagerAdapter.INDEX_MESSAGE, true /* animate */);
+ } else if (view == sendAndCall) {
+ if (!sessionReady()) {
+ LogUtil.i(
+ "CallComposerActivity.onClick", "sendAndCall pressed, but the session isn't ready");
+ Logger.get(this)
+ .logImpression(
+ DialerImpression.Type
+ .CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY);
+ return;
+ }
+ sendAndCall.setEnabled(false);
+ CallComposerFragment fragment =
+ (CallComposerFragment) adapter.instantiateItem(pager, currentIndex);
+ MultimediaData.Builder builder = MultimediaData.builder();
+
+ if (fragment instanceof MessageComposerFragment) {
+ MessageComposerFragment messageComposerFragment = (MessageComposerFragment) fragment;
+ builder.setSubject(messageComposerFragment.getMessage());
+ placeRCSCall(builder);
+ }
+ if (fragment instanceof GalleryComposerFragment) {
+ GalleryComposerFragment galleryComposerFragment = (GalleryComposerFragment) fragment;
+ // If the current data is not a copy, make one.
+ if (!galleryComposerFragment.selectedDataIsCopy()) {
+ new CopyAndResizeImageTask(
+ CallComposerActivity.this,
+ galleryComposerFragment.getGalleryData().getFileUri(),
+ new Callback() {
+ @Override
+ public void onCopySuccessful(File file, String mimeType) {
+ Uri shareableUri =
+ FileProvider.getUriForFile(
+ CallComposerActivity.this,
+ Constants.get().getFileProviderAuthority(),
+ file);
+
+ builder.setImage(grantUriPermission(shareableUri), mimeType);
+ placeRCSCall(builder);
+ }
+
+ @Override
+ public void onCopyFailed(Throwable throwable) {
+ // TODO(b/33753902)
+ LogUtil.e("CallComposerActivity.onCopyFailed", "copy Failed", throwable);
+ }
+ })
+ .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ } else {
+ Uri shareableUri =
+ FileProvider.getUriForFile(
+ this,
+ Constants.get().getFileProviderAuthority(),
+ new File(galleryComposerFragment.getGalleryData().getFilePath()));
+
+ builder.setImage(
+ grantUriPermission(shareableUri),
+ galleryComposerFragment.getGalleryData().getMimeType());
+
+ placeRCSCall(builder);
+ }
+ }
+ if (fragment instanceof CameraComposerFragment) {
+ CameraComposerFragment cameraComposerFragment = (CameraComposerFragment) fragment;
+ cameraComposerFragment.getCameraUriWhenReady(
+ uri -> {
+ builder.setImage(grantUriPermission(uri), cameraComposerFragment.getMimeType());
+ placeRCSCall(builder);
+ });
+ }
+ } else {
+ Assert.fail();
+ }
+ }
+
+ private boolean sessionReady() {
+ Session session = enrichedCallManager.getSession(sessionId);
+ if (session == null) {
+ return false;
+ }
+
+ return session.getState() == EnrichedCallManager.STATE_STARTED;
+ }
+
+ private void placeRCSCall(MultimediaData.Builder builder) {
+ LogUtil.i("CallComposerActivity.placeRCSCall", "placing enriched call");
+ Logger.get(this).logImpression(DialerImpression.Type.CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL);
+ enrichedCallManager.sendCallComposerData(sessionId, builder.build());
+ TelecomUtil.placeCall(
+ this, new CallIntentBuilder(contact.number, CallInitiationType.Type.CALL_COMPOSER).build());
+ finish();
+ }
+
+ /** Give permission to Messenger to view our image for RCS purposes. */
+ private Uri grantUriPermission(Uri uri) {
+ // TODO: Move this to the enriched call manager.
+ grantUriPermission(
+ "com.google.android.apps.messaging", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ return uri;
+ }
+
+ /** Animates {@code contactContainer} to align with content inside viewpager. */
+ @Override
+ public void onPageSelected(int position) {
+ if (currentIndex == CallComposerPagerAdapter.INDEX_MESSAGE) {
+ UiUtil.hideKeyboardFrom(this, windowContainer);
+ } else if (position == CallComposerPagerAdapter.INDEX_MESSAGE && inFullscreenMode) {
+ UiUtil.openKeyboardFrom(this, windowContainer);
+ }
+ currentIndex = position;
+ CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
+ locations[currentIndex] = fragment.getContentTopPx();
+ animateContactContainer(locations[currentIndex]);
+ animateSendAndCall(fragment.shouldHide());
+ setMediaIconSelected(position);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ CallComposerFragment fragment = (CallComposerFragment) adapter.instantiateItem(pager, position);
+ animateContactContainer(fragment.getContentTopPx());
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {}
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(VIEW_PAGER_STATE_KEY, pager.onSaveInstanceState());
+ outState.putBoolean(ENTRANCE_ANIMATION_KEY, shouldAnimateEntrance);
+ outState.putInt(CURRENT_INDEX_KEY, currentIndex);
+ outState.putIntArray(LOCATIONS_KEY, locations);
+ outState.putLong(SESSION_ID_KEY, sessionId);
+ }
+
+ @Override
+ public void onBackPressed() {
+ runExitAnimation();
+ }
+
+ @Override
+ public void composeCall(CallComposerFragment fragment) {
+ // Since our ViewPager restores state to our fragments, it's possible that they could call
+ // #composeCall, so we have to check if the calling fragment is the current fragment.
+ if (adapter.instantiateItem(pager, currentIndex) != fragment) {
+ return;
+ }
+ animateSendAndCall(fragment.shouldHide());
+ }
+
+ // To detect when the keyboard changes.
+ @Override
+ public void onLayoutChange(
+ View view,
+ int left,
+ int top,
+ int right,
+ int bottom,
+ int oldLeft,
+ int oldTop,
+ int oldRight,
+ int oldBottom) {
+ // To prevent infinite layout change loops
+ if (layoutChanged) {
+ layoutChanged = false;
+ return;
+ }
+
+ layoutChanged = true;
+ if (pager.getTop() < 0 || inFullscreenMode) {
+ ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
+ layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
+ pager.setLayoutParams(layoutParams);
+ }
+ }
+
+ @Override
+ public void onAnimationStart(Animator animation) {
+ isAnimatingContactBar = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ isAnimatingContactBar = false;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+
+ /**
+ * Reads arguments from the fragment arguments and populates the necessary instance variables.
+ * Copied from {@link com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void onHandleIntent(Intent intent) {
+ Bundle arguments = intent.getExtras();
+ if (arguments == null) {
+ throw new RuntimeException("CallComposerActivity.onHandleIntent, Arguments cannot be null.");
+ }
+ contact =
+ ProtoParsers.getFromInstanceState(
+ arguments, ARG_CALL_COMPOSER_CONTACT, new CallComposerContact());
+ updateContactInfo();
+ }
+
+ /**
+ * Populates the contact info fields based on the current contact information. Copied from {@link
+ * com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void updateContactInfo() {
+ if (contact.contactUri != null) {
+ setPhoto(
+ contact.photoId,
+ Uri.parse(contact.photoUri),
+ Uri.parse(contact.contactUri),
+ contact.nameOrNumber,
+ contact.isBusiness);
+ } else {
+ contactPhoto.setVisibility(View.GONE);
+ }
+ nameView.setText(contact.nameOrNumber);
+ getActionBar().setTitle(contact.nameOrNumber);
+ if (!TextUtils.isEmpty(contact.numberLabel) && !TextUtils.isEmpty(contact.displayNumber)) {
+ numberView.setVisibility(View.VISIBLE);
+ String secondaryInfo =
+ getString(
+ com.android.contacts.common.R.string.call_subject_type_and_number,
+ contact.numberLabel,
+ contact.displayNumber);
+ numberView.setText(secondaryInfo);
+ toolbar.setSubtitle(secondaryInfo);
+ } else {
+ numberView.setVisibility(View.GONE);
+ numberView.setText(null);
+ }
+ }
+
+ /**
+ * Sets the photo on the quick contact galleryIcon. Copied from {@link
+ * com.android.contacts.common.dialog.CallSubjectDialog}.
+ */
+ private void setPhoto(
+ long photoId, Uri photoUri, Uri contactUri, String displayName, boolean isBusiness) {
+ contactPhoto.assignContactUri(contactUri);
+ if (CompatUtils.isLollipopCompatible()) {
+ contactPhoto.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) {
+ contactPhoto.setImageDrawable(
+ getDrawable(R.drawable.product_logo_avatar_anonymous_color_120));
+ } else {
+ ContactPhotoManager.getInstance(this)
+ .loadThumbnail(
+ contactPhoto, photoId, false /* darkTheme */, true /* isCircular */, request);
+ }
+ }
+
+ private void animateContactContainer(int toY) {
+ if (toY == CallComposerFragment.CONTENT_TOP_UNSET
+ || toY == currentLocation
+ || (toY != locations[currentIndex]
+ && locations[currentIndex] != CallComposerFragment.CONTENT_TOP_UNSET)
+ || isAnimatingContactBar
+ || inFullscreenMode) {
+ return;
+ }
+ currentLocation = toY;
+ contactContainer
+ .animate()
+ .translationY(toY)
+ .setInterpolator(interpolator)
+ .setDuration(VIEW_PAGER_ANIMATION_DURATION_MILLIS)
+ .setListener(this)
+ .start();
+ }
+
+ /** Animates compose UI into view */
+ private void runEntranceAnimation() {
+ if (!shouldAnimateEntrance) {
+ return;
+ }
+ shouldAnimateEntrance = false;
+
+ int colorFrom = ContextCompat.getColor(this, android.R.color.transparent);
+ int colorTo = ContextCompat.getColor(this, R.color.call_composer_background_color);
+ ValueAnimator backgroundAnimation =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+ backgroundAnimation.setInterpolator(interpolator);
+ backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
+ backgroundAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ background.setBackgroundColor((int) animator.getAnimatedValue());
+ }
+ });
+
+ ValueAnimator contentAnimation = ValueAnimator.ofFloat(windowContainer.getHeight(), 0);
+ contentAnimation.setInterpolator(interpolator);
+ contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
+ contentAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ windowContainer.setY((Float) animation.getAnimatedValue());
+ }
+ });
+
+ AnimatorSet set = new AnimatorSet();
+ set.play(contentAnimation).with(backgroundAnimation);
+ set.start();
+ }
+
+ /** Animates compose UI out of view and ends the activity. */
+ private void runExitAnimation() {
+ int colorTo = ContextCompat.getColor(this, android.R.color.transparent);
+ int colorFrom = ContextCompat.getColor(this, R.color.call_composer_background_color);
+ ValueAnimator backgroundAnimation =
+ ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
+ backgroundAnimation.setInterpolator(interpolator);
+ backgroundAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS); // milliseconds
+ backgroundAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animator) {
+ background.setBackgroundColor((int) animator.getAnimatedValue());
+ }
+ });
+
+ ValueAnimator contentAnimation = ValueAnimator.ofFloat(0, windowContainer.getHeight());
+ contentAnimation.setInterpolator(interpolator);
+ contentAnimation.setDuration(ENTRANCE_ANIMATION_DURATION_MILLIS);
+ contentAnimation.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ windowContainer.setY((Float) animation.getAnimatedValue());
+ if (animation.getAnimatedFraction() > .75) {
+ finish();
+ }
+ }
+ });
+ AnimatorSet set = new AnimatorSet();
+ set.play(contentAnimation).with(backgroundAnimation);
+ set.start();
+ }
+
+ @Override
+ public void showFullscreen(boolean show) {
+ if (inFullscreenMode == show) {
+ return;
+ }
+ inFullscreenMode = show;
+ toolbar.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+ contactContainer.setVisibility(show ? View.GONE : View.VISIBLE);
+ ViewGroup.LayoutParams layoutParams = pager.getLayoutParams();
+ if (show) {
+ layoutParams.height = background.getHeight() - toolbar.getHeight() - messageIcon.getHeight();
+ } else {
+ layoutParams.height =
+ getResources().getDimensionPixelSize(R.dimen.call_composer_view_pager_height);
+ }
+ pager.setLayoutParams(layoutParams);
+ }
+
+ @Override
+ public boolean isFullscreen() {
+ return inFullscreenMode;
+ }
+
+ private void animateSendAndCall(final boolean shouldHide) {
+ // createCircularReveal doesn't respect animations being disabled, handle it here.
+ if (ViewUtil.areAnimationsDisabled(this)) {
+ isSendAndCallHidingOrHidden = shouldHide;
+ sendAndCall.setVisibility(shouldHide ? View.INVISIBLE : View.VISIBLE);
+ return;
+ }
+
+ // If the animation is changing directions, start it again. Else do nothing.
+ if (isSendAndCallHidingOrHidden != shouldHide) {
+ int centerX = sendAndCall.getWidth() / 2;
+ int centerY = sendAndCall.getHeight() / 2;
+ int startRadius = shouldHide ? centerX : 0;
+ int endRadius = shouldHide ? 0 : centerX;
+
+ // When the device rotates and state is restored, the send and call button may not be attached
+ // yet and this causes a crash when we attempt to to reveal it. To prevent this, we wait until
+ // {@code sendAndCall} is ready, then animate and reveal it.
+ ViewUtil.doOnPreDraw(
+ sendAndCall,
+ true,
+ new Runnable() {
+ @Override
+ public void run() {
+ Animator animator =
+ ViewAnimationUtils.createCircularReveal(
+ sendAndCall, centerX, centerY, startRadius, endRadius);
+ animator.addListener(
+ new AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ isSendAndCallHidingOrHidden = shouldHide;
+ sendAndCall.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (isSendAndCallHidingOrHidden) {
+ sendAndCall.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {}
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {}
+ });
+ animator.start();
+ }
+ });
+ }
+ }
+
+ private void setMediaIconSelected(int position) {
+ float alpha = 0.54f;
+ cameraIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_CAMERA ? 1 : alpha);
+ galleryIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_GALLERY ? 1 : alpha);
+ messageIcon.setAlpha(position == CallComposerPagerAdapter.INDEX_MESSAGE ? 1 : alpha);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/CallComposerFragment.java b/java/com/android/dialer/callcomposer/CallComposerFragment.java
new file mode 100644
index 000000000..d6f944955
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CallComposerFragment.java
@@ -0,0 +1,125 @@
+/*
+ * 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.callcomposer;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+
+/** Base fragment with fields and methods needed for all fragments in the call compose UI. */
+public abstract class CallComposerFragment extends Fragment {
+
+ protected static final int CAMERA_PERMISSION = 1;
+ protected static final int STORAGE_PERMISSION = 2;
+
+ private static final String LOCATION_KEY = "location_key";
+ public static final int CONTENT_TOP_UNSET = Integer.MAX_VALUE;
+
+ private View topView;
+ private int contentTopPx = CONTENT_TOP_UNSET;
+ private CallComposerListener testListener;
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = super.onCreateView(layoutInflater, viewGroup, bundle);
+ Assert.isNotNull(topView);
+ return view;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (!(context instanceof CallComposerListener) && testListener == null) {
+ LogUtil.e(
+ "CallComposerFragment.onAttach",
+ "Container activity must implement CallComposerListener.");
+ Assert.fail();
+ }
+ }
+
+ /** Call this method to declare which view is located at the top of the fragment's layout. */
+ public void setTopView(View view) {
+ topView = view;
+ // For each fragment that extends CallComposerFragment, the heights may vary and since
+ // ViewPagers cannot have their height set to wrap_content, we have to adjust the top of our
+ // container to match the top of the fragment. This listener populates {@code contentTopPx} as
+ // it's available.
+ topView
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ topView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ contentTopPx = topView.getTop();
+ }
+ });
+ }
+
+ public int getContentTopPx() {
+ return contentTopPx;
+ }
+
+ public void setParentForTesting(CallComposerListener listener) {
+ testListener = listener;
+ }
+
+ public CallComposerListener getListener() {
+ if (testListener != null) {
+ return testListener;
+ }
+ return FragmentUtils.getParentUnsafe(this, CallComposerListener.class);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(LOCATION_KEY, contentTopPx);
+ }
+
+ @Override
+ public void onViewStateRestored(Bundle savedInstanceState) {
+ super.onViewStateRestored(savedInstanceState);
+ if (savedInstanceState != null) {
+ contentTopPx = savedInstanceState.getInt(LOCATION_KEY);
+ }
+ }
+
+ public abstract boolean shouldHide();
+
+ /** Interface used to listen to CallComposeFragments */
+ public interface CallComposerListener {
+ /** Let the listener know when a call is ready to be composed. */
+ void composeCall(CallComposerFragment fragment);
+
+ /** Let the listener know when the layout has changed to full screen */
+ void showFullscreen(boolean show);
+
+ /** True is the listener is in fullscreen. */
+ boolean isFullscreen();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
new file mode 100644
index 000000000..4d4058a0a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * 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.callcomposer;
+
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import com.android.dialer.common.Assert;
+
+/** ViewPager adapter for call compose UI. */
+public class CallComposerPagerAdapter extends FragmentStatePagerAdapter {
+
+ public static final int INDEX_CAMERA = 0;
+ public static final int INDEX_GALLERY = 1;
+ public static final int INDEX_MESSAGE = 2;
+
+ private final int messageComposerCharLimit;
+
+ public CallComposerPagerAdapter(FragmentManager fragmentManager, int messageComposerCharLimit) {
+ super(fragmentManager);
+ this.messageComposerCharLimit = messageComposerCharLimit;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case INDEX_MESSAGE:
+ return MessageComposerFragment.newInstance(messageComposerCharLimit);
+ case INDEX_GALLERY:
+ return GalleryComposerFragment.newInstance();
+ case INDEX_CAMERA:
+ return new CameraComposerFragment();
+ default:
+ Assert.fail();
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return 3;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/CameraComposerFragment.java b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
new file mode 100644
index 000000000..f2d0a94a7
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/CameraComposerFragment.java
@@ -0,0 +1,378 @@
+/*
+ * 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.callcomposer;
+
+import android.Manifest;
+import android.Manifest.permission;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Animatable;
+import android.hardware.Camera.CameraInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.android.dialer.callcomposer.camera.CameraManager;
+import com.android.dialer.callcomposer.camera.CameraManager.CameraManagerListener;
+import com.android.dialer.callcomposer.camera.CameraManager.MediaCallback;
+import com.android.dialer.callcomposer.camera.CameraPreview.CameraPreviewHost;
+import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
+import com.android.dialer.callcomposer.cameraui.CameraMediaChooserView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.PermissionsUtil;
+
+/** Fragment used to compose call with image from the user's camera. */
+public class CameraComposerFragment extends CallComposerFragment
+ implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback {
+
+ private View permissionView;
+ private ImageButton exitFullscreen;
+ private ImageButton fullscreen;
+ private ImageButton swapCamera;
+ private ImageButton capture;
+ private ImageButton cancel;
+ private CameraMediaChooserView cameraView;
+ private RenderOverlay focus;
+ private View shutter;
+ private View allowPermission;
+ private CameraPreviewHost preview;
+ private ProgressBar loading;
+
+ private Uri cameraUri;
+ private boolean processingUri;
+ private String[] permissions = new String[] {Manifest.permission.CAMERA};
+ private CameraUriCallback uriCallback;
+
+ public static CameraComposerFragment newInstance() {
+ return new CameraComposerFragment();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) {
+ View root = inflater.inflate(R.layout.fragment_camera_composer, container, false);
+ permissionView = root.findViewById(R.id.permission_view);
+ loading = (ProgressBar) root.findViewById(R.id.loading);
+ cameraView = (CameraMediaChooserView) root.findViewById(R.id.camera_view);
+ shutter = cameraView.findViewById(R.id.camera_shutter_visual);
+ exitFullscreen = (ImageButton) cameraView.findViewById(R.id.camera_exit_fullscreen);
+ fullscreen = (ImageButton) cameraView.findViewById(R.id.camera_fullscreen);
+ swapCamera = (ImageButton) cameraView.findViewById(R.id.swap_camera_button);
+ capture = (ImageButton) cameraView.findViewById(R.id.camera_capture_button);
+ cancel = (ImageButton) cameraView.findViewById(R.id.camera_cancel_button);
+ focus = (RenderOverlay) cameraView.findViewById(R.id.focus_visual);
+ preview = (CameraPreviewHost) cameraView.findViewById(R.id.camera_preview);
+
+ exitFullscreen.setOnClickListener(this);
+ fullscreen.setOnClickListener(this);
+ swapCamera.setOnClickListener(this);
+ capture.setOnClickListener(this);
+ cancel.setOnClickListener(this);
+
+ if (!PermissionsUtil.hasPermission(getContext(), permission.CAMERA)) {
+ LogUtil.i("CameraComposerFragment.onCreateView", "Permission view shown.");
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DISPLAYED);
+ ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon);
+ TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text);
+ allowPermission = permissionView.findViewById(R.id.allow);
+
+ allowPermission.setOnClickListener(this);
+ permissionText.setText(R.string.camera_permission_text);
+ permissionImage.setImageResource(R.drawable.quantum_ic_camera_alt_white_48);
+ permissionImage.setColorFilter(
+ ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
+ permissionView.setVisibility(View.VISIBLE);
+ } else {
+ setupCamera();
+ }
+
+ setTopView(cameraView);
+ return root;
+ }
+
+ private void setupCamera() {
+ CameraManager.get().setListener(this);
+ preview.setShown();
+ CameraManager.get().setRenderOverlay(focus);
+ CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK);
+ setCameraUri(null);
+ }
+
+ @Override
+ public void onCameraError(int errorCode, Exception exception) {
+ LogUtil.e("CameraComposerFragment.onCameraError", "errorCode: ", errorCode, exception);
+ }
+
+ @Override
+ public void onCameraChanged() {
+ updateViewState();
+ }
+
+ @Override
+ public boolean shouldHide() {
+ return !processingUri && cameraUri == null;
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == capture) {
+ float heightPercent = 1;
+ if (!getListener().isFullscreen()) {
+ heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1);
+ }
+
+ showShutterEffect(shutter);
+ processingUri = true;
+ setCameraUri(null);
+ focus.getPieRenderer().clear();
+ CameraManager.get().takePicture(heightPercent, this);
+ } else if (view == swapCamera) {
+ ((Animatable) swapCamera.getDrawable()).start();
+ CameraManager.get().swapCamera();
+ } else if (view == cancel) {
+ processingUri = false;
+ setCameraUri(null);
+ } else if (view == exitFullscreen) {
+ getListener().showFullscreen(false);
+ fullscreen.setVisibility(View.VISIBLE);
+ exitFullscreen.setVisibility(View.GONE);
+ } else if (view == fullscreen) {
+ getListener().showFullscreen(true);
+ fullscreen.setVisibility(View.GONE);
+ exitFullscreen.setVisibility(View.VISIBLE);
+ } else if (view == allowPermission) {
+ // Checks to see if the user has permanently denied this permission. If this is the first
+ // time seeing this permission or they only pressed deny previously, they will see the
+ // permission request. If they permanently denied the permission, they will be sent to Dialer
+ // settings in order enable the permission.
+ if (PermissionsUtil.isFirstRequest(getContext(), permissions[0])
+ || shouldShowRequestPermissionRationale(permissions[0])) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_REQUESTED);
+ LogUtil.i("CameraComposerFragment.onClick", "Camera permission requested.");
+ requestPermissions(permissions, CAMERA_PERMISSION);
+ } else {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_SETTINGS);
+ LogUtil.i("CameraComposerFragment.onClick", "Settings opened to enable permission.");
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setData(Uri.parse("package:" + getContext().getPackageName()));
+ startActivity(intent);
+ }
+ }
+ }
+
+ /**
+ * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image is
+ * finished being cropped and stored on the device.
+ */
+ @Override
+ public void onMediaReady(Uri uri, String contentType, int width, int height) {
+ if (processingUri) {
+ processingUri = false;
+ setCameraUri(uri);
+ // If the user needed the URI before it was ready, uriCallback will be set and we should
+ // send the URI to them ASAP.
+ if (uriCallback != null) {
+ uriCallback.uriReady(uri);
+ uriCallback = null;
+ }
+ } else {
+ updateViewState();
+ }
+ }
+
+ /**
+ * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image failed
+ * to crop or be stored on the device.
+ */
+ @Override
+ public void onMediaFailed(Exception exception) {
+ LogUtil.e("CallComposerFragment.onMediaFailed", null, exception);
+ Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show();
+ setCameraUri(null);
+ processingUri = false;
+ if (uriCallback != null) {
+ loading.setVisibility(View.GONE);
+ uriCallback = null;
+ }
+ }
+
+ /**
+ * Usually called by {@link CameraManager} if the user does something to interrupt the picture
+ * while it's being taken (like switching the camera).
+ */
+ @Override
+ public void onMediaInfo(int what) {
+ if (what == MediaCallback.MEDIA_NO_DATA) {
+ Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show();
+ }
+ setCameraUri(null);
+ processingUri = false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ CameraManager.get().setListener(null);
+ }
+
+ private void showShutterEffect(final View shutterVisual) {
+ float maxAlpha = .7f;
+ int animationDurationMillis = 100;
+
+ AnimationSet animation = new AnimationSet(false /* shareInterpolator */);
+ Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha);
+ alphaInAnimation.setDuration(animationDurationMillis);
+ animation.addAnimation(alphaInAnimation);
+
+ Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f);
+ alphaOutAnimation.setStartOffset(animationDurationMillis);
+ alphaOutAnimation.setDuration(animationDurationMillis);
+ animation.addAnimation(alphaOutAnimation);
+
+ animation.setAnimationListener(
+ new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ shutterVisual.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ shutterVisual.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ shutterVisual.startAnimation(animation);
+ }
+
+ @NonNull
+ public String getMimeType() {
+ return "image/jpeg";
+ }
+
+ private void setCameraUri(Uri uri) {
+ cameraUri = uri;
+ // It's possible that if the user takes a picture and press back very quickly, the activity will
+ // no longer be alive and when the image cropping process completes, so we need to check that
+ // activity is still alive before trying to invoke it.
+ if (getListener() != null) {
+ updateViewState();
+ getListener().composeCall(this);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (PermissionsUtil.hasCameraPermissions(getContext())) {
+ permissionView.setVisibility(View.GONE);
+ setupCamera();
+ }
+ }
+
+ /** Updates the state of the buttons and overlays based on the current state of the view */
+ private void updateViewState() {
+ Assert.isNotNull(cameraView);
+ Assert.isNotNull(getContext());
+
+ boolean isCameraAvailable = CameraManager.get().isCameraAvailable();
+ boolean uriReadyOrProcessing = cameraUri != null || processingUri;
+
+ if (cameraUri == null && isCameraAvailable) {
+ CameraManager.get().resetPreview();
+ cancel.setVisibility(View.GONE);
+ }
+
+ if (!CameraManager.get().hasFrontAndBackCamera()) {
+ swapCamera.setVisibility(View.GONE);
+ } else {
+ swapCamera.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
+ }
+
+ capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
+ cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE);
+
+ if (uriReadyOrProcessing) {
+ fullscreen.setVisibility(View.GONE);
+ exitFullscreen.setVisibility(View.GONE);
+ } else if (getListener().isFullscreen()) {
+ exitFullscreen.setVisibility(View.VISIBLE);
+ fullscreen.setVisibility(View.GONE);
+ } else {
+ exitFullscreen.setVisibility(View.GONE);
+ fullscreen.setVisibility(View.VISIBLE);
+ }
+
+ swapCamera.setEnabled(isCameraAvailable);
+ capture.setEnabled(isCameraAvailable);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
+ PermissionsUtil.permissionRequested(getContext(), permissions[0]);
+ }
+ if (requestCode == CAMERA_PERMISSION
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_GRANTED);
+ LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission granted.");
+ permissionView.setVisibility(View.GONE);
+ setupCamera();
+ } else if (requestCode == CAMERA_PERMISSION) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DENIED);
+ LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission denied.");
+ }
+ }
+
+ public void getCameraUriWhenReady(CameraUriCallback callback) {
+ if (processingUri) {
+ loading.setVisibility(View.VISIBLE);
+ uriCallback = callback;
+ } else {
+ callback.uriReady(cameraUri);
+ }
+ }
+
+ /** Callback to let the caller know when the URI is ready. */
+ public interface CameraUriCallback {
+ void uriReady(Uri uri);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryComposerFragment.java b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
new file mode 100644
index 000000000..623127945
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryComposerFragment.java
@@ -0,0 +1,256 @@
+/*
+ * 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.callcomposer;
+
+import static android.app.Activity.RESULT_OK;
+
+import android.Manifest.permission;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask.Callback;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.File;
+
+/** Fragment used to compose call with image from the user's gallery. */
+public class GalleryComposerFragment extends CallComposerFragment
+ implements LoaderCallbacks<Cursor>, OnClickListener {
+
+ private static final int RESULT_LOAD_IMAGE = 1;
+ private static final int RESULT_OPEN_SETTINGS = 2;
+
+ private GalleryGridAdapter adapter;
+ private GridView galleryGridView;
+ private View permissionView;
+ private View allowPermission;
+
+ private String[] permissions = new String[] {permission.READ_EXTERNAL_STORAGE};
+ private CursorLoader cursorLoader;
+ private GalleryGridItemData selectedData = null;
+ private boolean selectedDataIsCopy;
+
+ public static GalleryComposerFragment newInstance() {
+ return new GalleryComposerFragment();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) {
+ View view = inflater.inflate(R.layout.fragment_gallery_composer, container, false);
+ galleryGridView = (GridView) view.findViewById(R.id.gallery_grid_view);
+ permissionView = view.findViewById(R.id.permission_view);
+
+ if (!PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DISPLAYED);
+ LogUtil.i("GalleryComposerFragment.onCreateView", "Permission view shown.");
+ ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon);
+ TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text);
+ allowPermission = permissionView.findViewById(R.id.allow);
+
+ allowPermission.setOnClickListener(this);
+ permissionText.setText(R.string.gallery_permission_text);
+ permissionImage.setImageResource(R.drawable.quantum_ic_photo_white_48);
+ permissionImage.setColorFilter(
+ ContextCompat.getColor(getContext(), R.color.dialer_theme_color));
+ permissionView.setVisibility(View.VISIBLE);
+ } else {
+ setupGallery();
+ }
+
+ setTopView(galleryGridView);
+ return view;
+ }
+
+ private void setupGallery() {
+ adapter = new GalleryGridAdapter(getContext(), null, this);
+ galleryGridView.setAdapter(adapter);
+ getLoaderManager().initLoader(0 /* id */, null /* args */, this /* loaderCallbacks */);
+ }
+
+ @Override
+ public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+ return cursorLoader = new GalleryCursorLoader(getContext());
+ }
+
+ @Override
+ public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+ adapter.swapCursor(cursor);
+ }
+
+ @Override
+ public void onLoaderReset(Loader<Cursor> loader) {
+ adapter.swapCursor(null);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == allowPermission) {
+ // Checks to see if the user has permanently denied this permission. If this is their first
+ // time seeing this permission or they've only pressed deny previously, they will see the
+ // permission request. If they've permanently denied the permission, they will be sent to
+ // Dialer settings in order to enable the permission.
+ if (PermissionsUtil.isFirstRequest(getContext(), permissions[0])
+ || shouldShowRequestPermissionRationale(permissions[0])) {
+ LogUtil.i("GalleryComposerFragment.onClick", "Storage permission requested.");
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_REQUESTED);
+ requestPermissions(permissions, STORAGE_PERMISSION);
+ } else {
+ LogUtil.i("GalleryComposerFragment.onClick", "Settings opened to enable permission.");
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_SETTINGS);
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ intent.setData(Uri.parse("package:" + getContext().getPackageName()));
+ startActivityForResult(intent, RESULT_OPEN_SETTINGS);
+ }
+ return;
+ } else {
+ GalleryGridItemView itemView = ((GalleryGridItemView) view);
+ if (itemView.isGallery()) {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ intent.putExtra(Intent.EXTRA_MIME_TYPES, GalleryCursorLoader.ACCEPTABLE_IMAGE_TYPES);
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ startActivityForResult(intent, RESULT_LOAD_IMAGE);
+ } else if (itemView.getData().equals(selectedData)) {
+ setSelected(null, false);
+ } else {
+ setSelected(new GalleryGridItemData(itemView.getData()), false);
+ }
+ }
+ }
+
+ @Nullable
+ public GalleryGridItemData getGalleryData() {
+ return selectedData;
+ }
+
+ public GridView getGalleryGridView() {
+ return galleryGridView;
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == RESULT_LOAD_IMAGE && resultCode == RESULT_OK && data != null) {
+ prepareDataForAttachment(data);
+ } else if (requestCode == RESULT_OPEN_SETTINGS
+ && PermissionsUtil.hasPermission(getContext(), permission.READ_EXTERNAL_STORAGE)) {
+ permissionView.setVisibility(View.GONE);
+ setupGallery();
+ }
+ }
+
+ private void setSelected(GalleryGridItemData data, boolean isCopy) {
+ selectedData = data;
+ selectedDataIsCopy = isCopy;
+ adapter.setSelected(selectedData);
+ getListener().composeCall(this);
+ }
+
+ @Override
+ public boolean shouldHide() {
+ return selectedData == null
+ || selectedData.getFilePath() == null
+ || selectedData.getMimeType() == null;
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
+ PermissionsUtil.permissionRequested(getContext(), permissions[0]);
+ }
+ if (requestCode == STORAGE_PERMISSION
+ && grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_GRANTED);
+ LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission granted.");
+ permissionView.setVisibility(View.GONE);
+ setupGallery();
+ } else if (requestCode == STORAGE_PERMISSION) {
+ Logger.get(getContext()).logImpression(DialerImpression.Type.STORAGE_PERMISSION_DENIED);
+ LogUtil.i("GalleryComposerFragment.onRequestPermissionsResult", "Permission denied.");
+ }
+ }
+
+ public CursorLoader getCursorLoader() {
+ return cursorLoader;
+ }
+
+ public boolean selectedDataIsCopy() {
+ return selectedDataIsCopy;
+ }
+
+ private void prepareDataForAttachment(Intent data) {
+ // We're using the builtin photo picker which supplies the return url as it's "data".
+ String url = data.getDataString();
+ if (url == null) {
+ final Bundle extras = data.getExtras();
+ if (extras != null) {
+ final Uri uri = extras.getParcelable(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ url = uri.toString();
+ }
+ }
+ }
+
+ // This should never happen, but just in case..
+ // Guard against null uri cases for when the activity returns a null/invalid intent.
+ if (url != null) {
+ new CopyAndResizeImageTask(
+ getContext(),
+ Uri.parse(url),
+ new Callback() {
+ @Override
+ public void onCopySuccessful(File file, String mimeType) {
+ setSelected(adapter.insertEntry(file.getAbsolutePath(), mimeType), true);
+ }
+
+ @Override
+ public void onCopyFailed(Throwable throwable) {
+ // TODO(b/33753902)
+ LogUtil.e(
+ "GalleryComposerFragment.onFailure", "Data preparation failed", throwable);
+ }
+ })
+ .execute();
+ } else {
+ // TODO(b/33753902)
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryCursorLoader.java b/java/com/android/dialer/callcomposer/GalleryCursorLoader.java
new file mode 100644
index 000000000..f9990e167
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryCursorLoader.java
@@ -0,0 +1,54 @@
+/*
+ * 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.callcomposer;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.MediaStore.Files;
+import android.provider.MediaStore.Files.FileColumns;
+import android.provider.MediaStore.Images.Media;
+import android.support.v4.content.CursorLoader;
+
+/** A BoundCursorLoader that reads local media on the device. */
+public class GalleryCursorLoader extends CursorLoader {
+ public static final String MEDIA_SCANNER_VOLUME_EXTERNAL = "external";
+ public static final String[] ACCEPTABLE_IMAGE_TYPES =
+ new String[] {"image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp"};
+
+ private static final Uri STORAGE_URI = Files.getContentUri(MEDIA_SCANNER_VOLUME_EXTERNAL);
+ private static final String SORT_ORDER = Media.DATE_MODIFIED + " DESC";
+ private static final String IMAGE_SELECTION = createSelection();
+
+ public GalleryCursorLoader(Context context) {
+ super(
+ context,
+ STORAGE_URI,
+ GalleryGridItemData.IMAGE_PROJECTION,
+ IMAGE_SELECTION,
+ null,
+ SORT_ORDER);
+ }
+
+ @SuppressLint("DefaultLocale")
+ private static String createSelection() {
+ return String.format(
+ "mime_type IN ('image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp')"
+ + " AND media_type in (%d)",
+ FileColumns.MEDIA_TYPE_IMAGE);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryGridAdapter.java b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
new file mode 100644
index 000000000..0a7fd769b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryGridAdapter.java
@@ -0,0 +1,118 @@
+/*
+ * 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.callcomposer;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.support.annotation.NonNull;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Bridges between the image cursor loaded by GalleryBoundCursorLoader and the GalleryGridView. */
+public class GalleryGridAdapter extends CursorAdapter {
+
+ @NonNull private final OnClickListener onClickListener;
+ @NonNull private final List<GalleryGridItemView> views = new ArrayList<>();
+ @NonNull private final Context context;
+
+ private GalleryGridItemData selectedData;
+
+ public GalleryGridAdapter(
+ @NonNull Context context, Cursor cursor, @NonNull OnClickListener onClickListener) {
+ super(context, cursor, 0);
+ this.onClickListener = Assert.isNotNull(onClickListener);
+ this.context = Assert.isNotNull(context);
+ }
+
+ @Override
+ public int getCount() {
+ // Add one for the header.
+ return super.getCount() + 1;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // At position 0, we want to insert a header. If position == 0, we don't need the cursor.
+ // If position != 0, then we need to move the cursor to position - 1 to account for the offset
+ // of the header.
+ if (position != 0 && !getCursor().moveToPosition(position - 1)) {
+ Assert.fail("couldn't move cursor to position " + (position - 1));
+ }
+ View view;
+ if (convertView == null) {
+ view = newView(context, getCursor(), parent);
+ } else {
+ view = convertView;
+ }
+ bindView(view, context, getCursor(), position);
+ return view;
+ }
+
+ private void bindView(View view, Context context, Cursor cursor, int position) {
+ if (position == 0) {
+ GalleryGridItemView gridView = (GalleryGridItemView) view;
+ gridView.showGallery(true);
+ } else {
+ bindView(view, context, cursor);
+ }
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ GalleryGridItemView gridView = (GalleryGridItemView) view;
+ gridView.bind(cursor);
+ gridView.setSelected(gridView.getData().equals(selectedData));
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ GalleryGridItemView view =
+ (GalleryGridItemView)
+ LayoutInflater.from(context).inflate(R.layout.gallery_grid_item_view, parent, false);
+ view.setOnClickListener(onClickListener);
+ views.add(view);
+ return view;
+ }
+
+ public void setSelected(GalleryGridItemData selectedData) {
+ this.selectedData = selectedData;
+ for (GalleryGridItemView view : views) {
+ view.setSelected(view.getData().equals(selectedData));
+ }
+ }
+
+ public GalleryGridItemData insertEntry(String filePath, String mimeType) {
+ LogUtil.i("GalleryGridAdapter.insertRow", mimeType + " " + filePath);
+
+ MatrixCursor extraRow = new MatrixCursor(GalleryGridItemData.IMAGE_PROJECTION);
+ extraRow.addRow(new Object[] {0L, filePath, mimeType, ""});
+ extraRow.moveToFirst();
+ Cursor extendedCursor = new MergeCursor(new Cursor[] {extraRow, getCursor()});
+ swapCursor(extendedCursor);
+
+ return new GalleryGridItemData(extraRow);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemData.java b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
new file mode 100644
index 000000000..402c6ce6d
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryGridItemData.java
@@ -0,0 +1,91 @@
+/*
+ * 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.callcomposer;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.MediaStore.Images.Media;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import java.io.File;
+import java.util.Objects;
+
+/** Provides data for GalleryGridItemView */
+public final class GalleryGridItemData {
+ public static final String[] IMAGE_PROJECTION =
+ new String[] {Media._ID, Media.DATA, Media.MIME_TYPE, Media.DATE_MODIFIED};
+
+ private static final int INDEX_DATA_PATH = 1;
+ private static final int INDEX_MIME_TYPE = 2;
+ private static final int INDEX_DATE_MODIFIED = 3;
+
+ private String filePath;
+ private String mimeType;
+ private long dateModifiedSeconds;
+
+ public GalleryGridItemData() {}
+
+ public GalleryGridItemData(GalleryGridItemData copyData) {
+ filePath = Assert.isNotNull(copyData.getFilePath());
+ mimeType = Assert.isNotNull(copyData.getMimeType());
+ dateModifiedSeconds = Assert.isNotNull(copyData.getDateModifiedSeconds());
+ }
+
+ public GalleryGridItemData(Cursor cursor) {
+ bind(cursor);
+ }
+
+ public void bind(Cursor cursor) {
+ mimeType = Assert.isNotNull(cursor.getString(INDEX_MIME_TYPE));
+ String dateModified = Assert.isNotNull(cursor.getString(INDEX_DATE_MODIFIED));
+ dateModifiedSeconds = !TextUtils.isEmpty(dateModified) ? Long.parseLong(dateModified) : -1;
+ filePath = Assert.isNotNull(cursor.getString(INDEX_DATA_PATH));
+ }
+
+ @Nullable
+ public String getFilePath() {
+ return filePath;
+ }
+
+ @Nullable
+ public Uri getFileUri() {
+ return TextUtils.isEmpty(filePath) ? null : Uri.fromFile(new File(filePath));
+ }
+
+ /** @return The date in seconds. This can be negative if we could not retrieve date info */
+ public long getDateModifiedSeconds() {
+ return dateModifiedSeconds;
+ }
+
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof GalleryGridItemData
+ && Objects.equals(mimeType, ((GalleryGridItemData) obj).mimeType)
+ && Objects.equals(filePath, ((GalleryGridItemData) obj).filePath)
+ && ((GalleryGridItemData) obj).dateModifiedSeconds == dateModifiedSeconds;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(filePath, mimeType, dateModifiedSeconds);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/GalleryGridItemView.java b/java/com/android/dialer/callcomposer/GalleryGridItemView.java
new file mode 100644
index 000000000..d70fd57c1
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/GalleryGridItemView.java
@@ -0,0 +1,126 @@
+/*
+ * 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.callcomposer;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
+import com.bumptech.glide.request.RequestOptions;
+import java.util.concurrent.TimeUnit;
+
+/** Shows an item in the gallery picker grid view. Hosts an FileImageView with a checkbox. */
+public class GalleryGridItemView extends FrameLayout {
+
+ private final GalleryGridItemData data = new GalleryGridItemData();
+
+ private ImageView image;
+ private View checkbox;
+ private View gallery;
+ private String currentFilePath;
+ private boolean isGallery;
+
+ public GalleryGridItemView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ image = (ImageView) findViewById(R.id.image);
+ checkbox = findViewById(R.id.checkbox);
+ gallery = findViewById(R.id.gallery);
+
+ image.setClipToOutline(true);
+ checkbox.setClipToOutline(true);
+ gallery.setClipToOutline(true);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // The grid view auto-fit the columns, so we want to let the height match the width
+ // to make the image square.
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+
+ public GalleryGridItemData getData() {
+ return data;
+ }
+
+ @Override
+ public void setSelected(boolean selected) {
+ if (selected) {
+ checkbox.setVisibility(VISIBLE);
+ int paddingPx = getResources().getDimensionPixelSize(R.dimen.gallery_item_selected_padding);
+ setPadding(paddingPx, paddingPx, paddingPx, paddingPx);
+ } else {
+ checkbox.setVisibility(GONE);
+ int paddingPx = getResources().getDimensionPixelOffset(R.dimen.gallery_item_padding);
+ setPadding(paddingPx, paddingPx, paddingPx, paddingPx);
+ }
+ }
+
+ public boolean isGallery() {
+ return isGallery;
+ }
+
+ public void showGallery(boolean show) {
+ isGallery = show;
+ gallery.setVisibility(show ? VISIBLE : INVISIBLE);
+ }
+
+ public void bind(Cursor cursor) {
+ data.bind(cursor);
+ showGallery(false);
+ updateImageView();
+ }
+
+ private void updateImageView() {
+ image.setScaleType(ScaleType.CENTER_CROP);
+
+ if (currentFilePath == null || !currentFilePath.equals(data.getFilePath())) {
+ currentFilePath = data.getFilePath();
+
+ // Downloads/loads an image from the given URI so that the image's largest dimension is
+ // between 1/2 the given dimensions and the given dimensions, with no restrictions on the
+ // image's smallest dimension. We skip the memory cache, but glide still applies it's disk
+ // cache to optimize loads.
+ Glide.with(getContext())
+ .load(data.getFileUri())
+ .apply(RequestOptions.downsampleOf(DownsampleStrategy.AT_MOST).skipMemoryCache(true))
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .into(image);
+ }
+ long dateModifiedSeconds = data.getDateModifiedSeconds();
+ if (dateModifiedSeconds > 0) {
+ image.setContentDescription(
+ getResources()
+ .getString(
+ R.string.gallery_item_description,
+ TimeUnit.SECONDS.toMillis(dateModifiedSeconds)));
+ } else {
+ image.setContentDescription(
+ getResources().getString(R.string.gallery_item_description_no_date));
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/MessageComposerFragment.java b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
new file mode 100644
index 000000000..521b71402
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/MessageComposerFragment.java
@@ -0,0 +1,143 @@
+/*
+ * 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.callcomposer;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/** Fragment used to compose call with message fragment. */
+public class MessageComposerFragment extends CallComposerFragment
+ implements OnClickListener, TextWatcher, OnTouchListener, OnLongClickListener {
+ private static final String CHAR_LIMIT_KEY = "char_limit";
+
+ public static final int NO_CHAR_LIMIT = -1;
+
+ private EditText customMessage;
+ private boolean isLongClick = false;
+ private int charLimit;
+
+ public static MessageComposerFragment newInstance(int charLimit) {
+ MessageComposerFragment fragment = new MessageComposerFragment();
+ Bundle args = new Bundle();
+ args.putInt(CHAR_LIMIT_KEY, charLimit);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ public String getMessage() {
+ return customMessage == null ? null : customMessage.getText().toString();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ charLimit = getArguments().getInt(CHAR_LIMIT_KEY, NO_CHAR_LIMIT);
+
+ View view = inflater.inflate(R.layout.fragment_message_composer, container, false);
+ TextView urgent = (TextView) view.findViewById(R.id.message_urgent);
+ customMessage = (EditText) view.findViewById(R.id.custom_message);
+
+ urgent.setOnClickListener(this);
+ customMessage.setOnTouchListener(this);
+ customMessage.setOnLongClickListener(this);
+ customMessage.addTextChangedListener(this);
+ if (charLimit != NO_CHAR_LIMIT) {
+ TextView remainingChar = (TextView) view.findViewById(R.id.remaining_characters);
+ remainingChar.setText("" + charLimit);
+ customMessage.setFilters(new InputFilter[] {new InputFilter.LengthFilter(charLimit)});
+ customMessage.addTextChangedListener(
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ remainingChar.setText("" + (charLimit - editable.length()));
+ }
+ });
+ }
+ view.findViewById(R.id.message_chat).setOnClickListener(this);
+ view.findViewById(R.id.message_question).setOnClickListener(this);
+
+ setTopView(urgent);
+ return view;
+ }
+
+ @Override
+ public void onClick(View view) {
+ customMessage.setText(((TextView) view).getText());
+ customMessage.setSelection(customMessage.getText().length());
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ getListener().composeCall(this);
+ }
+
+ /**
+ * EditTexts take two clicks to dispatch an onClick() event, so instead we add an onTouchListener
+ * to listen for them. The caveat to this is that it also requires listening for onLongClicks to
+ * distinguish whether a MotionEvent came from a click or a long click.
+ */
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ if (isLongClick) {
+ isLongClick = false;
+ } else {
+ getListener().showFullscreen(true);
+ }
+ }
+ view.performClick();
+ return false;
+ }
+
+ @Override
+ public boolean onLongClick(View v) {
+ isLongClick = true;
+ return false;
+ }
+
+ @Override
+ public boolean shouldHide() {
+ return TextUtils.isEmpty(getMessage());
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml b/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml
new file mode 100644
index 000000000..82f141284
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.callcomposer.camera"/> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/CameraManager.java b/java/com/android/dialer/callcomposer/camera/CameraManager.java
new file mode 100644
index 000000000..87cd16a99
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/CameraManager.java
@@ -0,0 +1,822 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.hardware.Camera;
+import android.hardware.Camera.CameraInfo;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.view.MotionEvent;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.view.View;
+import android.view.WindowManager;
+import com.android.dialer.callcomposer.camera.camerafocus.FocusOverlayManager;
+import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Class which manages interactions with the camera, but does not do any UI. This class is designed
+ * to be a singleton to ensure there is one component managing the camera and releasing the native
+ * resources. In order to acquire a camera, a caller must:
+ *
+ * <ul>
+ * <li>Call selectCamera to select front or back camera
+ * <li>Call setSurface to control where the preview is shown
+ * <li>Call openCamera to request the camera start preview
+ * </ul>
+ *
+ * Callers should call onPause and onResume to ensure that the camera is release while the activity
+ * is not active. This class is not thread safe. It should only be called from one thread (the UI
+ * thread or test thread)
+ */
+public class CameraManager implements FocusOverlayManager.Listener {
+ /** Callbacks for the camera manager listener */
+ public interface CameraManagerListener {
+ void onCameraError(int errorCode, Exception e);
+
+ void onCameraChanged();
+ }
+
+ /** Callback when taking image or video */
+ public interface MediaCallback {
+ int MEDIA_CAMERA_CHANGED = 1;
+ int MEDIA_NO_DATA = 2;
+
+ void onMediaReady(Uri uriToMedia, String contentType, int width, int height);
+
+ void onMediaFailed(Exception exception);
+
+ void onMediaInfo(int what);
+ }
+
+ // Error codes
+ private static final int ERROR_OPENING_CAMERA = 1;
+ private static final int ERROR_SHOWING_PREVIEW = 2;
+ private static final int ERROR_HARDWARE_ACCELERATION_DISABLED = 3;
+ private static final int ERROR_TAKING_PICTURE = 4;
+
+ private static final int NO_CAMERA_SELECTED = -1;
+
+ private static final Camera.ShutterCallback DUMMY_SHUTTER_CALLBACK =
+ new Camera.ShutterCallback() {
+ @Override
+ public void onShutter() {
+ // Do nothing
+ }
+ };
+
+ private static CameraManager sInstance;
+
+ /** The CameraInfo for the currently selected camera */
+ private final CameraInfo mCameraInfo;
+
+ /** The index of the selected camera or NO_CAMERA_SELECTED if a camera hasn't been selected yet */
+ private int mCameraIndex;
+
+ /** True if the device has front and back cameras */
+ private final boolean mHasFrontAndBackCamera;
+
+ /** True if the camera should be open (may not yet be actually open) */
+ private boolean mOpenRequested;
+
+ /** The preview view to show the preview on */
+ private CameraPreview mCameraPreview;
+
+ /** The helper classs to handle orientation changes */
+ private OrientationHandler mOrientationHandler;
+
+ /** Tracks whether the preview has hardware acceleration */
+ private boolean mIsHardwareAccelerationSupported;
+
+ /**
+ * The task for opening the camera, so it doesn't block the UI thread Using AsyncTask rather than
+ * SafeAsyncTask because the tasks need to be serialized, but don't need to be on the UI thread
+ * TODO: If we have other AyncTasks (not SafeAsyncTasks) this may contend and we may need
+ * to create a dedicated thread, or synchronize the threads in the thread pool
+ */
+ private AsyncTask<Integer, Void, Camera> mOpenCameraTask;
+
+ /**
+ * The camera index that is queued to be opened, but not completed yet, or NO_CAMERA_SELECTED if
+ * no open task is pending
+ */
+ private int mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+
+ /** The instance of the currently opened camera */
+ private Camera mCamera;
+
+ /** The rotation of the screen relative to the camera's natural orientation */
+ private int mRotation;
+
+ /** The callback to notify when errors or other events occur */
+ private CameraManagerListener mListener;
+
+ /** True if the camera is currently in the process of taking an image */
+ private boolean mTakingPicture;
+
+ /** Manages auto focus visual and behavior */
+ private final FocusOverlayManager mFocusOverlayManager;
+
+ private CameraManager() {
+ mCameraInfo = new CameraInfo();
+ mCameraIndex = NO_CAMERA_SELECTED;
+
+ // Check to see if a front and back camera exist
+ boolean hasFrontCamera = false;
+ boolean hasBackCamera = false;
+ final CameraInfo cameraInfo = new CameraInfo();
+ final int cameraCount = Camera.getNumberOfCameras();
+ try {
+ for (int i = 0; i < cameraCount; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT) {
+ hasFrontCamera = true;
+ } else if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
+ hasBackCamera = true;
+ }
+ if (hasFrontCamera && hasBackCamera) {
+ break;
+ }
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.CameraManager", "Unable to load camera info", e);
+ }
+ mHasFrontAndBackCamera = hasFrontCamera && hasBackCamera;
+ mFocusOverlayManager = new FocusOverlayManager(this, Looper.getMainLooper());
+
+ // Assume the best until we are proven otherwise
+ mIsHardwareAccelerationSupported = true;
+ }
+
+ /** Gets the singleton instance */
+ public static CameraManager get() {
+ if (sInstance == null) {
+ sInstance = new CameraManager();
+ }
+ return sInstance;
+ }
+
+ /**
+ * Sets the surface to use to display the preview This must only be called AFTER the CameraPreview
+ * has a texture ready
+ *
+ * @param preview The preview surface view
+ */
+ void setSurface(final CameraPreview preview) {
+ if (preview == mCameraPreview) {
+ return;
+ }
+
+ if (preview != null) {
+ Assert.checkArgument(preview.isValid());
+ preview.setOnTouchListener(
+ new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(final View view, final MotionEvent motionEvent) {
+ if ((motionEvent.getActionMasked() & MotionEvent.ACTION_UP)
+ == MotionEvent.ACTION_UP) {
+ mFocusOverlayManager.setPreviewSize(view.getWidth(), view.getHeight());
+ mFocusOverlayManager.onSingleTapUp(
+ (int) motionEvent.getX() + view.getLeft(),
+ (int) motionEvent.getY() + view.getTop());
+ }
+ view.performClick();
+ return true;
+ }
+ });
+ }
+ mCameraPreview = preview;
+ tryShowPreview();
+ }
+
+ public void setRenderOverlay(final RenderOverlay renderOverlay) {
+ mFocusOverlayManager.setFocusRenderer(
+ renderOverlay != null ? renderOverlay.getPieRenderer() : null);
+ }
+
+ /** Convenience function to swap between front and back facing cameras */
+ public void swapCamera() {
+ Assert.checkState(mCameraIndex >= 0);
+ selectCamera(
+ mCameraInfo.facing == CameraInfo.CAMERA_FACING_FRONT
+ ? CameraInfo.CAMERA_FACING_BACK
+ : CameraInfo.CAMERA_FACING_FRONT);
+ }
+
+ /**
+ * Selects the first camera facing the desired direction, or the first camera if there is no
+ * camera in the desired direction
+ *
+ * @param desiredFacing One of the CameraInfo.CAMERA_FACING_* constants
+ * @return True if a camera was selected, or false if selecting a camera failed
+ */
+ public boolean selectCamera(final int desiredFacing) {
+ try {
+ // We already selected a camera facing that direction
+ if (mCameraIndex >= 0 && mCameraInfo.facing == desiredFacing) {
+ return true;
+ }
+
+ final int cameraCount = Camera.getNumberOfCameras();
+ Assert.checkState(cameraCount > 0);
+
+ mCameraIndex = NO_CAMERA_SELECTED;
+ setCamera(null);
+ final CameraInfo cameraInfo = new CameraInfo();
+ for (int i = 0; i < cameraCount; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == desiredFacing) {
+ mCameraIndex = i;
+ Camera.getCameraInfo(i, mCameraInfo);
+ break;
+ }
+ }
+
+ // There's no camera in the desired facing direction, just select the first camera
+ // regardless of direction
+ if (mCameraIndex < 0) {
+ mCameraIndex = 0;
+ Camera.getCameraInfo(0, mCameraInfo);
+ }
+
+ if (mOpenRequested) {
+ // The camera is open, so reopen with the newly selected camera
+ openCamera();
+ }
+ return true;
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.selectCamera", "RuntimeException in CameraManager.selectCamera", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ return false;
+ }
+ }
+
+ public int getCameraIndex() {
+ return mCameraIndex;
+ }
+
+ public void selectCameraByIndex(final int cameraIndex) {
+ if (mCameraIndex == cameraIndex) {
+ return;
+ }
+
+ try {
+ mCameraIndex = cameraIndex;
+ Camera.getCameraInfo(mCameraIndex, mCameraInfo);
+ if (mOpenRequested) {
+ openCamera();
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.selectCameraByIndex",
+ "RuntimeException in CameraManager.selectCameraByIndex",
+ e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public CameraInfo getCameraInfo() {
+ if (mCameraIndex == NO_CAMERA_SELECTED) {
+ return null;
+ }
+ return mCameraInfo;
+ }
+
+ /** @return True if the device has both a front and back camera */
+ public boolean hasFrontAndBackCamera() {
+ return mHasFrontAndBackCamera;
+ }
+
+ /** Opens the camera on a separate thread and initiates the preview if one is available */
+ void openCamera() {
+ if (mCameraIndex == NO_CAMERA_SELECTED) {
+ // Ensure a selected camera if none is currently selected. This may happen if the
+ // camera chooser is not the default media chooser.
+ selectCamera(CameraInfo.CAMERA_FACING_BACK);
+ }
+ mOpenRequested = true;
+ // We're already opening the camera or already have the camera handle, nothing more to do
+ if (mPendingOpenCameraIndex == mCameraIndex || mCamera != null) {
+ return;
+ }
+
+ // True if the task to open the camera has to be delayed until the current one completes
+ boolean delayTask = false;
+
+ // Cancel any previous open camera tasks
+ if (mOpenCameraTask != null) {
+ mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+ delayTask = true;
+ }
+
+ mPendingOpenCameraIndex = mCameraIndex;
+ mOpenCameraTask =
+ new AsyncTask<Integer, Void, Camera>() {
+ private Exception mException;
+
+ @Override
+ protected Camera doInBackground(final Integer... params) {
+ try {
+ final int cameraIndex = params[0];
+ LogUtil.v("CameraManager.doInBackground", "Opening camera " + mCameraIndex);
+ return Camera.open(cameraIndex);
+ } catch (final Exception e) {
+ LogUtil.e("CameraManager.doInBackground", "Exception while opening camera", e);
+ mException = e;
+ return null;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(final Camera camera) {
+ // If we completed, but no longer want this camera, then release the camera
+ if (mOpenCameraTask != this || !mOpenRequested) {
+ releaseCamera(camera);
+ cleanup();
+ return;
+ }
+
+ cleanup();
+
+ LogUtil.v(
+ "CameraManager.onPostExecute",
+ "Opened camera " + mCameraIndex + " " + (camera != null));
+ setCamera(camera);
+ if (camera == null) {
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, mException);
+ }
+ LogUtil.e("CameraManager.onPostExecute", "Error opening camera");
+ }
+ }
+
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ cleanup();
+ }
+
+ private void cleanup() {
+ mPendingOpenCameraIndex = NO_CAMERA_SELECTED;
+ if (mOpenCameraTask != null && mOpenCameraTask.getStatus() == Status.PENDING) {
+ // If there's another task waiting on this one to complete, start it now
+ mOpenCameraTask.execute(mCameraIndex);
+ } else {
+ mOpenCameraTask = null;
+ }
+ }
+ };
+ LogUtil.v("CameraManager.openCamera", "Start opening camera " + mCameraIndex);
+ if (!delayTask) {
+ mOpenCameraTask.execute(mCameraIndex);
+ }
+ }
+
+ /** Closes the camera releasing the resources it uses */
+ void closeCamera() {
+ mOpenRequested = false;
+ setCamera(null);
+ }
+
+ /**
+ * Sets the listener which will be notified of errors or other events in the camera
+ *
+ * @param listener The listener to notify
+ */
+ public void setListener(final CameraManagerListener listener) {
+ Assert.isMainThread();
+ mListener = listener;
+ if (!mIsHardwareAccelerationSupported && mListener != null) {
+ mListener.onCameraError(ERROR_HARDWARE_ACCELERATION_DISABLED, null);
+ }
+ }
+
+ public void takePicture(final float heightPercent, @NonNull final MediaCallback callback) {
+ Assert.checkState(!mTakingPicture);
+ Assert.isNotNull(callback);
+ mCameraPreview.setFocusable(false);
+ mFocusOverlayManager.cancelAutoFocus();
+ if (mCamera == null) {
+ // The caller should have checked isCameraAvailable first, but just in case, protect
+ // against a null camera by notifying the callback that taking the picture didn't work
+ callback.onMediaFailed(null);
+ return;
+ }
+ final Camera.PictureCallback jpegCallback =
+ new Camera.PictureCallback() {
+ @Override
+ public void onPictureTaken(final byte[] bytes, final Camera camera) {
+ mTakingPicture = false;
+ if (mCamera != camera) {
+ // This may happen if the camera was changed between front/back while the
+ // picture is being taken.
+ callback.onMediaInfo(MediaCallback.MEDIA_CAMERA_CHANGED);
+ return;
+ }
+
+ if (bytes == null) {
+ callback.onMediaInfo(MediaCallback.MEDIA_NO_DATA);
+ return;
+ }
+
+ final Camera.Size size = camera.getParameters().getPictureSize();
+ int width;
+ int height;
+ if (mRotation == 90 || mRotation == 270) {
+ // Is rotated, so swapping dimensions is desired
+ //noinspection SuspiciousNameCombination
+ width = size.height;
+ //noinspection SuspiciousNameCombination
+ height = size.width;
+ } else {
+ width = size.width;
+ height = size.height;
+ }
+ LogUtil.i(
+ "CameraManager.onPictureTaken", "taken picture size: " + bytes.length + " bytes");
+ new ImagePersistTask(
+ width, height, heightPercent, bytes, mCameraPreview.getContext(), callback)
+ .execute();
+ }
+ };
+
+ mTakingPicture = true;
+ try {
+ mCamera.takePicture(
+ // A shutter callback is required to enable shutter sound
+ DUMMY_SHUTTER_CALLBACK, null /* raw */, null /* postView */, jpegCallback);
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.takePicture", "RuntimeException in CameraManager.takePicture", e);
+ mTakingPicture = false;
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_TAKING_PICTURE, e);
+ }
+ }
+ }
+
+ /**
+ * Asynchronously releases a camera
+ *
+ * @param camera The camera to release
+ */
+ private void releaseCamera(final Camera camera) {
+ if (camera == null) {
+ return;
+ }
+
+ mFocusOverlayManager.onCameraReleased();
+
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(final Void... params) {
+ LogUtil.v("CameraManager.doInBackground", "Releasing camera " + mCameraIndex);
+ camera.release();
+ return null;
+ }
+ }.execute();
+ }
+
+ /** Updates the orientation of the camera to match the orientation of the device */
+ private void updateCameraOrientation() {
+ if (mCamera == null || mCameraPreview == null || mTakingPicture) {
+ return;
+ }
+
+ final WindowManager windowManager =
+ (WindowManager) mCameraPreview.getContext().getSystemService(Context.WINDOW_SERVICE);
+
+ int degrees = 0;
+ switch (windowManager.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ }
+
+ // The display orientation of the camera (this controls the preview image).
+ int orientation;
+
+ // The clockwise rotation angle relative to the orientation of the camera. This affects
+ // pictures returned by the camera in Camera.PictureCallback.
+ int rotation;
+ if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ orientation = (mCameraInfo.orientation + degrees) % 360;
+ rotation = orientation;
+ // compensate the mirror but only for orientation
+ orientation = (360 - orientation) % 360;
+ } else { // back-facing
+ orientation = (mCameraInfo.orientation - degrees + 360) % 360;
+ rotation = orientation;
+ }
+ mRotation = rotation;
+ try {
+ mCamera.setDisplayOrientation(orientation);
+ final Camera.Parameters params = mCamera.getParameters();
+ params.setRotation(rotation);
+ mCamera.setParameters(params);
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.updateCameraOrientation",
+ "RuntimeException in CameraManager.updateCameraOrientation",
+ e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_OPENING_CAMERA, e);
+ }
+ }
+ }
+
+ /** Sets the current camera, releasing any previously opened camera */
+ private void setCamera(final Camera camera) {
+ if (mCamera == camera) {
+ return;
+ }
+
+ releaseCamera(mCamera);
+ mCamera = camera;
+ tryShowPreview();
+ if (mListener != null) {
+ mListener.onCameraChanged();
+ }
+ }
+
+ /** Shows the preview if the camera is open and the preview is loaded */
+ private void tryShowPreview() {
+ if (mCameraPreview == null || mCamera == null) {
+ if (mOrientationHandler != null) {
+ mOrientationHandler.disable();
+ mOrientationHandler = null;
+ }
+ // releaseMediaRecorder(true /* cleanupFile */);
+ mFocusOverlayManager.onPreviewStopped();
+ return;
+ }
+ try {
+ mCamera.stopPreview();
+ updateCameraOrientation();
+
+ final Camera.Parameters params = mCamera.getParameters();
+ final Camera.Size pictureSize = chooseBestPictureSize();
+ final Camera.Size previewSize = chooseBestPreviewSize(pictureSize);
+ params.setPreviewSize(previewSize.width, previewSize.height);
+ params.setPictureSize(pictureSize.width, pictureSize.height);
+ logCameraSize("Setting preview size: ", previewSize);
+ logCameraSize("Setting picture size: ", pictureSize);
+ mCameraPreview.setSize(previewSize, mCameraInfo.orientation);
+ for (final String focusMode : params.getSupportedFocusModes()) {
+ if (TextUtils.equals(focusMode, Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ // Use continuous focus if available
+ params.setFocusMode(focusMode);
+ break;
+ }
+ }
+
+ mCamera.setParameters(params);
+ mCameraPreview.startPreview(mCamera);
+ mCamera.startPreview();
+ mCamera.setAutoFocusMoveCallback(
+ new Camera.AutoFocusMoveCallback() {
+ @Override
+ public void onAutoFocusMoving(final boolean start, final Camera camera) {
+ mFocusOverlayManager.onAutoFocusMoving(start);
+ }
+ });
+ mFocusOverlayManager.setParameters(mCamera.getParameters());
+ mFocusOverlayManager.setMirror(mCameraInfo.facing == CameraInfo.CAMERA_FACING_BACK);
+ mFocusOverlayManager.onPreviewStarted();
+ if (mOrientationHandler == null) {
+ mOrientationHandler = new OrientationHandler(mCameraPreview.getContext());
+ mOrientationHandler.enable();
+ }
+ } catch (final IOException e) {
+ LogUtil.e("CameraManager.tryShowPreview", "IOException in CameraManager.tryShowPreview", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_SHOWING_PREVIEW, e);
+ }
+ } catch (final RuntimeException e) {
+ LogUtil.e(
+ "CameraManager.tryShowPreview", "RuntimeException in CameraManager.tryShowPreview", e);
+ if (mListener != null) {
+ mListener.onCameraError(ERROR_SHOWING_PREVIEW, e);
+ }
+ }
+ }
+
+ public boolean isCameraAvailable() {
+ return mCamera != null && !mTakingPicture && mIsHardwareAccelerationSupported;
+ }
+
+ /**
+ * Choose the best picture size by trying to find a size close to the MmsConfig's max size, which
+ * is closest to the screen aspect ratio. In case of RCS conversation returns default size.
+ */
+ private Camera.Size chooseBestPictureSize() {
+ return mCamera.getParameters().getPictureSize();
+ }
+
+ /**
+ * Chose the best preview size based on the picture size. Try to find a size with the same aspect
+ * ratio and size as the picture if possible
+ */
+ private Camera.Size chooseBestPreviewSize(final Camera.Size pictureSize) {
+ final List<Camera.Size> sizes =
+ new ArrayList<Camera.Size>(mCamera.getParameters().getSupportedPreviewSizes());
+ final float aspectRatio = pictureSize.width / (float) pictureSize.height;
+ final int capturePixels = pictureSize.width * pictureSize.height;
+
+ // Sort the sizes so the best size is first
+ Collections.sort(
+ sizes,
+ new SizeComparator(Integer.MAX_VALUE, Integer.MAX_VALUE, aspectRatio, capturePixels));
+
+ return sizes.get(0);
+ }
+
+ private class OrientationHandler extends OrientationEventListener {
+ OrientationHandler(final Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onOrientationChanged(final int orientation) {
+ updateCameraOrientation();
+ }
+ }
+
+ private static class SizeComparator implements Comparator<Camera.Size> {
+ private static final int PREFER_LEFT = -1;
+ private static final int PREFER_RIGHT = 1;
+
+ // The max width/height for the preferred size. Integer.MAX_VALUE if no size limit
+ private final int mMaxWidth;
+ private final int mMaxHeight;
+
+ // The desired aspect ratio
+ private final float mTargetAspectRatio;
+
+ // The desired size (width x height) to try to match
+ private final int mTargetPixels;
+
+ public SizeComparator(
+ final int maxWidth,
+ final int maxHeight,
+ final float targetAspectRatio,
+ final int targetPixels) {
+ mMaxWidth = maxWidth;
+ mMaxHeight = maxHeight;
+ mTargetAspectRatio = targetAspectRatio;
+ mTargetPixels = targetPixels;
+ }
+
+ /**
+ * Returns a negative value if left is a better choice than right, or a positive value if right
+ * is a better choice is better than left. 0 if they are equal
+ */
+ @Override
+ public int compare(final Camera.Size left, final Camera.Size right) {
+ // If one size is less than the max size prefer it over the other
+ if ((left.width <= mMaxWidth && left.height <= mMaxHeight)
+ != (right.width <= mMaxWidth && right.height <= mMaxHeight)) {
+ return left.width <= mMaxWidth ? PREFER_LEFT : PREFER_RIGHT;
+ }
+
+ // If one is closer to the target aspect ratio, prefer it.
+ final float leftAspectRatio = left.width / (float) left.height;
+ final float rightAspectRatio = right.width / (float) right.height;
+ final float leftAspectRatioDiff = Math.abs(leftAspectRatio - mTargetAspectRatio);
+ final float rightAspectRatioDiff = Math.abs(rightAspectRatio - mTargetAspectRatio);
+ if (leftAspectRatioDiff != rightAspectRatioDiff) {
+ return (leftAspectRatioDiff - rightAspectRatioDiff) < 0 ? PREFER_LEFT : PREFER_RIGHT;
+ }
+
+ // At this point they have the same aspect ratio diff and are either both bigger
+ // than the max size or both smaller than the max size, so prefer the one closest
+ // to target size
+ final int leftDiff = Math.abs((left.width * left.height) - mTargetPixels);
+ final int rightDiff = Math.abs((right.width * right.height) - mTargetPixels);
+ return leftDiff - rightDiff;
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void autoFocus() {
+ if (mCamera == null) {
+ return;
+ }
+
+ try {
+ mCamera.autoFocus(
+ new Camera.AutoFocusCallback() {
+ @Override
+ public void onAutoFocus(final boolean success, final Camera camera) {
+ mFocusOverlayManager.onAutoFocus(success, false /* shutterDown */);
+ }
+ });
+ } catch (final RuntimeException e) {
+ LogUtil.e("CameraManager.autoFocus", "RuntimeException in CameraManager.autoFocus", e);
+ // If autofocus fails, the camera should have called the callback with success=false,
+ // but some throw an exception here
+ mFocusOverlayManager.onAutoFocus(false /*success*/, false /*shutterDown*/);
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void cancelAutoFocus() {
+ if (mCamera == null) {
+ return;
+ }
+ try {
+ mCamera.cancelAutoFocus();
+ } catch (final RuntimeException e) {
+ // Ignore
+ LogUtil.e(
+ "CameraManager.cancelAutoFocus", "RuntimeException in CameraManager.cancelAutoFocus", e);
+ }
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public boolean capture() {
+ return false;
+ }
+
+ @Override // From FocusOverlayManager.Listener
+ public void setFocusParameters() {
+ if (mCamera == null) {
+ return;
+ }
+ try {
+ final Camera.Parameters parameters = mCamera.getParameters();
+ parameters.setFocusMode(mFocusOverlayManager.getFocusMode());
+ if (parameters.getMaxNumFocusAreas() > 0) {
+ // Don't set focus areas (even to null) if focus areas aren't supported, camera may
+ // crash
+ parameters.setFocusAreas(mFocusOverlayManager.getFocusAreas());
+ }
+ parameters.setMeteringAreas(mFocusOverlayManager.getMeteringAreas());
+ mCamera.setParameters(parameters);
+ } catch (final RuntimeException e) {
+ // This occurs when the device is out of space or when the camera is locked
+ LogUtil.e(
+ "CameraManager.setFocusParameters",
+ "RuntimeException in CameraManager setFocusParameters");
+ }
+ }
+
+ public void resetPreview() {
+ mCamera.startPreview();
+ if (mCameraPreview != null) {
+ mCameraPreview.setFocusable(true);
+ }
+ }
+
+ private void logCameraSize(final String prefix, final Camera.Size size) {
+ // Log the camera size and aspect ratio for help when examining bug reports for camera
+ // failures
+ LogUtil.i(
+ "CameraManager.logCameraSize",
+ prefix + size.width + "x" + size.height + " (" + (size.width / (float) size.height) + ")");
+ }
+
+ @VisibleForTesting
+ public void resetCameraManager() {
+ sInstance = null;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/CameraPreview.java b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
new file mode 100644
index 000000000..6581ad67b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/CameraPreview.java
@@ -0,0 +1,177 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.hardware.Camera;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.PermissionsUtil;
+import java.io.IOException;
+
+/**
+ * Contains shared code for SoftwareCameraPreview and HardwareCameraPreview, cannot use inheritance
+ * because those classes must inherit from separate Views, so those classes delegate calls to this
+ * helper class. Specifics for each implementation are in CameraPreviewHost
+ */
+public class CameraPreview {
+ /** Implemented by the camera for rendering. */
+ public interface CameraPreviewHost {
+ View getView();
+
+ boolean isValid();
+
+ void startPreview(final Camera camera) throws IOException;
+
+ void onCameraPermissionGranted();
+
+ void setShown();
+ }
+
+ private int mCameraWidth = -1;
+ private int mCameraHeight = -1;
+ private boolean mTabHasBeenShown = false;
+ private OnTouchListener mListener;
+
+ private final CameraPreviewHost mHost;
+
+ public CameraPreview(final CameraPreviewHost host) {
+ Assert.isNotNull(host);
+ Assert.isNotNull(host.getView());
+ mHost = host;
+ }
+
+ // This is set when the tab is actually selected.
+ public void setShown() {
+ mTabHasBeenShown = true;
+ maybeOpenCamera();
+ }
+
+ // Opening camera is very expensive. Most of the ANR reports seem to be related to the camera.
+ // So we delay until the camera is actually needed. See b/23287938
+ private void maybeOpenCamera() {
+ boolean visible = mHost.getView().getVisibility() == View.VISIBLE;
+ if (mTabHasBeenShown && visible && PermissionsUtil.hasCameraPermissions(getContext())) {
+ CameraManager.get().openCamera();
+ }
+ }
+
+ public void setSize(final Camera.Size size, final int orientation) {
+ switch (orientation) {
+ case 0:
+ case 180:
+ mCameraWidth = size.width;
+ mCameraHeight = size.height;
+ break;
+ case 90:
+ case 270:
+ default:
+ mCameraWidth = size.height;
+ mCameraHeight = size.width;
+ }
+ mHost.getView().requestLayout();
+ }
+
+ public int getWidthMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
+ if (mCameraHeight >= 0) {
+ final int width = View.MeasureSpec.getSize(widthMeasureSpec);
+ return MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
+ } else {
+ return widthMeasureSpec;
+ }
+ }
+
+ public int getHeightMeasureSpec(final int widthMeasureSpec, final int heightMeasureSpec) {
+ if (mCameraHeight >= 0) {
+ final int orientation = getContext().getResources().getConfiguration().orientation;
+ final int width = View.MeasureSpec.getSize(widthMeasureSpec);
+ final float aspectRatio = (float) mCameraWidth / (float) mCameraHeight;
+ int height;
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ height = (int) (width * aspectRatio);
+ } else {
+ height = (int) (width / aspectRatio);
+ }
+ return View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
+ } else {
+ return heightMeasureSpec;
+ }
+ }
+
+ // onVisibilityChanged is set to Visible when the tab is _created_,
+ // which may be when the user is viewing a different tab.
+ public void onVisibilityChanged(final int visibility) {
+ if (PermissionsUtil.hasCameraPermissions(getContext())) {
+ if (visibility == View.VISIBLE) {
+ maybeOpenCamera();
+ } else {
+ CameraManager.get().closeCamera();
+ }
+ }
+ }
+
+ public Context getContext() {
+ return mHost.getView().getContext();
+ }
+
+ public void setOnTouchListener(final View.OnTouchListener listener) {
+ mListener = listener;
+ mHost.getView().setOnTouchListener(listener);
+ }
+
+ public void setFocusable(boolean focusable) {
+ mHost.getView().setOnTouchListener(focusable ? mListener : null);
+ }
+
+ public int getHeight() {
+ return mHost.getView().getHeight();
+ }
+
+ public void onAttachedToWindow() {
+ maybeOpenCamera();
+ }
+
+ public void onDetachedFromWindow() {
+ CameraManager.get().closeCamera();
+ }
+
+ public void onRestoreInstanceState() {
+ maybeOpenCamera();
+ }
+
+ public void onCameraPermissionGranted() {
+ maybeOpenCamera();
+ }
+
+ /** @return True if the view is valid and prepared for the camera to start showing the preview */
+ public boolean isValid() {
+ return mHost.isValid();
+ }
+
+ /**
+ * Starts the camera preview on the current surface. Abstracts out the differences in API from the
+ * CameraManager
+ *
+ * @throws IOException Which is caught by the CameraManager to display an error
+ */
+ public void startPreview(final Camera camera) throws IOException {
+ mHost.startPreview(camera);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java b/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java
new file mode 100644
index 000000000..c0d61f3f8
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java
@@ -0,0 +1,125 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.TextureView;
+import android.view.View;
+import java.io.IOException;
+
+/**
+ * A hardware accelerated preview texture for the camera. This is the preferred CameraPreview
+ * because it animates smoother. When hardware acceleration isn't available, SoftwareCameraPreview
+ * is used.
+ *
+ * <p>There is a significant amount of duplication between HardwareCameraPreview and
+ * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
+ * implementations of the shared methods are delegated to CameraPreview
+ */
+public class HardwareCameraPreview extends TextureView implements CameraPreview.CameraPreviewHost {
+ private CameraPreview mPreview;
+
+ public HardwareCameraPreview(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ mPreview = new CameraPreview(this);
+ setSurfaceTextureListener(
+ new SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(
+ final SurfaceTexture surfaceTexture, final int i, final int i2) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(
+ final SurfaceTexture surfaceTexture, final int i, final int i2) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(final SurfaceTexture surfaceTexture) {
+ CameraManager.get().setSurface(null);
+ return true;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(final SurfaceTexture surfaceTexture) {
+ CameraManager.get().setSurface(mPreview);
+ }
+ });
+ }
+
+ @Override
+ public void setShown() {
+ mPreview.setShown();
+ }
+
+ @Override
+ protected void onVisibilityChanged(final View changedView, final int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mPreview.onVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPreview.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPreview.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ super.onRestoreInstanceState(state);
+ mPreview.onRestoreInstanceState();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public boolean isValid() {
+ return getSurfaceTexture() != null;
+ }
+
+ @Override
+ public void startPreview(final Camera camera) throws IOException {
+ camera.setPreviewTexture(getSurfaceTexture());
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ mPreview.onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
new file mode 100644
index 000000000..150009495
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/ImagePersistTask.java
@@ -0,0 +1,143 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.v4.content.FileProvider;
+import com.android.dialer.callcomposer.camera.exif.ExifInterface;
+import com.android.dialer.callcomposer.camera.exif.ExifTag;
+import com.android.dialer.callcomposer.util.CopyAndResizeImageTask;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.constants.Constants;
+import com.android.dialer.util.DialerUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Persisting image routine. */
+@TargetApi(VERSION_CODES.M)
+public class ImagePersistTask extends FallibleAsyncTask<Void, Void, Uri> {
+ private int mWidth;
+ private int mHeight;
+ private final float mHeightPercent;
+ private final byte[] mBytes;
+ private final Context mContext;
+ private final CameraManager.MediaCallback mCallback;
+
+ ImagePersistTask(
+ final int width,
+ final int height,
+ final float heightPercent,
+ final byte[] bytes,
+ final Context context,
+ final CameraManager.MediaCallback callback) {
+ Assert.checkArgument(heightPercent >= 0 && heightPercent <= 1);
+ Assert.isNotNull(bytes);
+ Assert.isNotNull(context);
+ Assert.isNotNull(callback);
+ mWidth = width;
+ mHeight = height;
+ mHeightPercent = heightPercent;
+ mBytes = bytes;
+ mContext = context;
+ mCallback = callback;
+ }
+
+ @Override
+ protected Uri doInBackgroundFallible(final Void... params) throws Exception {
+ File outputFile = DialerUtils.createShareableFile(mContext);
+
+ try (OutputStream outputStream = new FileOutputStream(outputFile)) {
+ if (mHeightPercent != 1.0f) {
+ writeClippedBitmap(outputStream);
+ } else {
+ outputStream.write(mBytes, 0, mBytes.length);
+ }
+ }
+
+ return FileProvider.getUriForFile(
+ mContext, Constants.get().getFileProviderAuthority(), outputFile);
+ }
+
+ @Override
+ protected void onPostExecute(FallibleTaskResult<Uri> result) {
+ if (result.isFailure()) {
+ mCallback.onMediaFailed(new Exception("Persisting image failed", result.getThrowable()));
+ } else {
+ mCallback.onMediaReady(result.getResult(), "image/jpeg", mWidth, mHeight);
+ }
+ }
+
+ private void writeClippedBitmap(OutputStream outputStream) throws IOException {
+ int orientation = android.media.ExifInterface.ORIENTATION_UNDEFINED;
+ final ExifInterface exifInterface = new ExifInterface();
+ try {
+ exifInterface.readExif(mBytes);
+ final Integer orientationValue = exifInterface.getTagIntValue(ExifInterface.TAG_ORIENTATION);
+ if (orientationValue != null) {
+ orientation = orientationValue.intValue();
+ }
+ } catch (final IOException e) {
+ // Couldn't get exif tags, not the end of the world
+ }
+ Bitmap bitmap = BitmapFactory.decodeByteArray(mBytes, 0, mBytes.length);
+ final int clippedWidth;
+ final int clippedHeight;
+ if (ExifInterface.getOrientationParams(orientation).invertDimensions) {
+ Assert.checkState(mWidth == bitmap.getHeight());
+ Assert.checkState(mHeight == bitmap.getWidth());
+ clippedWidth = (int) (mHeight * mHeightPercent);
+ clippedHeight = mWidth;
+ } else {
+ Assert.checkState(mWidth == bitmap.getWidth());
+ Assert.checkState(mHeight == bitmap.getHeight());
+ clippedWidth = mWidth;
+ clippedHeight = (int) (mHeight * mHeightPercent);
+ }
+ final int offsetTop = (bitmap.getHeight() - clippedHeight) / 2;
+ final int offsetLeft = (bitmap.getWidth() - clippedWidth) / 2;
+ mWidth = clippedWidth;
+ mHeight = clippedHeight;
+ Bitmap clippedBitmap =
+ Bitmap.createBitmap(clippedWidth, clippedHeight, Bitmap.Config.ARGB_8888);
+ clippedBitmap.setDensity(bitmap.getDensity());
+ final Canvas clippedBitmapCanvas = new Canvas(clippedBitmap);
+ final Matrix matrix = new Matrix();
+ matrix.postTranslate(-offsetLeft, -offsetTop);
+ clippedBitmapCanvas.drawBitmap(bitmap, matrix, null /* paint */);
+ clippedBitmapCanvas.save();
+ clippedBitmap = CopyAndResizeImageTask.resizeForEnrichedCalling(clippedBitmap);
+ // EXIF data can take a big chunk of the file size and is often cleared by the
+ // carrier, only store orientation since that's critical
+ final ExifTag orientationTag = exifInterface.getTag(ExifInterface.TAG_ORIENTATION);
+ exifInterface.clearExif();
+ exifInterface.setTag(orientationTag);
+ exifInterface.writeExif(clippedBitmap, outputStream);
+
+ clippedBitmap.recycle();
+ bitmap.recycle();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java b/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java
new file mode 100644
index 000000000..fe2c600df
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java
@@ -0,0 +1,120 @@
+/*
+ * 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.callcomposer.camera;
+
+import android.content.Context;
+import android.hardware.Camera;
+import android.os.Parcelable;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import java.io.IOException;
+
+/**
+ * A software rendered preview surface for the camera. This renders slower and causes more jank, so
+ * HardwareCameraPreview is preferred if possible.
+ *
+ * <p>There is a significant amount of duplication between HardwareCameraPreview and
+ * SoftwareCameraPreview which we can't easily share due to a lack of multiple inheritance, The
+ * implementations of the shared methods are delegated to CameraPreview
+ */
+public class SoftwareCameraPreview extends SurfaceView implements CameraPreview.CameraPreviewHost {
+ private final CameraPreview mPreview;
+
+ public SoftwareCameraPreview(final Context context) {
+ super(context);
+ mPreview = new CameraPreview(this);
+ getHolder()
+ .addCallback(
+ new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(final SurfaceHolder surfaceHolder) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void surfaceChanged(
+ final SurfaceHolder surfaceHolder,
+ final int format,
+ final int width,
+ final int height) {
+ CameraManager.get().setSurface(mPreview);
+ }
+
+ @Override
+ public void surfaceDestroyed(final SurfaceHolder surfaceHolder) {
+ CameraManager.get().setSurface(null);
+ }
+ });
+ }
+
+ @Override
+ public void setShown() {
+ mPreview.setShown();
+ }
+
+ @Override
+ protected void onVisibilityChanged(final View changedView, final int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+ mPreview.onVisibilityChanged(visibility);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mPreview.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mPreview.onAttachedToWindow();
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ super.onRestoreInstanceState(state);
+ mPreview.onRestoreInstanceState();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ widthMeasureSpec = mPreview.getWidthMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ heightMeasureSpec = mPreview.getHeightMeasureSpec(widthMeasureSpec, heightMeasureSpec);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ @Override
+ public View getView() {
+ return this;
+ }
+
+ @Override
+ public boolean isValid() {
+ return getHolder() != null;
+ }
+
+ @Override
+ public void startPreview(final Camera camera) throws IOException {
+ camera.setPreviewDisplay(getHolder());
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ mPreview.onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml b/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml
new file mode 100644
index 000000000..77ef22295
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.callcomposer.camera.camerafocus"/> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java
new file mode 100644
index 000000000..234cf5388
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.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.dialer.callcomposer.camera.camerafocus;
+
+/** Methods needed by the CameraPreview in order communicate camera events. */
+public interface FocusIndicator {
+ void showStart();
+
+ void showSuccess(boolean timeout);
+
+ void showFail(boolean timeout);
+
+ void clear();
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java
new file mode 100644
index 000000000..1c5ac380c
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java
@@ -0,0 +1,482 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.hardware.Camera.Area;
+import android.hardware.Camera.Parameters;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that handles everything about focus in still picture mode. This also handles the metering
+ * area because it is the same as focus area.
+ *
+ * <p>The test cases: (1) The camera has continuous autofocus. Move the camera. Take a picture when
+ * CAF is not in progress. (2) The camera has continuous autofocus. Move the camera. Take a picture
+ * when CAF is in progress. (3) The camera has face detection. Point the camera at some faces. Hold
+ * the shutter. Release to take a picture. (4) The camera has face detection. Point the camera at
+ * some faces. Single tap the shutter to take a picture. (5) The camera has autofocus. Single tap
+ * the shutter to take a picture. (6) The camera has autofocus. Hold the shutter. Release to take a
+ * picture. (7) The camera has no autofocus. Single tap the shutter and take a picture. (8) The
+ * camera has autofocus and supports focus area. Touch the screen to trigger autofocus. Take a
+ * picture. (9) The camera has autofocus and supports focus area. Touch the screen to trigger
+ * autofocus. Wait until it times out. (10) The camera has no autofocus and supports metering area.
+ * Touch the screen to change metering area.
+ */
+public class FocusOverlayManager {
+ private static final String TRUE = "true";
+ private static final String AUTO_EXPOSURE_LOCK_SUPPORTED = "auto-exposure-lock-supported";
+ private static final String AUTO_WHITE_BALANCE_LOCK_SUPPORTED =
+ "auto-whitebalance-lock-supported";
+
+ private static final int RESET_TOUCH_FOCUS = 0;
+ private static final int RESET_TOUCH_FOCUS_DELAY = 3000;
+
+ private int mState = STATE_IDLE;
+ private static final int STATE_IDLE = 0; // Focus is not active.
+ private static final int STATE_FOCUSING = 1; // Focus is in progress.
+ // Focus is in progress and the camera should take a picture after focus finishes.
+ private static final int STATE_FOCUSING_SNAP_ON_FINISH = 2;
+ private static final int STATE_SUCCESS = 3; // Focus finishes and succeeds.
+ private static final int STATE_FAIL = 4; // Focus finishes and fails.
+
+ private boolean mInitialized;
+ private boolean mFocusAreaSupported;
+ private boolean mMeteringAreaSupported;
+ private boolean mLockAeAwbNeeded;
+ private boolean mAeAwbLock;
+ private Matrix mMatrix;
+
+ private PieRenderer mPieRenderer;
+
+ private int mPreviewWidth; // The width of the preview frame layout.
+ private int mPreviewHeight; // The height of the preview frame layout.
+ private boolean mMirror; // true if the camera is front-facing.
+ private List<Area> mFocusArea; // focus area in driver format
+ private List<Area> mMeteringArea; // metering area in driver format
+ private String mFocusMode;
+ private Parameters mParameters;
+ private Handler mHandler;
+ private Listener mListener;
+
+ /** Listener used for the focus indicator to communicate back to the camera. */
+ public interface Listener {
+ void autoFocus();
+
+ void cancelAutoFocus();
+
+ boolean capture();
+
+ void setFocusParameters();
+ }
+
+ private class MainHandler extends Handler {
+ public MainHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case RESET_TOUCH_FOCUS:
+ {
+ cancelAutoFocus();
+ break;
+ }
+ }
+ }
+ }
+
+ public FocusOverlayManager(Listener listener, Looper looper) {
+ mHandler = new MainHandler(looper);
+ mMatrix = new Matrix();
+ mListener = listener;
+ }
+
+ public void setFocusRenderer(PieRenderer renderer) {
+ mPieRenderer = renderer;
+ mInitialized = (mMatrix != null);
+ }
+
+ public void setParameters(Parameters parameters) {
+ // parameters can only be null when onConfigurationChanged is called
+ // before camera is open. We will just return in this case, because
+ // parameters will be set again later with the right parameters after
+ // camera is open.
+ if (parameters == null) {
+ return;
+ }
+ mParameters = parameters;
+ mFocusAreaSupported = isFocusAreaSupported(parameters);
+ mMeteringAreaSupported = isMeteringAreaSupported(parameters);
+ mLockAeAwbNeeded =
+ (isAutoExposureLockSupported(mParameters) || isAutoWhiteBalanceLockSupported(mParameters));
+ }
+
+ public void setPreviewSize(int previewWidth, int previewHeight) {
+ if (mPreviewWidth != previewWidth || mPreviewHeight != previewHeight) {
+ mPreviewWidth = previewWidth;
+ mPreviewHeight = previewHeight;
+ setMatrix();
+ }
+ }
+
+ public void setMirror(boolean mirror) {
+ mMirror = mirror;
+ setMatrix();
+ }
+
+ private void setMatrix() {
+ if (mPreviewWidth != 0 && mPreviewHeight != 0) {
+ Matrix matrix = new Matrix();
+ prepareMatrix(matrix, mMirror, mPreviewWidth, mPreviewHeight);
+ // In face detection, the matrix converts the driver coordinates to UI
+ // coordinates. In tap focus, the inverted matrix converts the UI
+ // coordinates to driver coordinates.
+ matrix.invert(mMatrix);
+ mInitialized = (mPieRenderer != null);
+ }
+ }
+
+ private void lockAeAwbIfNeeded() {
+ if (mLockAeAwbNeeded && !mAeAwbLock) {
+ mAeAwbLock = true;
+ mListener.setFocusParameters();
+ }
+ }
+
+ public void onAutoFocus(boolean focused, boolean shutterButtonPressed) {
+ if (mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ // Take the picture no matter focus succeeds or fails. No need
+ // to play the AF sound if we're about to play the shutter
+ // sound.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ capture();
+ } else if (mState == STATE_FOCUSING) {
+ // This happens when (1) user is half-pressing the focus key or
+ // (2) touch focus is triggered. Play the focus tone. Do not
+ // take the picture now.
+ if (focused) {
+ mState = STATE_SUCCESS;
+ } else {
+ mState = STATE_FAIL;
+ }
+ updateFocusUI();
+ // If this is triggered by touch focus, cancel focus after a
+ // while.
+ if (mFocusArea != null) {
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ if (shutterButtonPressed) {
+ // Lock AE & AWB so users can half-press shutter and recompose.
+ lockAeAwbIfNeeded();
+ }
+ } else if (mState == STATE_IDLE) {
+ // User has released the focus key before focus completes.
+ // Do nothing.
+ }
+ }
+
+ public void onAutoFocusMoving(boolean moving) {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Ignore if we have requested autofocus. This method only handles
+ // continuous autofocus.
+ if (mState != STATE_IDLE) {
+ return;
+ }
+
+ if (moving) {
+ mPieRenderer.showStart();
+ } else {
+ mPieRenderer.showSuccess(true);
+ }
+ }
+
+ private void initializeFocusAreas(
+ int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) {
+ if (mFocusArea == null) {
+ mFocusArea = new ArrayList<>();
+ mFocusArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ calculateTapArea(
+ focusWidth, focusHeight, 1f, x, y, previewWidth, previewHeight, mFocusArea.get(0).rect);
+ }
+
+ private void initializeMeteringAreas(
+ int focusWidth, int focusHeight, int x, int y, int previewWidth, int previewHeight) {
+ if (mMeteringArea == null) {
+ mMeteringArea = new ArrayList<>();
+ mMeteringArea.add(new Area(new Rect(), 1));
+ }
+
+ // Convert the coordinates to driver format.
+ // AE area is bigger because exposure is sensitive and
+ // easy to over- or underexposure if area is too small.
+ calculateTapArea(
+ focusWidth,
+ focusHeight,
+ 1.5f,
+ x,
+ y,
+ previewWidth,
+ previewHeight,
+ mMeteringArea.get(0).rect);
+ }
+
+ public void onSingleTapUp(int x, int y) {
+ if (!mInitialized || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ return;
+ }
+
+ // Let users be able to cancel previous touch focus.
+ if ((mFocusArea != null)
+ && (mState == STATE_FOCUSING || mState == STATE_SUCCESS || mState == STATE_FAIL)) {
+ cancelAutoFocus();
+ }
+ // Initialize variables.
+ int focusWidth = mPieRenderer.getSize();
+ int focusHeight = mPieRenderer.getSize();
+ if (focusWidth == 0 || mPieRenderer.getWidth() == 0 || mPieRenderer.getHeight() == 0) {
+ return;
+ }
+ int previewWidth = mPreviewWidth;
+ int previewHeight = mPreviewHeight;
+ // Initialize mFocusArea.
+ if (mFocusAreaSupported) {
+ initializeFocusAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+ }
+ // Initialize mMeteringArea.
+ if (mMeteringAreaSupported) {
+ initializeMeteringAreas(focusWidth, focusHeight, x, y, previewWidth, previewHeight);
+ }
+
+ // Use margin to set the focus indicator to the touched area.
+ mPieRenderer.setFocus(x, y);
+
+ // Set the focus area and metering area.
+ mListener.setFocusParameters();
+ if (mFocusAreaSupported) {
+ autoFocus();
+ } else { // Just show the indicator in all other cases.
+ updateFocusUI();
+ // Reset the metering area in 3 seconds.
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ mHandler.sendEmptyMessageDelayed(RESET_TOUCH_FOCUS, RESET_TOUCH_FOCUS_DELAY);
+ }
+ }
+
+ public void onPreviewStarted() {
+ mState = STATE_IDLE;
+ }
+
+ public void onPreviewStopped() {
+ // If auto focus was in progress, it would have been stopped.
+ mState = STATE_IDLE;
+ resetTouchFocus();
+ updateFocusUI();
+ }
+
+ public void onCameraReleased() {
+ onPreviewStopped();
+ }
+
+ private void autoFocus() {
+ LogUtil.v("FocusOverlayManager.autoFocus", "Start autofocus.");
+ mListener.autoFocus();
+ mState = STATE_FOCUSING;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ public void cancelAutoFocus() {
+ LogUtil.v("FocusOverlayManager.cancelAutoFocus", "Cancel autofocus.");
+
+ // Reset the tap area before calling mListener.cancelAutofocus.
+ // Otherwise, focus mode stays at auto and the tap area passed to the
+ // driver is not reset.
+ resetTouchFocus();
+ mListener.cancelAutoFocus();
+ mState = STATE_IDLE;
+ updateFocusUI();
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+
+ private void capture() {
+ if (mListener.capture()) {
+ mState = STATE_IDLE;
+ mHandler.removeMessages(RESET_TOUCH_FOCUS);
+ }
+ }
+
+ public String getFocusMode() {
+ List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
+
+ if (mFocusAreaSupported && mFocusArea != null) {
+ // Always use autofocus in tap-to-focus.
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = Parameters.FOCUS_MODE_CONTINUOUS_PICTURE;
+ }
+
+ if (!isSupported(mFocusMode, supportedFocusModes)) {
+ // For some reasons, the driver does not support the current
+ // focus mode. Fall back to auto.
+ if (isSupported(Parameters.FOCUS_MODE_AUTO, mParameters.getSupportedFocusModes())) {
+ mFocusMode = Parameters.FOCUS_MODE_AUTO;
+ } else {
+ mFocusMode = mParameters.getFocusMode();
+ }
+ }
+ return mFocusMode;
+ }
+
+ public List<Area> getFocusAreas() {
+ return mFocusArea;
+ }
+
+ public List<Area> getMeteringAreas() {
+ return mMeteringArea;
+ }
+
+ private void updateFocusUI() {
+ if (!mInitialized) {
+ return;
+ }
+ FocusIndicator focusIndicator = mPieRenderer;
+
+ if (mState == STATE_IDLE) {
+ if (mFocusArea == null) {
+ focusIndicator.clear();
+ } else {
+ // Users touch on the preview and the indicator represents the
+ // metering area. Either focus area is not supported or
+ // autoFocus call is not required.
+ focusIndicator.showStart();
+ }
+ } else if (mState == STATE_FOCUSING || mState == STATE_FOCUSING_SNAP_ON_FINISH) {
+ focusIndicator.showStart();
+ } else {
+ if (Parameters.FOCUS_MODE_CONTINUOUS_PICTURE.equals(mFocusMode)) {
+ // TODO: check HAL behavior and decide if this can be removed.
+ focusIndicator.showSuccess(false);
+ } else if (mState == STATE_SUCCESS) {
+ focusIndicator.showSuccess(false);
+ } else if (mState == STATE_FAIL) {
+ focusIndicator.showFail(false);
+ }
+ }
+ }
+
+ private void resetTouchFocus() {
+ if (!mInitialized) {
+ return;
+ }
+
+ // Put focus indicator to the center. clear reset position
+ mPieRenderer.clear();
+
+ mFocusArea = null;
+ mMeteringArea = null;
+ }
+
+ private void calculateTapArea(
+ int focusWidth,
+ int focusHeight,
+ float areaMultiple,
+ int x,
+ int y,
+ int previewWidth,
+ int previewHeight,
+ Rect rect) {
+ int areaWidth = (int) (focusWidth * areaMultiple);
+ int areaHeight = (int) (focusHeight * areaMultiple);
+ final int maxW = previewWidth - areaWidth;
+ int left = maxW > 0 ? clamp(x - areaWidth / 2, 0, maxW) : 0;
+ final int maxH = previewHeight - areaHeight;
+ int top = maxH > 0 ? clamp(y - areaHeight / 2, 0, maxH) : 0;
+
+ RectF rectF = new RectF(left, top, left + areaWidth, top + areaHeight);
+ mMatrix.mapRect(rectF);
+ rectFToRect(rectF, rect);
+ }
+
+ private int clamp(int x, int min, int max) {
+ Assert.checkArgument(max >= min);
+ if (x > max) {
+ return max;
+ }
+ if (x < min) {
+ return min;
+ }
+ return x;
+ }
+
+ private boolean isAutoExposureLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_EXPOSURE_LOCK_SUPPORTED));
+ }
+
+ private boolean isAutoWhiteBalanceLockSupported(Parameters params) {
+ return TRUE.equals(params.get(AUTO_WHITE_BALANCE_LOCK_SUPPORTED));
+ }
+
+ private boolean isSupported(String value, List<String> supported) {
+ return supported != null && supported.indexOf(value) >= 0;
+ }
+
+ private boolean isMeteringAreaSupported(Parameters params) {
+ return params.getMaxNumMeteringAreas() > 0;
+ }
+
+ private boolean isFocusAreaSupported(Parameters params) {
+ return (params.getMaxNumFocusAreas() > 0
+ && isSupported(Parameters.FOCUS_MODE_AUTO, params.getSupportedFocusModes()));
+ }
+
+ private void prepareMatrix(Matrix matrix, boolean mirror, int viewWidth, int viewHeight) {
+ // Need mirror for front camera.
+ matrix.setScale(mirror ? -1 : 1, 1);
+ // Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
+ // UI coordinates range from (0, 0) to (width, height).
+ matrix.postScale(viewWidth / 2000f, viewHeight / 2000f);
+ matrix.postTranslate(viewWidth / 2f, viewHeight / 2f);
+ }
+
+ private void rectFToRect(RectF rectF, Rect rect) {
+ rect.left = Math.round(rectF.left);
+ rect.top = Math.round(rectF.top);
+ rect.right = Math.round(rectF.right);
+ rect.bottom = Math.round(rectF.bottom);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java b/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java
new file mode 100644
index 000000000..4a3b522bb
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java
@@ -0,0 +1,97 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+
+/** Abstract class that all Camera overlays should implement. */
+public abstract class OverlayRenderer implements RenderOverlay.Renderer {
+
+ protected RenderOverlay mOverlay;
+
+ private int mLeft;
+ private int mTop;
+ private int mRight;
+ private int mBottom;
+ private boolean mVisible;
+
+ public void setVisible(boolean vis) {
+ mVisible = vis;
+ update();
+ }
+
+ public boolean isVisible() {
+ return mVisible;
+ }
+
+ // default does not handle touch
+ @Override
+ public boolean handlesTouch() {
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ return false;
+ }
+
+ public abstract void onDraw(Canvas canvas);
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mVisible) {
+ onDraw(canvas);
+ }
+ }
+
+ @Override
+ public void setOverlay(RenderOverlay overlay) {
+ mOverlay = overlay;
+ }
+
+ @Override
+ public void layout(int left, int top, int right, int bottom) {
+ mLeft = left;
+ mRight = right;
+ mTop = top;
+ mBottom = bottom;
+ }
+
+ protected Context getContext() {
+ if (mOverlay != null) {
+ return mOverlay.getContext();
+ } else {
+ return null;
+ }
+ }
+
+ public int getWidth() {
+ return mRight - mLeft;
+ }
+
+ public int getHeight() {
+ return mBottom - mTop;
+ }
+
+ protected void update() {
+ if (mOverlay != null) {
+ mOverlay.update();
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java b/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java
new file mode 100644
index 000000000..86f69c0ae
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java
@@ -0,0 +1,179 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.drawable.Drawable;
+import java.util.List;
+
+/** Pie menu item. */
+public class PieItem {
+
+ /** Listener to detect pie item clicks. */
+ public interface OnClickListener {
+ void onClick(PieItem item);
+ }
+
+ private Drawable mDrawable;
+ private int level;
+ private float mCenter;
+ private float start;
+ private float sweep;
+ private float animate;
+ private int inner;
+ private int outer;
+ private boolean mSelected;
+ private boolean mEnabled;
+ private List<PieItem> mItems;
+ private Path mPath;
+ private OnClickListener mOnClickListener;
+ private float mAlpha;
+
+ // Gray out the view when disabled
+ private static final float ENABLED_ALPHA = 1;
+ private static final float DISABLED_ALPHA = (float) 0.3;
+
+ public PieItem(Drawable drawable, int level) {
+ mDrawable = drawable;
+ this.level = level;
+ setAlpha(1f);
+ mEnabled = true;
+ setAnimationAngle(getAnimationAngle());
+ start = -1;
+ mCenter = -1;
+ }
+
+ public boolean hasItems() {
+ return mItems != null;
+ }
+
+ public List<PieItem> getItems() {
+ return mItems;
+ }
+
+ public void setPath(Path p) {
+ mPath = p;
+ }
+
+ public Path getPath() {
+ return mPath;
+ }
+
+ public void setAlpha(float alpha) {
+ mAlpha = alpha;
+ mDrawable.setAlpha((int) (255 * alpha));
+ }
+
+ public void setAnimationAngle(float a) {
+ animate = a;
+ }
+
+ private float getAnimationAngle() {
+ return animate;
+ }
+
+ public void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ if (mEnabled) {
+ setAlpha(ENABLED_ALPHA);
+ } else {
+ setAlpha(DISABLED_ALPHA);
+ }
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public void setSelected(boolean s) {
+ mSelected = s;
+ }
+
+ public boolean isSelected() {
+ return mSelected;
+ }
+
+ public int getLevel() {
+ return level;
+ }
+
+ public void setGeometry(float st, float sw, int inside, int outside) {
+ start = st;
+ sweep = sw;
+ inner = inside;
+ outer = outside;
+ }
+
+ public float getCenter() {
+ return mCenter;
+ }
+
+ public float getStart() {
+ return start;
+ }
+
+ public float getStartAngle() {
+ return start + animate;
+ }
+
+ public float getSweep() {
+ return sweep;
+ }
+
+ public int getInnerRadius() {
+ return inner;
+ }
+
+ public int getOuterRadius() {
+ return outer;
+ }
+
+ public void setOnClickListener(OnClickListener listener) {
+ mOnClickListener = listener;
+ }
+
+ public void performClick() {
+ if (mOnClickListener != null) {
+ mOnClickListener.onClick(this);
+ }
+ }
+
+ public int getIntrinsicWidth() {
+ return mDrawable.getIntrinsicWidth();
+ }
+
+ public int getIntrinsicHeight() {
+ return mDrawable.getIntrinsicHeight();
+ }
+
+ public void setBounds(int left, int top, int right, int bottom) {
+ mDrawable.setBounds(left, top, right, bottom);
+ }
+
+ public void draw(Canvas canvas) {
+ mDrawable.draw(canvas);
+ }
+
+ public void setImageResource(Context context, int resId) {
+ Drawable d = context.getResources().getDrawable(resId).mutate();
+ d.setBounds(mDrawable.getBounds());
+ mDrawable = d;
+ setAlpha(mAlpha);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java b/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java
new file mode 100644
index 000000000..59b57b002
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java
@@ -0,0 +1,816 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.Handler;
+import android.os.Message;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Used to draw and render the pie item focus indicator. */
+public class PieRenderer extends OverlayRenderer implements FocusIndicator {
+ // Sometimes continuous autofocus starts and stops several times quickly.
+ // These states are used to make sure the animation is run for at least some
+ // time.
+ private volatile int mState;
+ private ScaleAnimation mAnimation = new ScaleAnimation();
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_FOCUSING = 1;
+ private static final int STATE_FINISHING = 2;
+ private static final int STATE_PIE = 8;
+
+ private Runnable mDisappear = new Disappear();
+ private Animation.AnimationListener mEndAction = new EndAction();
+ private static final int SCALING_UP_TIME = 600;
+ private static final int SCALING_DOWN_TIME = 100;
+ private static final int DISAPPEAR_TIMEOUT = 200;
+ private static final int DIAL_HORIZONTAL = 157;
+
+ private static final long PIE_FADE_IN_DURATION = 200;
+ private static final long PIE_XFADE_DURATION = 200;
+ private static final long PIE_SELECT_FADE_DURATION = 300;
+
+ private static final int MSG_OPEN = 0;
+ private static final int MSG_CLOSE = 1;
+ private static final float PIE_SWEEP = (float) (Math.PI * 2 / 3);
+ // geometry
+ private Point mCenter;
+ private int mRadius;
+ private int mRadiusInc;
+
+ // the detection if touch is inside a slice is offset
+ // inbounds by this amount to allow the selection to show before the
+ // finger covers it
+ private int mTouchOffset;
+
+ private List<PieItem> mItems;
+
+ private PieItem mOpenItem;
+
+ private Paint mSelectedPaint;
+ private Paint mSubPaint;
+
+ // touch handling
+ private PieItem mCurrentItem;
+
+ private Paint mFocusPaint;
+ private int mSuccessColor;
+ private int mFailColor;
+ private int mCircleSize;
+ private int mFocusX;
+ private int mFocusY;
+ private int mCenterX;
+ private int mCenterY;
+
+ private int mDialAngle;
+ private RectF mCircle;
+ private RectF mDial;
+ private Point mPoint1;
+ private Point mPoint2;
+ private int mStartAnimationAngle;
+ private boolean mFocused;
+ private int mInnerOffset;
+ private int mOuterStroke;
+ private int mInnerStroke;
+ private boolean mTapMode;
+ private boolean mBlockFocus;
+ private int mTouchSlopSquared;
+ private Point mDown;
+ private boolean mOpening;
+ private LinearAnimation mXFade;
+ private LinearAnimation mFadeIn;
+ private volatile boolean mFocusCancelled;
+
+ private Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_OPEN:
+ if (mListener != null) {
+ mListener.onPieOpened(mCenter.x, mCenter.y);
+ }
+ break;
+ case MSG_CLOSE:
+ if (mListener != null) {
+ mListener.onPieClosed();
+ }
+ break;
+ }
+ }
+ };
+
+ private PieListener mListener;
+
+ /** Listener for the pie item to communicate back to the renderer. */
+ public interface PieListener {
+ void onPieOpened(int centerX, int centerY);
+
+ void onPieClosed();
+ }
+
+ public void setPieListener(PieListener pl) {
+ mListener = pl;
+ }
+
+ public PieRenderer(Context context) {
+ init(context);
+ }
+
+ private void init(Context ctx) {
+ setVisible(false);
+ mItems = new ArrayList<PieItem>();
+ Resources res = ctx.getResources();
+ mRadius = res.getDimensionPixelSize(R.dimen.pie_radius_start);
+ mCircleSize = mRadius - res.getDimensionPixelSize(R.dimen.focus_radius_offset);
+ mRadiusInc = res.getDimensionPixelSize(R.dimen.pie_radius_increment);
+ mTouchOffset = res.getDimensionPixelSize(R.dimen.pie_touch_offset);
+ mCenter = new Point(0, 0);
+ mSelectedPaint = new Paint();
+ mSelectedPaint.setColor(Color.argb(255, 51, 181, 229));
+ mSelectedPaint.setAntiAlias(true);
+ mSubPaint = new Paint();
+ mSubPaint.setAntiAlias(true);
+ mSubPaint.setColor(Color.argb(200, 250, 230, 128));
+ mFocusPaint = new Paint();
+ mFocusPaint.setAntiAlias(true);
+ mFocusPaint.setColor(Color.WHITE);
+ mFocusPaint.setStyle(Paint.Style.STROKE);
+ mSuccessColor = Color.GREEN;
+ mFailColor = Color.RED;
+ mCircle = new RectF();
+ mDial = new RectF();
+ mPoint1 = new Point();
+ mPoint2 = new Point();
+ mInnerOffset = res.getDimensionPixelSize(R.dimen.focus_inner_offset);
+ mOuterStroke = res.getDimensionPixelSize(R.dimen.focus_outer_stroke);
+ mInnerStroke = res.getDimensionPixelSize(R.dimen.focus_inner_stroke);
+ mState = STATE_IDLE;
+ mBlockFocus = false;
+ mTouchSlopSquared = ViewConfiguration.get(ctx).getScaledTouchSlop();
+ mTouchSlopSquared = mTouchSlopSquared * mTouchSlopSquared;
+ mDown = new Point();
+ }
+
+ public boolean showsItems() {
+ return mTapMode;
+ }
+
+ public void addItem(PieItem item) {
+ // add the item to the pie itself
+ mItems.add(item);
+ }
+
+ public void removeItem(PieItem item) {
+ mItems.remove(item);
+ }
+
+ public void clearItems() {
+ mItems.clear();
+ }
+
+ public void showInCenter() {
+ if ((mState == STATE_PIE) && isVisible()) {
+ mTapMode = false;
+ show(false);
+ } else {
+ if (mState != STATE_IDLE) {
+ cancelFocus();
+ }
+ mState = STATE_PIE;
+ setCenter(mCenterX, mCenterY);
+ mTapMode = true;
+ show(true);
+ }
+ }
+
+ public void hide() {
+ show(false);
+ }
+
+ /**
+ * guaranteed has center set
+ *
+ * @param show
+ */
+ private void show(boolean show) {
+ if (show) {
+ mState = STATE_PIE;
+ // ensure clean state
+ mCurrentItem = null;
+ mOpenItem = null;
+ for (PieItem item : mItems) {
+ item.setSelected(false);
+ }
+ layoutPie();
+ fadeIn();
+ } else {
+ mState = STATE_IDLE;
+ mTapMode = false;
+ if (mXFade != null) {
+ mXFade.cancel();
+ }
+ }
+ setVisible(show);
+ mHandler.sendEmptyMessage(show ? MSG_OPEN : MSG_CLOSE);
+ }
+
+ private void fadeIn() {
+ mFadeIn = new LinearAnimation(0, 1);
+ mFadeIn.setDuration(PIE_FADE_IN_DURATION);
+ mFadeIn.setAnimationListener(
+ new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mFadeIn = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mFadeIn.startNow();
+ mOverlay.startAnimation(mFadeIn);
+ }
+
+ public void setCenter(int x, int y) {
+ mCenter.x = x;
+ mCenter.y = y;
+ // when using the pie menu, align the focus ring
+ alignFocus(x, y);
+ }
+
+ private void layoutPie() {
+ int rgap = 2;
+ int inner = mRadius + rgap;
+ int outer = mRadius + mRadiusInc - rgap;
+ int gap = 1;
+ layoutItems(mItems, (float) (Math.PI / 2), inner, outer, gap);
+ }
+
+ private void layoutItems(List<PieItem> items, float centerAngle, int inner, int outer, int gap) {
+ float emptyangle = PIE_SWEEP / 16;
+ float sweep = (PIE_SWEEP - 2 * emptyangle) / items.size();
+ float angle = centerAngle - PIE_SWEEP / 2 + emptyangle + sweep / 2;
+ // check if we have custom geometry
+ // first item we find triggers custom sweep for all
+ // this allows us to re-use the path
+ for (PieItem item : items) {
+ if (item.getCenter() >= 0) {
+ sweep = item.getSweep();
+ break;
+ }
+ }
+ Path path = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
+ for (PieItem item : items) {
+ // shared between items
+ item.setPath(path);
+ if (item.getCenter() >= 0) {
+ angle = item.getCenter();
+ }
+ int w = item.getIntrinsicWidth();
+ int h = item.getIntrinsicHeight();
+ // move views to outer border
+ int r = inner + (outer - inner) * 2 / 3;
+ int x = (int) (r * Math.cos(angle));
+ int y = mCenter.y - (int) (r * Math.sin(angle)) - h / 2;
+ x = mCenter.x + x - w / 2;
+ item.setBounds(x, y, x + w, y + h);
+ float itemstart = angle - sweep / 2;
+ item.setGeometry(itemstart, sweep, inner, outer);
+ if (item.hasItems()) {
+ layoutItems(item.getItems(), angle, inner, outer + mRadiusInc / 2, gap);
+ }
+ angle += sweep;
+ }
+ }
+
+ private Path makeSlice(float start, float end, int outer, int inner, Point center) {
+ RectF bb = new RectF(center.x - outer, center.y - outer, center.x + outer, center.y + outer);
+ RectF bbi = new RectF(center.x - inner, center.y - inner, center.x + inner, center.y + inner);
+ Path path = new Path();
+ path.arcTo(bb, start, end - start, true);
+ path.arcTo(bbi, end, start - end);
+ path.close();
+ return path;
+ }
+
+ /**
+ * converts a
+ *
+ * @param angle from 0..PI to Android degrees (clockwise starting at 3 o'clock)
+ * @return skia angle
+ */
+ private float getDegrees(double angle) {
+ return (float) (360 - 180 * angle / Math.PI);
+ }
+
+ private void startFadeOut() {
+ mOverlay
+ .animate()
+ .alpha(0)
+ .setListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ deselect();
+ show(false);
+ mOverlay.setAlpha(1);
+ super.onAnimationEnd(animation);
+ }
+ })
+ .setDuration(PIE_SELECT_FADE_DURATION);
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ float alpha = 1;
+ if (mXFade != null) {
+ alpha = mXFade.getValue();
+ } else if (mFadeIn != null) {
+ alpha = mFadeIn.getValue();
+ }
+ int state = canvas.save();
+ if (mFadeIn != null) {
+ float sf = 0.9f + alpha * 0.1f;
+ canvas.scale(sf, sf, mCenter.x, mCenter.y);
+ }
+ drawFocus(canvas);
+ if (mState == STATE_FINISHING) {
+ canvas.restoreToCount(state);
+ return;
+ }
+ if ((mOpenItem == null) || (mXFade != null)) {
+ // draw base menu
+ for (PieItem item : mItems) {
+ drawItem(canvas, item, alpha);
+ }
+ }
+ if (mOpenItem != null) {
+ for (PieItem inner : mOpenItem.getItems()) {
+ drawItem(canvas, inner, (mXFade != null) ? (1 - 0.5f * alpha) : 1);
+ }
+ }
+ canvas.restoreToCount(state);
+ }
+
+ private void drawItem(Canvas canvas, PieItem item, float alpha) {
+ if (mState == STATE_PIE) {
+ if (item.getPath() != null) {
+ if (item.isSelected()) {
+ Paint p = mSelectedPaint;
+ int state = canvas.save();
+ float r = getDegrees(item.getStartAngle());
+ canvas.rotate(r, mCenter.x, mCenter.y);
+ canvas.drawPath(item.getPath(), p);
+ canvas.restoreToCount(state);
+ }
+ alpha = alpha * (item.isEnabled() ? 1 : 0.3f);
+ // draw the item view
+ item.setAlpha(alpha);
+ item.draw(canvas);
+ }
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ float x = evt.getX();
+ float y = evt.getY();
+ int action = evt.getActionMasked();
+ PointF polar = getPolar(x, y, !(mTapMode));
+ if (MotionEvent.ACTION_DOWN == action) {
+ mDown.x = (int) evt.getX();
+ mDown.y = (int) evt.getY();
+ mOpening = false;
+ if (mTapMode) {
+ PieItem item = findItem(polar);
+ if ((item != null) && (mCurrentItem != item)) {
+ mState = STATE_PIE;
+ onEnter(item);
+ }
+ } else {
+ setCenter((int) x, (int) y);
+ show(true);
+ }
+ return true;
+ } else if (MotionEvent.ACTION_UP == action) {
+ if (isVisible()) {
+ PieItem item = mCurrentItem;
+ if (mTapMode) {
+ item = findItem(polar);
+ if (item != null && mOpening) {
+ mOpening = false;
+ return true;
+ }
+ }
+ if (item == null) {
+ mTapMode = false;
+ show(false);
+ } else if (!mOpening && !item.hasItems()) {
+ item.performClick();
+ startFadeOut();
+ mTapMode = false;
+ }
+ return true;
+ }
+ } else if (MotionEvent.ACTION_CANCEL == action) {
+ if (isVisible() || mTapMode) {
+ show(false);
+ }
+ deselect();
+ return false;
+ } else if (MotionEvent.ACTION_MOVE == action) {
+ if (polar.y < mRadius) {
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ } else {
+ deselect();
+ }
+ return false;
+ }
+ PieItem item = findItem(polar);
+ boolean moved = hasMoved(evt);
+ if ((item != null) && (mCurrentItem != item) && (!mOpening || moved)) {
+ // only select if we didn't just open or have moved past slop
+ mOpening = false;
+ if (moved) {
+ // switch back to swipe mode
+ mTapMode = false;
+ }
+ onEnter(item);
+ }
+ }
+ return false;
+ }
+
+ private boolean hasMoved(MotionEvent e) {
+ return mTouchSlopSquared
+ < (e.getX() - mDown.x) * (e.getX() - mDown.x) + (e.getY() - mDown.y) * (e.getY() - mDown.y);
+ }
+
+ /**
+ * enter a slice for a view updates model only
+ *
+ * @param item
+ */
+ private void onEnter(PieItem item) {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (item != null && item.isEnabled()) {
+ item.setSelected(true);
+ mCurrentItem = item;
+ if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+ openCurrentItem();
+ }
+ } else {
+ mCurrentItem = null;
+ }
+ }
+
+ private void deselect() {
+ if (mCurrentItem != null) {
+ mCurrentItem.setSelected(false);
+ }
+ if (mOpenItem != null) {
+ mOpenItem = null;
+ }
+ mCurrentItem = null;
+ }
+
+ private void openCurrentItem() {
+ if ((mCurrentItem != null) && mCurrentItem.hasItems()) {
+ mCurrentItem.setSelected(false);
+ mOpenItem = mCurrentItem;
+ mOpening = true;
+ mXFade = new LinearAnimation(1, 0);
+ mXFade.setDuration(PIE_XFADE_DURATION);
+ mXFade.setAnimationListener(
+ new AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mXFade = null;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mXFade.startNow();
+ mOverlay.startAnimation(mXFade);
+ }
+ }
+
+ private PointF getPolar(float x, float y, boolean useOffset) {
+ PointF res = new PointF();
+ // get angle and radius from x/y
+ res.x = (float) Math.PI / 2;
+ x = x - mCenter.x;
+ y = mCenter.y - y;
+ res.y = (float) Math.sqrt(x * x + y * y);
+ if (x != 0) {
+ res.x = (float) Math.atan2(y, x);
+ if (res.x < 0) {
+ res.x = (float) (2 * Math.PI + res.x);
+ }
+ }
+ res.y = res.y + (useOffset ? mTouchOffset : 0);
+ return res;
+ }
+
+ /**
+ * @param polar x: angle, y: dist
+ * @return the item at angle/dist or null
+ */
+ private PieItem findItem(PointF polar) {
+ // find the matching item:
+ List<PieItem> items = (mOpenItem != null) ? mOpenItem.getItems() : mItems;
+ for (PieItem item : items) {
+ if (inside(polar, item)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ private boolean inside(PointF polar, PieItem item) {
+ return (item.getInnerRadius() < polar.y)
+ && (item.getStartAngle() < polar.x)
+ && (item.getStartAngle() + item.getSweep() > polar.x)
+ && (!mTapMode || (item.getOuterRadius() > polar.y));
+ }
+
+ @Override
+ public boolean handlesTouch() {
+ return true;
+ }
+
+ // focus specific code
+
+ public void setBlockFocus(boolean blocked) {
+ mBlockFocus = blocked;
+ if (blocked) {
+ clear();
+ }
+ }
+
+ public void setFocus(int x, int y) {
+ mFocusX = x;
+ mFocusY = y;
+ setCircle(mFocusX, mFocusY);
+ }
+
+ public void alignFocus(int x, int y) {
+ mOverlay.removeCallbacks(mDisappear);
+ mAnimation.cancel();
+ mAnimation.reset();
+ mFocusX = x;
+ mFocusY = y;
+ mDialAngle = DIAL_HORIZONTAL;
+ setCircle(x, y);
+ mFocused = false;
+ }
+
+ public int getSize() {
+ return 2 * mCircleSize;
+ }
+
+ private int getRandomRange() {
+ return (int) (-60 + 120 * Math.random());
+ }
+
+ @Override
+ public void layout(int l, int t, int r, int b) {
+ super.layout(l, t, r, b);
+ mCenterX = (r - l) / 2;
+ mCenterY = (b - t) / 2;
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ setCircle(mFocusX, mFocusY);
+ if (isVisible() && mState == STATE_PIE) {
+ setCenter(mCenterX, mCenterY);
+ layoutPie();
+ }
+ }
+
+ private void setCircle(int cx, int cy) {
+ mCircle.set(cx - mCircleSize, cy - mCircleSize, cx + mCircleSize, cy + mCircleSize);
+ mDial.set(
+ cx - mCircleSize + mInnerOffset,
+ cy - mCircleSize + mInnerOffset,
+ cx + mCircleSize - mInnerOffset,
+ cy + mCircleSize - mInnerOffset);
+ }
+
+ public void drawFocus(Canvas canvas) {
+ if (mBlockFocus) {
+ return;
+ }
+ mFocusPaint.setStrokeWidth(mOuterStroke);
+ canvas.drawCircle((float) mFocusX, (float) mFocusY, (float) mCircleSize, mFocusPaint);
+ if (mState == STATE_PIE) {
+ return;
+ }
+ int color = mFocusPaint.getColor();
+ if (mState == STATE_FINISHING) {
+ mFocusPaint.setColor(mFocused ? mSuccessColor : mFailColor);
+ }
+ mFocusPaint.setStrokeWidth(mInnerStroke);
+ drawLine(canvas, mDialAngle, mFocusPaint);
+ drawLine(canvas, mDialAngle + 45, mFocusPaint);
+ drawLine(canvas, mDialAngle + 180, mFocusPaint);
+ drawLine(canvas, mDialAngle + 225, mFocusPaint);
+ canvas.save();
+ // rotate the arc instead of its offset to better use framework's shape caching
+ canvas.rotate(mDialAngle, mFocusX, mFocusY);
+ canvas.drawArc(mDial, 0, 45, false, mFocusPaint);
+ canvas.drawArc(mDial, 180, 45, false, mFocusPaint);
+ canvas.restore();
+ mFocusPaint.setColor(color);
+ }
+
+ private void drawLine(Canvas canvas, int angle, Paint p) {
+ convertCart(angle, mCircleSize - mInnerOffset, mPoint1);
+ convertCart(angle, mCircleSize - mInnerOffset + mInnerOffset / 3, mPoint2);
+ canvas.drawLine(
+ mPoint1.x + mFocusX, mPoint1.y + mFocusY, mPoint2.x + mFocusX, mPoint2.y + mFocusY, p);
+ }
+
+ private static void convertCart(int angle, int radius, Point out) {
+ double a = 2 * Math.PI * (angle % 360) / 360;
+ out.x = (int) (radius * Math.cos(a) + 0.5);
+ out.y = (int) (radius * Math.sin(a) + 0.5);
+ }
+
+ @Override
+ public void showStart() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ cancelFocus();
+ mStartAnimationAngle = 67;
+ int range = getRandomRange();
+ startAnimation(SCALING_UP_TIME, false, mStartAnimationAngle, mStartAnimationAngle + range);
+ mState = STATE_FOCUSING;
+ }
+
+ @Override
+ public void showSuccess(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME, timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = true;
+ }
+ }
+
+ @Override
+ public void showFail(boolean timeout) {
+ if (mState == STATE_FOCUSING) {
+ startAnimation(SCALING_DOWN_TIME, timeout, mStartAnimationAngle);
+ mState = STATE_FINISHING;
+ mFocused = false;
+ }
+ }
+
+ private void cancelFocus() {
+ mFocusCancelled = true;
+ mOverlay.removeCallbacks(mDisappear);
+ if (mAnimation != null) {
+ mAnimation.cancel();
+ }
+ mFocusCancelled = false;
+ mFocused = false;
+ mState = STATE_IDLE;
+ }
+
+ @Override
+ public void clear() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ cancelFocus();
+ mOverlay.post(mDisappear);
+ }
+
+ private void startAnimation(long duration, boolean timeout, float toScale) {
+ startAnimation(duration, timeout, mDialAngle, toScale);
+ }
+
+ private void startAnimation(long duration, boolean timeout, float fromScale, float toScale) {
+ setVisible(true);
+ mAnimation.reset();
+ mAnimation.setDuration(duration);
+ mAnimation.setScale(fromScale, toScale);
+ mAnimation.setAnimationListener(timeout ? mEndAction : null);
+ mOverlay.startAnimation(mAnimation);
+ update();
+ }
+
+ private class EndAction implements Animation.AnimationListener {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // Keep the focus indicator for some time.
+ if (!mFocusCancelled) {
+ mOverlay.postDelayed(mDisappear, DISAPPEAR_TIMEOUT);
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+
+ @Override
+ public void onAnimationStart(Animation animation) {}
+ }
+
+ private class Disappear implements Runnable {
+ @Override
+ public void run() {
+ if (mState == STATE_PIE) {
+ return;
+ }
+ setVisible(false);
+ mFocusX = mCenterX;
+ mFocusY = mCenterY;
+ mState = STATE_IDLE;
+ setCircle(mFocusX, mFocusY);
+ mFocused = false;
+ }
+ }
+
+ private class ScaleAnimation extends Animation {
+ private float mFrom = 1f;
+ private float mTo = 1f;
+
+ public ScaleAnimation() {
+ setFillAfter(true);
+ }
+
+ public void setScale(float from, float to) {
+ mFrom = from;
+ mTo = to;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mDialAngle = (int) (mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+
+ private static class LinearAnimation extends Animation {
+ private float mFrom;
+ private float mTo;
+ private float mValue;
+
+ public LinearAnimation(float from, float to) {
+ setFillAfter(true);
+ setInterpolator(new LinearInterpolator());
+ mFrom = from;
+ mTo = to;
+ }
+
+ public float getValue() {
+ return mValue;
+ }
+
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ mValue = (mFrom + (mTo - mFrom) * interpolatedTime);
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java b/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java
new file mode 100644
index 000000000..c38ae6c81
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java
@@ -0,0 +1,153 @@
+/*
+ * 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.callcomposer.camera.camerafocus;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Focusing overlay. */
+public class RenderOverlay extends FrameLayout {
+
+ /** Render interface. */
+ interface Renderer {
+ boolean handlesTouch();
+
+ boolean onTouchEvent(MotionEvent evt);
+
+ void setOverlay(RenderOverlay overlay);
+
+ void layout(int left, int top, int right, int bottom);
+
+ void draw(Canvas canvas);
+ }
+
+ private RenderView mRenderView;
+ private List<Renderer> mClients;
+
+ // reverse list of touch clients
+ private List<Renderer> mTouchClients;
+ private int[] mPosition = new int[2];
+
+ public RenderOverlay(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mRenderView = new RenderView(context);
+ addView(mRenderView, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ mClients = new ArrayList<>(10);
+ mTouchClients = new ArrayList<>(10);
+ setWillNotDraw(false);
+
+ addRenderer(new PieRenderer(context));
+ }
+
+ public PieRenderer getPieRenderer() {
+ for (Renderer renderer : mClients) {
+ if (renderer instanceof PieRenderer) {
+ return (PieRenderer) renderer;
+ }
+ }
+ return null;
+ }
+
+ public void addRenderer(Renderer renderer) {
+ mClients.add(renderer);
+ renderer.setOverlay(this);
+ if (renderer.handlesTouch()) {
+ mTouchClients.add(0, renderer);
+ }
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void addRenderer(int pos, Renderer renderer) {
+ mClients.add(pos, renderer);
+ renderer.setOverlay(this);
+ renderer.layout(getLeft(), getTop(), getRight(), getBottom());
+ }
+
+ public void remove(Renderer renderer) {
+ mClients.remove(renderer);
+ renderer.setOverlay(null);
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent m) {
+ return false;
+ }
+
+ private void adjustPosition() {
+ getLocationInWindow(mPosition);
+ }
+
+ public void update() {
+ mRenderView.invalidate();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private class RenderView extends View {
+
+ public RenderView(Context context) {
+ super(context);
+ setWillNotDraw(false);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent evt) {
+ if (mTouchClients != null) {
+ boolean res = false;
+ for (Renderer client : mTouchClients) {
+ res |= client.onTouchEvent(evt);
+ }
+ return res;
+ }
+ return false;
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ adjustPosition();
+ super.onLayout(changed, left, top, right, bottom);
+ if (mClients == null) {
+ return;
+ }
+ for (Renderer renderer : mClients) {
+ renderer.layout(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+ if (mClients == null) {
+ return;
+ }
+ boolean redraw = false;
+ for (Renderer renderer : mClients) {
+ renderer.draw(canvas);
+ redraw = redraw || ((OverlayRenderer) renderer).isVisible();
+ }
+ if (redraw) {
+ invalidate();
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml b/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml
new file mode 100644
index 000000000..fba631b0e
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <!-- Camera focus indicator values -->
+ <dimen name="pie_radius_start">40dp</dimen>
+ <dimen name="pie_radius_increment">30dp</dimen>
+ <dimen name="pie_touch_offset">20dp</dimen>
+ <dimen name="focus_radius_offset">8dp</dimen>
+ <dimen name="focus_inner_offset">12dp</dimen>
+ <dimen name="focus_outer_stroke">3dp</dimen>
+ <dimen name="focus_inner_stroke">2dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java b/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java
new file mode 100644
index 000000000..e2c8185da
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java
@@ -0,0 +1,129 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import com.android.dialer.common.Assert;
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+
+class CountedDataInputStream extends FilterInputStream {
+
+ private int mCount = 0;
+
+ // allocate a byte buffer for a long value;
+ private final byte[] mByteArray = new byte[8];
+ private final ByteBuffer mByteBuffer = ByteBuffer.wrap(mByteArray);
+
+ CountedDataInputStream(InputStream in) {
+ super(in);
+ }
+
+ int getReadByteCount() {
+ return mCount;
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ int r = in.read(b);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ int r = in.read(b, off, len);
+ mCount += (r >= 0) ? r : 0;
+ return r;
+ }
+
+ @Override
+ public int read() throws IOException {
+ int r = in.read();
+ mCount += (r >= 0) ? 1 : 0;
+ return r;
+ }
+
+ @Override
+ public long skip(long length) throws IOException {
+ long skip = in.skip(length);
+ mCount += skip;
+ return skip;
+ }
+
+ private void skipOrThrow(long length) throws IOException {
+ if (skip(length) != length) {
+ throw new EOFException();
+ }
+ }
+
+ void skipTo(long target) throws IOException {
+ long cur = mCount;
+ long diff = target - cur;
+ Assert.checkArgument(diff >= 0);
+ skipOrThrow(diff);
+ }
+
+ private void readOrThrow(byte[] b, int off, int len) throws IOException {
+ int r = read(b, off, len);
+ if (r != len) {
+ throw new EOFException();
+ }
+ }
+
+ private void readOrThrow(byte[] b) throws IOException {
+ readOrThrow(b, 0, b.length);
+ }
+
+ void setByteOrder(ByteOrder order) {
+ mByteBuffer.order(order);
+ }
+
+ ByteOrder getByteOrder() {
+ return mByteBuffer.order();
+ }
+
+ short readShort() throws IOException {
+ readOrThrow(mByteArray, 0, 2);
+ mByteBuffer.rewind();
+ return mByteBuffer.getShort();
+ }
+
+ int readUnsignedShort() throws IOException {
+ return readShort() & 0xffff;
+ }
+
+ int readInt() throws IOException {
+ readOrThrow(mByteArray, 0, 4);
+ mByteBuffer.rewind();
+ return mByteBuffer.getInt();
+ }
+
+ long readUnsignedInt() throws IOException {
+ return readInt() & 0xffffffffL;
+ }
+
+ String readString(int n, Charset charset) throws IOException {
+ byte[] buf = new byte[n];
+ readOrThrow(buf);
+ return new String(buf, charset);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifData.java b/java/com/android/dialer/callcomposer/camera/exif/ExifData.java
new file mode 100644
index 000000000..27936ae2f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifData.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.dialer.callcomposer.camera.exif;
+
+/**
+ * This class stores the EXIF header in IFDs according to the JPEG specification. It is the result
+ * produced by {@link ExifReader}.
+ *
+ * @see ExifReader
+ * @see IfdData
+ */
+public class ExifData {
+
+ private final IfdData[] mIfdDatas = new IfdData[IfdId.TYPE_IFD_COUNT];
+
+ /**
+ * Adds IFD data. If IFD data of the same type already exists, it will be replaced by the new
+ * data.
+ */
+ void addIfdData(IfdData data) {
+ mIfdDatas[data.getId()] = data;
+ }
+
+ /** Returns the {@link IfdData} object corresponding to a given IFD if it exists or null. */
+ IfdData getIfdData(int ifdId) {
+ if (ExifTag.isValidIfd(ifdId)) {
+ return mIfdDatas[ifdId];
+ }
+ return null;
+ }
+
+ /**
+ * Returns the tag with a given TID in the given IFD if the tag exists. Otherwise returns null.
+ */
+ protected ExifTag getTag(short tag, int ifd) {
+ IfdData ifdData = mIfdDatas[ifd];
+ return (ifdData == null) ? null : ifdData.getTag(tag);
+ }
+
+ /**
+ * Adds the given ExifTag to its default IFD and returns an existing ExifTag with the same TID or
+ * null if none exist.
+ */
+ ExifTag addTag(ExifTag tag) {
+ if (tag != null) {
+ int ifd = tag.getIfd();
+ return addTag(tag, ifd);
+ }
+ return null;
+ }
+
+ /**
+ * Adds the given ExifTag to the given IFD and returns an existing ExifTag with the same TID or
+ * null if none exist.
+ */
+ private ExifTag addTag(ExifTag tag, int ifdId) {
+ if (tag != null && ExifTag.isValidIfd(ifdId)) {
+ IfdData ifdData = getOrCreateIfdData(ifdId);
+ return ifdData.setTag(tag);
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link IfdData} object corresponding to a given IFD or generates one if none exist.
+ */
+ private IfdData getOrCreateIfdData(int ifdId) {
+ IfdData ifdData = mIfdDatas[ifdId];
+ if (ifdData == null) {
+ ifdData = new IfdData(ifdId);
+ mIfdDatas[ifdId] = ifdData;
+ }
+ return ifdData;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java b/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java
new file mode 100644
index 000000000..92dee1c94
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java
@@ -0,0 +1,374 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.util.SparseIntArray;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.HashSet;
+import java.util.TimeZone;
+
+/**
+ * This class provides methods and constants for reading and writing jpeg file metadata. It contains
+ * a collection of ExifTags, and a collection of definitions for creating valid ExifTags. The
+ * collection of ExifTags can be updated by: reading new ones from a file, deleting or adding
+ * existing ones, or building new ExifTags from a tag definition. These ExifTags can be written to a
+ * valid jpeg image as exif metadata.
+ *
+ * <p>Each ExifTag has a tag ID (TID) and is stored in a specific image file directory (IFD) as
+ * specified by the exif standard. A tag definition can be looked up with a constant that is a
+ * combination of TID and IFD. This definition has information about the type, number of components,
+ * and valid IFDs for a tag.
+ *
+ * @see ExifTag
+ */
+public class ExifInterface {
+ private static final int IFD_NULL = -1;
+ static final int DEFINITION_NULL = 0;
+
+ /** Tag constants for Jeita EXIF 2.2 */
+ // IFD 0
+ public static final int TAG_ORIENTATION = defineTag(IfdId.TYPE_IFD_0, (short) 0x0112);
+
+ static final int TAG_EXIF_IFD = defineTag(IfdId.TYPE_IFD_0, (short) 0x8769);
+ static final int TAG_GPS_IFD = defineTag(IfdId.TYPE_IFD_0, (short) 0x8825);
+ static final int TAG_STRIP_OFFSETS = defineTag(IfdId.TYPE_IFD_0, (short) 0x0111);
+ static final int TAG_STRIP_BYTE_COUNTS = defineTag(IfdId.TYPE_IFD_0, (short) 0x0117);
+ // IFD 1
+ static final int TAG_JPEG_INTERCHANGE_FORMAT = defineTag(IfdId.TYPE_IFD_1, (short) 0x0201);
+ static final int TAG_JPEG_INTERCHANGE_FORMAT_LENGTH = defineTag(IfdId.TYPE_IFD_1, (short) 0x0202);
+ // IFD Exif Tags
+ static final int TAG_INTEROPERABILITY_IFD = defineTag(IfdId.TYPE_IFD_EXIF, (short) 0xA005);
+
+ /** Tags that contain offset markers. These are included in the banned defines. */
+ private static HashSet<Short> sOffsetTags = new HashSet<>();
+
+ static {
+ sOffsetTags.add(getTrueTagKey(TAG_GPS_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_EXIF_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_JPEG_INTERCHANGE_FORMAT));
+ sOffsetTags.add(getTrueTagKey(TAG_INTEROPERABILITY_IFD));
+ sOffsetTags.add(getTrueTagKey(TAG_STRIP_OFFSETS));
+ }
+
+ private static final String NULL_ARGUMENT_STRING = "Argument is null";
+
+ private static final String GPS_DATE_FORMAT_STR = "yyyy:MM:dd";
+
+ private ExifData mData = new ExifData();
+
+ @SuppressLint("SimpleDateFormat")
+ public ExifInterface() {
+ DateFormat mGPSDateStampFormat = new SimpleDateFormat(GPS_DATE_FORMAT_STR);
+ mGPSDateStampFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+ }
+
+ /**
+ * Reads the exif tags from a byte array, clearing this ExifInterface object's existing exif tags.
+ *
+ * @param jpeg a byte array containing a jpeg compressed image.
+ * @throws java.io.IOException
+ */
+ public void readExif(byte[] jpeg) throws IOException {
+ readExif(new ByteArrayInputStream(jpeg));
+ }
+
+ /**
+ * Reads the exif tags from an InputStream, clearing this ExifInterface object's existing exif
+ * tags.
+ *
+ * @param inStream an InputStream containing a jpeg compressed image.
+ * @throws java.io.IOException
+ */
+ private void readExif(InputStream inStream) throws IOException {
+ if (inStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ ExifData d;
+ try {
+ d = new ExifReader(this).read(inStream);
+ } catch (ExifInvalidFormatException e) {
+ throw new IOException("Invalid exif format : " + e);
+ }
+ mData = d;
+ }
+
+ /** Returns the TID for a tag constant. */
+ static short getTrueTagKey(int tag) {
+ // Truncate
+ return (short) tag;
+ }
+
+ /** Returns the constant representing a tag with a given TID and default IFD. */
+ private static int defineTag(int ifdId, short tagId) {
+ return (tagId & 0x0000ffff) | (ifdId << 16);
+ }
+
+ static boolean isIfdAllowed(int info, int ifd) {
+ int[] ifds = IfdData.getIfds();
+ int ifdFlags = getAllowedIfdFlagsFromInfo(info);
+ for (int i = 0; i < ifds.length; i++) {
+ if (ifd == ifds[i] && ((ifdFlags >> i) & 1) == 1) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int getAllowedIfdFlagsFromInfo(int info) {
+ return info >>> 24;
+ }
+
+ /**
+ * Returns true if tag TID is one of the following: {@code TAG_EXIF_IFD}, {@code TAG_GPS_IFD},
+ * {@code TAG_JPEG_INTERCHANGE_FORMAT}, {@code TAG_STRIP_OFFSETS}, {@code
+ * TAG_INTEROPERABILITY_IFD}
+ *
+ * <p>Note: defining tags with these TID's is disallowed.
+ *
+ * @param tag a tag's TID (can be obtained from a defined tag constant with {@link
+ * #getTrueTagKey}).
+ * @return true if the TID is that of an offset tag.
+ */
+ static boolean isOffsetTag(short tag) {
+ return sOffsetTags.contains(tag);
+ }
+
+ private SparseIntArray mTagInfo = null;
+
+ SparseIntArray getTagInfo() {
+ if (mTagInfo == null) {
+ mTagInfo = new SparseIntArray();
+ initTagInfo();
+ }
+ return mTagInfo;
+ }
+
+ private void initTagInfo() {
+ /**
+ * We put tag information in a 4-bytes integer. The first byte a bitmask representing the
+ * allowed IFDs of the tag, the second byte is the data type, and the last two byte are a short
+ * value indicating the default component count of this tag.
+ */
+ // IFD0 tags
+ int[] ifdAllowedIfds = {IfdId.TYPE_IFD_0, IfdId.TYPE_IFD_1};
+ int ifdFlags = getFlagsFromAllowedIfds(ifdAllowedIfds) << 24;
+ mTagInfo.put(ExifInterface.TAG_STRIP_OFFSETS, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16);
+ mTagInfo.put(ExifInterface.TAG_EXIF_IFD, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_GPS_IFD, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_ORIENTATION, ifdFlags | ExifTag.TYPE_UNSIGNED_SHORT << 16 | 1);
+ mTagInfo.put(ExifInterface.TAG_STRIP_BYTE_COUNTS, ifdFlags | ExifTag.TYPE_UNSIGNED_LONG << 16);
+ // IFD1 tags
+ int[] ifd1AllowedIfds = {IfdId.TYPE_IFD_1};
+ int ifdFlags1 = getFlagsFromAllowedIfds(ifd1AllowedIfds) << 24;
+ mTagInfo.put(
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ mTagInfo.put(
+ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
+ ifdFlags1 | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ // Exif tags
+ int[] exifAllowedIfds = {IfdId.TYPE_IFD_EXIF};
+ int exifFlags = getFlagsFromAllowedIfds(exifAllowedIfds) << 24;
+ mTagInfo.put(
+ ExifInterface.TAG_INTEROPERABILITY_IFD, exifFlags | ExifTag.TYPE_UNSIGNED_LONG << 16 | 1);
+ }
+
+ private static int getFlagsFromAllowedIfds(int[] allowedIfds) {
+ if (allowedIfds == null || allowedIfds.length == 0) {
+ return 0;
+ }
+ int flags = 0;
+ int[] ifds = IfdData.getIfds();
+ for (int i = 0; i < IfdId.TYPE_IFD_COUNT; i++) {
+ for (int j : allowedIfds) {
+ if (ifds[i] == j) {
+ flags |= 1 << i;
+ break;
+ }
+ }
+ }
+ return flags;
+ }
+
+ private Integer getTagIntValue(int tagId, int ifdId) {
+ int[] l = getTagIntValues(tagId, ifdId);
+ if (l == null || l.length <= 0) {
+ return null;
+ }
+ return l[0];
+ }
+
+ private int[] getTagIntValues(int tagId, int ifdId) {
+ ExifTag t = getTag(tagId, ifdId);
+ if (t == null) {
+ return null;
+ }
+ return t.getValueAsInts();
+ }
+
+ /** Gets an ExifTag for an IFD other than the tag's default. */
+ public ExifTag getTag(int tagId, int ifdId) {
+ if (!ExifTag.isValidIfd(ifdId)) {
+ return null;
+ }
+ return mData.getTag(getTrueTagKey(tagId), ifdId);
+ }
+
+ public Integer getTagIntValue(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTagIntValue(tagId, ifdId);
+ }
+
+ /**
+ * Gets the default IFD for a tag.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_EXIF_IFD}.
+ * @return the default IFD for a tag definition or {@link #IFD_NULL} if no definition exists.
+ */
+ private int getDefinedTagDefaultIfd(int tagId) {
+ int info = getTagInfo().get(tagId);
+ if (info == DEFINITION_NULL) {
+ return IFD_NULL;
+ }
+ return getTrueIfd(tagId);
+ }
+
+ /** Returns the default IFD for a tag constant. */
+ private static int getTrueIfd(int tag) {
+ return tag >>> 16;
+ }
+
+ /**
+ * Constants for {@code TAG_ORIENTATION}. They can be interpreted as follows:
+ *
+ * <ul>
+ * <li>TOP_LEFT is the normal orientation.
+ * <li>TOP_RIGHT is a left-right mirror.
+ * <li>BOTTOM_LEFT is a 180 degree rotation.
+ * <li>BOTTOM_RIGHT is a top-bottom mirror.
+ * <li>LEFT_TOP is mirrored about the top-left<->bottom-right axis.
+ * <li>RIGHT_TOP is a 90 degree clockwise rotation.
+ * <li>LEFT_BOTTOM is mirrored about the top-right<->bottom-left axis.
+ * <li>RIGHT_BOTTOM is a 270 degree clockwise rotation.
+ * </ul>
+ */
+ interface Orientation {
+ short TOP_LEFT = 1;
+ short TOP_RIGHT = 2;
+ short BOTTOM_LEFT = 3;
+ short BOTTOM_RIGHT = 4;
+ short LEFT_TOP = 5;
+ short RIGHT_TOP = 6;
+ short LEFT_BOTTOM = 7;
+ short RIGHT_BOTTOM = 8;
+ }
+
+ /** Wrapper class to define some orientation parameters. */
+ public static class OrientationParams {
+ int rotation = 0;
+ int scaleX = 1;
+ int scaleY = 1;
+ public boolean invertDimensions = false;
+ }
+
+ public static OrientationParams getOrientationParams(int orientation) {
+ OrientationParams params = new OrientationParams();
+ switch (orientation) {
+ case Orientation.TOP_RIGHT: // Flip horizontal
+ params.scaleX = -1;
+ break;
+ case Orientation.BOTTOM_RIGHT: // Flip vertical
+ params.scaleY = -1;
+ break;
+ case Orientation.BOTTOM_LEFT: // Rotate 180
+ params.rotation = 180;
+ break;
+ case Orientation.RIGHT_BOTTOM: // Rotate 270
+ params.rotation = 270;
+ params.invertDimensions = true;
+ break;
+ case Orientation.RIGHT_TOP: // Rotate 90
+ params.rotation = 90;
+ params.invertDimensions = true;
+ break;
+ case Orientation.LEFT_TOP: // Transpose
+ params.rotation = 90;
+ params.scaleX = -1;
+ params.invertDimensions = true;
+ break;
+ case Orientation.LEFT_BOTTOM: // Transverse
+ params.rotation = 270;
+ params.scaleX = -1;
+ params.invertDimensions = true;
+ break;
+ }
+ return params;
+ }
+
+ /** Clears this ExifInterface object's existing exif tags. */
+ public void clearExif() {
+ mData = new ExifData();
+ }
+
+ /**
+ * Puts an ExifTag into this ExifInterface object's tags, removing a previous ExifTag with the
+ * same TID and IFD. The IFD it is put into will be the one the tag was created with in {@link
+ * #buildTag}.
+ *
+ * @param tag an ExifTag to put into this ExifInterface's tags.
+ * @return the previous ExifTag with the same TID and IFD or null if none exists.
+ */
+ public ExifTag setTag(ExifTag tag) {
+ return mData.addTag(tag);
+ }
+
+ /**
+ * Returns the ExifTag in that tag's default IFD for a defined tag constant or null if none
+ * exists.
+ *
+ * @param tagId a defined tag constant, e.g. {@link #TAG_EXIF_IFD}.
+ * @return an {@link ExifTag} or null if none exists.
+ */
+ public ExifTag getTag(int tagId) {
+ int ifdId = getDefinedTagDefaultIfd(tagId);
+ return getTag(tagId, ifdId);
+ }
+
+ /**
+ * Writes the tags from this ExifInterface object into a jpeg compressed bitmap, removing prior
+ * exif tags.
+ *
+ * @param bmap a bitmap to compress and write exif into.
+ * @param exifOutStream the OutputStream to which the jpeg image with added exif tags will be
+ * written.
+ * @throws java.io.IOException
+ */
+ public void writeExif(Bitmap bmap, OutputStream exifOutStream) throws IOException {
+ if (bmap == null || exifOutStream == null) {
+ throw new IllegalArgumentException(NULL_ARGUMENT_STRING);
+ }
+ bmap.compress(Bitmap.CompressFormat.JPEG, 90, exifOutStream);
+ exifOutStream.flush();
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java b/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java
new file mode 100644
index 000000000..92449d74f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java
@@ -0,0 +1,24 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+/** Exception for invalid exif formats. */
+public class ExifInvalidFormatException extends Exception {
+ ExifInvalidFormatException(String meg) {
+ super(meg);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java b/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java
new file mode 100644
index 000000000..23d748c17
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifParser.java
@@ -0,0 +1,846 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import android.annotation.SuppressLint;
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteOrder;
+import java.nio.charset.Charset;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+/**
+ * This class provides a low-level EXIF parsing API. Given a JPEG format InputStream, the caller can
+ * request which IFD's to read via {@link #parse(java.io.InputStream, int)} with given options.
+ *
+ * <p>Below is an example of getting EXIF data from IFD 0 and EXIF IFD using the parser.
+ *
+ * <pre>
+ * void parse() {
+ * ExifParser parser = ExifParser.parse(mImageInputStream,
+ * ExifParser.OPTION_IFD_0 | ExifParser.OPTIONS_IFD_EXIF);
+ * int event = parser.next();
+ * while (event != ExifParser.EVENT_END) {
+ * switch (event) {
+ * case ExifParser.EVENT_START_OF_IFD:
+ * break;
+ * case ExifParser.EVENT_NEW_TAG:
+ * ExifTag tag = parser.getTag();
+ * if (!tag.hasValue()) {
+ * parser.registerForTagValue(tag);
+ * } else {
+ * processTag(tag);
+ * }
+ * break;
+ * case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ * tag = parser.getTag();
+ * if (tag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ * processTag(tag);
+ * }
+ * break;
+ * }
+ * event = parser.next();
+ * }
+ * }
+ *
+ * void processTag(ExifTag tag) {
+ * // process the tag as you like.
+ * }
+ * </pre>
+ */
+public class ExifParser {
+ private static final boolean LOGV = false;
+ /**
+ * When the parser reaches a new IFD area. Call {@link #getCurrentIfd()} to know which IFD we are
+ * in.
+ */
+ static final int EVENT_START_OF_IFD = 0;
+ /** When the parser reaches a new tag. Call {@link #getTag()}to get the corresponding tag. */
+ static final int EVENT_NEW_TAG = 1;
+ /**
+ * When the parser reaches the value area of tag that is registered by {@link
+ * #registerForTagValue(ExifTag)} previously. Call {@link #getTag()} to get the corresponding tag.
+ */
+ static final int EVENT_VALUE_OF_REGISTERED_TAG = 2;
+
+ /** When the parser reaches the compressed image area. */
+ static final int EVENT_COMPRESSED_IMAGE = 3;
+ /**
+ * When the parser reaches the uncompressed image strip. Call {@link #getStripIndex()} to get the
+ * index of the strip.
+ *
+ * @see #getStripIndex()
+ */
+ static final int EVENT_UNCOMPRESSED_STRIP = 4;
+ /** When there is nothing more to parse. */
+ static final int EVENT_END = 5;
+
+ /** Option bit to request to parse IFD0. */
+ private static final int OPTION_IFD_0 = 1;
+ /** Option bit to request to parse IFD1. */
+ private static final int OPTION_IFD_1 = 1 << 1;
+ /** Option bit to request to parse Exif-IFD. */
+ private static final int OPTION_IFD_EXIF = 1 << 2;
+ /** Option bit to request to parse GPS-IFD. */
+ private static final int OPTION_IFD_GPS = 1 << 3;
+ /** Option bit to request to parse Interoperability-IFD. */
+ private static final int OPTION_IFD_INTEROPERABILITY = 1 << 4;
+ /** Option bit to request to parse thumbnail. */
+ private static final int OPTION_THUMBNAIL = 1 << 5;
+
+ private static final int EXIF_HEADER = 0x45786966; // EXIF header "Exif"
+ private static final short EXIF_HEADER_TAIL = (short) 0x0000; // EXIF header in APP1
+
+ // TIFF header
+ private static final short LITTLE_ENDIAN_TAG = (short) 0x4949; // "II"
+ private static final short BIG_ENDIAN_TAG = (short) 0x4d4d; // "MM"
+ private static final short TIFF_HEADER_TAIL = 0x002A;
+
+ private static final int TAG_SIZE = 12;
+ private static final int OFFSET_SIZE = 2;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+
+ private static final int DEFAULT_IFD0_OFFSET = 8;
+
+ private final CountedDataInputStream mTiffStream;
+ private final int mOptions;
+ private int mIfdStartOffset = 0;
+ private int mNumOfTagInIfd = 0;
+ private int mIfdType;
+ private ExifTag mTag;
+ private ImageEvent mImageEvent;
+ private ExifTag mStripSizeTag;
+ private ExifTag mJpegSizeTag;
+ private boolean mNeedToParseOffsetsInCurrentIfd;
+ private boolean mContainExifData = false;
+ private int mApp1End;
+ private byte[] mDataAboveIfd0;
+ private int mIfd0Position;
+ private final ExifInterface mInterface;
+
+ private static final short TAG_EXIF_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_EXIF_IFD);
+ private static final short TAG_GPS_IFD = ExifInterface.getTrueTagKey(ExifInterface.TAG_GPS_IFD);
+ private static final short TAG_INTEROPERABILITY_IFD =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_INTEROPERABILITY_IFD);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT);
+ private static final short TAG_JPEG_INTERCHANGE_FORMAT_LENGTH =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH);
+ private static final short TAG_STRIP_OFFSETS =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_OFFSETS);
+ private static final short TAG_STRIP_BYTE_COUNTS =
+ ExifInterface.getTrueTagKey(ExifInterface.TAG_STRIP_BYTE_COUNTS);
+
+ private final TreeMap<Integer, Object> mCorrespondingEvent = new TreeMap<>();
+
+ private boolean isIfdRequested(int ifdType) {
+ switch (ifdType) {
+ case IfdId.TYPE_IFD_0:
+ return (mOptions & OPTION_IFD_0) != 0;
+ case IfdId.TYPE_IFD_1:
+ return (mOptions & OPTION_IFD_1) != 0;
+ case IfdId.TYPE_IFD_EXIF:
+ return (mOptions & OPTION_IFD_EXIF) != 0;
+ case IfdId.TYPE_IFD_GPS:
+ return (mOptions & OPTION_IFD_GPS) != 0;
+ case IfdId.TYPE_IFD_INTEROPERABILITY:
+ return (mOptions & OPTION_IFD_INTEROPERABILITY) != 0;
+ }
+ return false;
+ }
+
+ private boolean isThumbnailRequested() {
+ return (mOptions & OPTION_THUMBNAIL) != 0;
+ }
+
+ private ExifParser(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ if (inputStream == null) {
+ throw new IOException("Null argument inputStream to ExifParser");
+ }
+ if (LOGV) {
+ LogUtil.v("ExifParser.ExifParser", "Reading exif...");
+ }
+ mInterface = iRef;
+ mContainExifData = seekTiffData(inputStream);
+ mTiffStream = new CountedDataInputStream(inputStream);
+ mOptions = options;
+ if (!mContainExifData) {
+ return;
+ }
+
+ parseTiffHeader();
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Invalid offset " + offset);
+ }
+ mIfd0Position = (int) offset;
+ mIfdType = IfdId.TYPE_IFD_0;
+ if (isIfdRequested(IfdId.TYPE_IFD_0) || needToParseOffsetsInCurrentIfd()) {
+ registerIfd(IfdId.TYPE_IFD_0, offset);
+ if (offset != DEFAULT_IFD0_OFFSET) {
+ mDataAboveIfd0 = new byte[(int) offset - DEFAULT_IFD0_OFFSET];
+ read(mDataAboveIfd0);
+ }
+ }
+ }
+
+ /**
+ * Parses the the given InputStream with the given options
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ */
+ protected static ExifParser parse(InputStream inputStream, int options, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(inputStream, options, iRef);
+ }
+
+ /**
+ * Parses the the given InputStream with default options; that is, every IFD and thumbnaill will
+ * be parsed.
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ * @see #parse(java.io.InputStream, int, ExifInterface)
+ */
+ protected static ExifParser parse(InputStream inputStream, ExifInterface iRef)
+ throws IOException, ExifInvalidFormatException {
+ return new ExifParser(
+ inputStream,
+ OPTION_IFD_0
+ | OPTION_IFD_1
+ | OPTION_IFD_EXIF
+ | OPTION_IFD_GPS
+ | OPTION_IFD_INTEROPERABILITY
+ | OPTION_THUMBNAIL,
+ iRef);
+ }
+
+ /**
+ * Moves the parser forward and returns the next parsing event
+ *
+ * @exception java.io.IOException
+ * @exception ExifInvalidFormatException
+ * @see #EVENT_START_OF_IFD
+ * @see #EVENT_NEW_TAG
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ * @see #EVENT_COMPRESSED_IMAGE
+ * @see #EVENT_UNCOMPRESSED_STRIP
+ * @see #EVENT_END
+ */
+ protected int next() throws IOException, ExifInvalidFormatException {
+ if (!mContainExifData) {
+ return EVENT_END;
+ }
+ int offset = mTiffStream.getReadByteCount();
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ if (offset < endOfTags) {
+ mTag = readTag();
+ if (mTag == null) {
+ return next();
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ checkOffsetOrImageTag(mTag);
+ }
+ return EVENT_NEW_TAG;
+ } else if (offset == endOfTags) {
+ // There is a link to ifd1 at the end of ifd0
+ if (mIfdType == IfdId.TYPE_IFD_0) {
+ long ifdOffset = readUnsignedLong();
+ if (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested()) {
+ if (ifdOffset != 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ } else {
+ int offsetSize = 4;
+ // Some camera models use invalid length of the offset
+ if (mCorrespondingEvent.size() > 0) {
+ offsetSize = mCorrespondingEvent.firstEntry().getKey() - mTiffStream.getReadByteCount();
+ }
+ if (offsetSize < 4) {
+ LogUtil.i("ExifParser.next", "Invalid size of link to next IFD: " + offsetSize);
+ } else {
+ long ifdOffset = readUnsignedLong();
+ if (ifdOffset != 0) {
+ LogUtil.i("ExifParser.next", "Invalid link to next IFD: " + ifdOffset);
+ }
+ }
+ }
+ }
+ while (mCorrespondingEvent.size() != 0) {
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ Object event = entry.getValue();
+ try {
+ skipTo(entry.getKey());
+ } catch (IOException e) {
+ LogUtil.i(
+ "ExifParser.next",
+ "Failed to skip to data at: "
+ + entry.getKey()
+ + " for "
+ + event.getClass().getName()
+ + ", the file may be broken.");
+ continue;
+ }
+ if (event instanceof IfdEvent) {
+ mIfdType = ((IfdEvent) event).ifd;
+ mNumOfTagInIfd = mTiffStream.readUnsignedShort();
+ mIfdStartOffset = entry.getKey();
+
+ if (mNumOfTagInIfd * TAG_SIZE + mIfdStartOffset + OFFSET_SIZE > mApp1End) {
+ LogUtil.i("ExifParser.next", "Invalid size of IFD " + mIfdType);
+ return EVENT_END;
+ }
+
+ mNeedToParseOffsetsInCurrentIfd = needToParseOffsetsInCurrentIfd();
+ if (((IfdEvent) event).isRequested) {
+ return EVENT_START_OF_IFD;
+ } else {
+ skipRemainingTagsInCurrentIfd();
+ }
+ } else if (event instanceof ImageEvent) {
+ mImageEvent = (ImageEvent) event;
+ return mImageEvent.type;
+ } else {
+ ExifTagEvent tagEvent = (ExifTagEvent) event;
+ mTag = tagEvent.tag;
+ if (mTag.getDataType() != ExifTag.TYPE_UNDEFINED) {
+ readFullTagValue(mTag);
+ checkOffsetOrImageTag(mTag);
+ }
+ if (tagEvent.isRequested) {
+ return EVENT_VALUE_OF_REGISTERED_TAG;
+ }
+ }
+ }
+ return EVENT_END;
+ }
+
+ /**
+ * Skips the tags area of current IFD, if the parser is not in the tag area, nothing will happen.
+ *
+ * @throws java.io.IOException
+ * @throws ExifInvalidFormatException
+ */
+ private void skipRemainingTagsInCurrentIfd() throws IOException, ExifInvalidFormatException {
+ int endOfTags = mIfdStartOffset + OFFSET_SIZE + TAG_SIZE * mNumOfTagInIfd;
+ int offset = mTiffStream.getReadByteCount();
+ if (offset > endOfTags) {
+ return;
+ }
+ if (mNeedToParseOffsetsInCurrentIfd) {
+ while (offset < endOfTags) {
+ mTag = readTag();
+ offset += TAG_SIZE;
+ if (mTag == null) {
+ continue;
+ }
+ checkOffsetOrImageTag(mTag);
+ }
+ } else {
+ skipTo(endOfTags);
+ }
+ long ifdOffset = readUnsignedLong();
+ // For ifd0, there is a link to ifd1 in the end of all tags
+ if (mIfdType == IfdId.TYPE_IFD_0
+ && (isIfdRequested(IfdId.TYPE_IFD_1) || isThumbnailRequested())) {
+ if (ifdOffset > 0) {
+ registerIfd(IfdId.TYPE_IFD_1, ifdOffset);
+ }
+ }
+ }
+
+ private boolean needToParseOffsetsInCurrentIfd() {
+ switch (mIfdType) {
+ case IfdId.TYPE_IFD_0:
+ return isIfdRequested(IfdId.TYPE_IFD_EXIF)
+ || isIfdRequested(IfdId.TYPE_IFD_GPS)
+ || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)
+ || isIfdRequested(IfdId.TYPE_IFD_1);
+ case IfdId.TYPE_IFD_1:
+ return isThumbnailRequested();
+ case IfdId.TYPE_IFD_EXIF:
+ // The offset to interoperability IFD is located in Exif IFD
+ return isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * If {@link #next()} return {@link #EVENT_NEW_TAG} or {@link #EVENT_VALUE_OF_REGISTERED_TAG},
+ * call this function to get the corresponding tag.
+ *
+ * <p>For {@link #EVENT_NEW_TAG}, the tag may not contain the value if the size of the value is
+ * greater than 4 bytes. One should call {@link ExifTag#hasValue()} to check if the tag contains
+ * value. If there is no value,call {@link #registerForTagValue(ExifTag)} to have the parser emit
+ * {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches the area pointed by the offset.
+ *
+ * <p>When {@link #EVENT_VALUE_OF_REGISTERED_TAG} is emitted, the value of the tag will have
+ * already been read except for tags of undefined type. For tags of undefined type, call one of
+ * the read methods to get the value.
+ *
+ * @see #registerForTagValue(ExifTag)
+ * @see #read(byte[])
+ * @see #read(byte[], int, int)
+ * @see #readLong()
+ * @see #readRational()
+ * @see #readString(int)
+ * @see #readString(int, java.nio.charset.Charset)
+ */
+ protected ExifTag getTag() {
+ return mTag;
+ }
+
+ /**
+ * Gets the ID of current IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ * @see IfdId#TYPE_IFD_EXIF
+ */
+ int getCurrentIfd() {
+ return mIfdType;
+ }
+
+ /**
+ * When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the index of this
+ * strip.
+ */
+ int getStripIndex() {
+ return mImageEvent.stripIndex;
+ }
+
+ /** When receiving {@link #EVENT_UNCOMPRESSED_STRIP}, call this function to get the strip size. */
+ int getStripSize() {
+ if (mStripSizeTag == null) {
+ return 0;
+ }
+ return (int) mStripSizeTag.getValueAt(0);
+ }
+
+ /**
+ * When receiving {@link #EVENT_COMPRESSED_IMAGE}, call this function to get the image data size.
+ */
+ int getCompressedImageSize() {
+ if (mJpegSizeTag == null) {
+ return 0;
+ }
+ return (int) mJpegSizeTag.getValueAt(0);
+ }
+
+ private void skipTo(int offset) throws IOException {
+ mTiffStream.skipTo(offset);
+ while (!mCorrespondingEvent.isEmpty() && mCorrespondingEvent.firstKey() < offset) {
+ mCorrespondingEvent.pollFirstEntry();
+ }
+ }
+
+ /**
+ * When getting {@link #EVENT_NEW_TAG} in the tag area of IFD, the tag may not contain the value
+ * if the size of the value is greater than 4 bytes. When the value is not available here, call
+ * this method so that the parser will emit {@link #EVENT_VALUE_OF_REGISTERED_TAG} when it reaches
+ * the area where the value is located.
+ *
+ * @see #EVENT_VALUE_OF_REGISTERED_TAG
+ */
+ void registerForTagValue(ExifTag tag) {
+ if (tag.getOffset() >= mTiffStream.getReadByteCount()) {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, true));
+ }
+ }
+
+ private void registerIfd(int ifdType, long offset) {
+ // Cast unsigned int to int since the offset is always smaller
+ // than the size of APP1 (65536)
+ mCorrespondingEvent.put((int) offset, new IfdEvent(ifdType, isIfdRequested(ifdType)));
+ }
+
+ private void registerCompressedImage(long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_COMPRESSED_IMAGE));
+ }
+
+ private void registerUncompressedStrip(int stripIndex, long offset) {
+ mCorrespondingEvent.put((int) offset, new ImageEvent(EVENT_UNCOMPRESSED_STRIP, stripIndex));
+ }
+
+ @SuppressLint("DefaultLocale")
+ private ExifTag readTag() throws IOException, ExifInvalidFormatException {
+ short tagId = mTiffStream.readShort();
+ short dataFormat = mTiffStream.readShort();
+ long numOfComp = mTiffStream.readUnsignedInt();
+ if (numOfComp > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("Number of component is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid image file contains invalid data type. Ignore those tags
+ if (!ExifTag.isValidType(dataFormat)) {
+ LogUtil.i("ExifParser.readTag", "Tag %04x: Invalid data type %d", tagId, dataFormat);
+ mTiffStream.skip(4);
+ return null;
+ }
+ // TODO: handle numOfComp overflow
+ ExifTag tag =
+ new ExifTag(
+ tagId,
+ dataFormat,
+ (int) numOfComp,
+ mIfdType,
+ ((int) numOfComp) != ExifTag.SIZE_UNDEFINED);
+ int dataSize = tag.getDataSize();
+ if (dataSize > 4) {
+ long offset = mTiffStream.readUnsignedInt();
+ if (offset > Integer.MAX_VALUE) {
+ throw new ExifInvalidFormatException("offset is larger then Integer.MAX_VALUE");
+ }
+ // Some invalid images put some undefined data before IFD0.
+ // Read the data here.
+ if ((offset < mIfd0Position) && (dataFormat == ExifTag.TYPE_UNDEFINED)) {
+ byte[] buf = new byte[(int) numOfComp];
+ System.arraycopy(
+ mDataAboveIfd0, (int) offset - DEFAULT_IFD0_OFFSET, buf, 0, (int) numOfComp);
+ tag.setValue(buf);
+ } else {
+ tag.setOffset((int) offset);
+ }
+ } else {
+ boolean defCount = tag.hasDefinedCount();
+ // Set defined count to 0 so we can add \0 to non-terminated strings
+ tag.setHasDefinedCount(false);
+ // Read value
+ readFullTagValue(tag);
+ tag.setHasDefinedCount(defCount);
+ mTiffStream.skip(4 - dataSize);
+ // Set the offset to the position of value.
+ tag.setOffset(mTiffStream.getReadByteCount() - 4);
+ }
+ return tag;
+ }
+
+ /**
+ * Check the if the tag is one of the offset tag that points to the IFD or image the caller is
+ * interested in, register the IFD or image.
+ */
+ private void checkOffsetOrImageTag(ExifTag tag) {
+ // Some invalid formattd image contains tag with 0 size.
+ if (tag.getComponentCount() == 0) {
+ return;
+ }
+ short tid = tag.getTagId();
+ int ifd = tag.getIfd();
+ if (tid == TAG_EXIF_IFD && checkAllowed(ifd, ExifInterface.TAG_EXIF_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_EXIF) || isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_EXIF, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_GPS_IFD && checkAllowed(ifd, ExifInterface.TAG_GPS_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_GPS)) {
+ registerIfd(IfdId.TYPE_IFD_GPS, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_INTEROPERABILITY_IFD
+ && checkAllowed(ifd, ExifInterface.TAG_INTEROPERABILITY_IFD)) {
+ if (isIfdRequested(IfdId.TYPE_IFD_INTEROPERABILITY)) {
+ registerIfd(IfdId.TYPE_IFD_INTEROPERABILITY, tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT)) {
+ if (isThumbnailRequested()) {
+ registerCompressedImage(tag.getValueAt(0));
+ }
+ } else if (tid == TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
+ && checkAllowed(ifd, ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH)) {
+ if (isThumbnailRequested()) {
+ mJpegSizeTag = tag;
+ }
+ } else if (tid == TAG_STRIP_OFFSETS && checkAllowed(ifd, ExifInterface.TAG_STRIP_OFFSETS)) {
+ if (isThumbnailRequested()) {
+ if (tag.hasValue()) {
+ for (int i = 0; i < tag.getComponentCount(); i++) {
+ if (tag.getDataType() == ExifTag.TYPE_UNSIGNED_SHORT) {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ } else {
+ registerUncompressedStrip(i, tag.getValueAt(i));
+ }
+ }
+ } else {
+ mCorrespondingEvent.put(tag.getOffset(), new ExifTagEvent(tag, false));
+ }
+ }
+ } else if (tid == TAG_STRIP_BYTE_COUNTS
+ && checkAllowed(ifd, ExifInterface.TAG_STRIP_BYTE_COUNTS)
+ && isThumbnailRequested()
+ && tag.hasValue()) {
+ mStripSizeTag = tag;
+ }
+ }
+
+ private boolean checkAllowed(int ifd, int tagId) {
+ int info = mInterface.getTagInfo().get(tagId);
+ return info != ExifInterface.DEFINITION_NULL && ExifInterface.isIfdAllowed(info, ifd);
+ }
+
+ void readFullTagValue(ExifTag tag) throws IOException {
+ // Some invalid images contains tags with wrong size, check it here
+ short type = tag.getDataType();
+ if (type == ExifTag.TYPE_ASCII
+ || type == ExifTag.TYPE_UNDEFINED
+ || type == ExifTag.TYPE_UNSIGNED_BYTE) {
+ int size = tag.getComponentCount();
+ if (mCorrespondingEvent.size() > 0) {
+ if (mCorrespondingEvent.firstEntry().getKey() < mTiffStream.getReadByteCount() + size) {
+ Object event = mCorrespondingEvent.firstEntry().getValue();
+ if (event instanceof ImageEvent) {
+ // Tag value overlaps thumbnail, ignore thumbnail.
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Thumbnail overlaps value for tag: \n" + tag.toString());
+ Entry<Integer, Object> entry = mCorrespondingEvent.pollFirstEntry();
+ LogUtil.i("ExifParser.readFullTagValue", "Invalid thumbnail offset: " + entry.getKey());
+ } else {
+ // Tag value overlaps another shorten count
+ if (event instanceof IfdEvent) {
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Ifd " + ((IfdEvent) event).ifd + " overlaps value for tag: \n" + tag.toString());
+ } else if (event instanceof ExifTagEvent) {
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Tag value for tag: \n"
+ + ((ExifTagEvent) event).tag.toString()
+ + " overlaps value for tag: \n"
+ + tag.toString());
+ }
+ size = mCorrespondingEvent.firstEntry().getKey() - mTiffStream.getReadByteCount();
+ LogUtil.i(
+ "ExifParser.readFullTagValue",
+ "Invalid size of tag: \n" + tag.toString() + " setting count to: " + size);
+ tag.forceSetComponentCount(size);
+ }
+ }
+ }
+ }
+ switch (tag.getDataType()) {
+ case ExifTag.TYPE_UNSIGNED_BYTE:
+ case ExifTag.TYPE_UNDEFINED:
+ {
+ byte[] buf = new byte[tag.getComponentCount()];
+ read(buf);
+ tag.setValue(buf);
+ }
+ break;
+ case ExifTag.TYPE_ASCII:
+ tag.setValue(readString(tag.getComponentCount()));
+ break;
+ case ExifTag.TYPE_UNSIGNED_LONG:
+ {
+ long[] value = new long[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_RATIONAL:
+ {
+ Rational[] value = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_UNSIGNED_SHORT:
+ {
+ int[] value = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readUnsignedShort();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_LONG:
+ {
+ int[] value = new int[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readLong();
+ }
+ tag.setValue(value);
+ }
+ break;
+ case ExifTag.TYPE_RATIONAL:
+ {
+ Rational[] value = new Rational[tag.getComponentCount()];
+ for (int i = 0, n = value.length; i < n; i++) {
+ value[i] = readRational();
+ }
+ tag.setValue(value);
+ }
+ break;
+ }
+ if (LOGV) {
+ LogUtil.v("ExifParser.readFullTagValue", "\n" + tag.toString());
+ }
+ }
+
+ private void parseTiffHeader() throws IOException, ExifInvalidFormatException {
+ short byteOrder = mTiffStream.readShort();
+ if (LITTLE_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.LITTLE_ENDIAN);
+ } else if (BIG_ENDIAN_TAG == byteOrder) {
+ mTiffStream.setByteOrder(ByteOrder.BIG_ENDIAN);
+ } else {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+
+ if (mTiffStream.readShort() != TIFF_HEADER_TAIL) {
+ throw new ExifInvalidFormatException("Invalid TIFF header");
+ }
+ }
+
+ private boolean seekTiffData(InputStream inputStream)
+ throws IOException, ExifInvalidFormatException {
+ CountedDataInputStream dataStream = new CountedDataInputStream(inputStream);
+ if (dataStream.readShort() != JpegHeader.SOI) {
+ throw new ExifInvalidFormatException("Invalid JPEG format");
+ }
+
+ short marker = dataStream.readShort();
+ while (marker != JpegHeader.EOI && !JpegHeader.isSofMarker(marker)) {
+ int length = dataStream.readUnsignedShort();
+ // Some invalid formatted image contains multiple APP1,
+ // try to find the one with Exif data.
+ if (marker == JpegHeader.APP1) {
+ int header;
+ short headerTail;
+ if (length >= 8) {
+ header = dataStream.readInt();
+ headerTail = dataStream.readShort();
+ length -= 6;
+ if (header == EXIF_HEADER && headerTail == EXIF_HEADER_TAIL) {
+ mApp1End = length;
+ return true;
+ }
+ }
+ }
+ if (length < 2 || (length - 2) != dataStream.skip(length - 2)) {
+ LogUtil.i("ExifParser.seekTiffData", "Invalid JPEG format.");
+ return false;
+ }
+ marker = dataStream.readShort();
+ }
+ return false;
+ }
+
+ /** Reads bytes from the InputStream. */
+ protected int read(byte[] buffer, int offset, int length) throws IOException {
+ return mTiffStream.read(buffer, offset, length);
+ }
+
+ /** Equivalent to read(buffer, 0, buffer.length). */
+ protected int read(byte[] buffer) throws IOException {
+ return mTiffStream.read(buffer);
+ }
+
+ /**
+ * Reads a String from the InputStream with US-ASCII charset. The parser will read n bytes and
+ * convert it to ascii string. This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ private String readString(int n) throws IOException {
+ return readString(n, US_ASCII);
+ }
+
+ /**
+ * Reads a String from the InputStream with the given charset. The parser will read n bytes and
+ * convert it to string. This is used for reading values of type {@link ExifTag#TYPE_ASCII}.
+ */
+ private String readString(int n, Charset charset) throws IOException {
+ if (n > 0) {
+ return mTiffStream.readString(n, charset);
+ } else {
+ return "";
+ }
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_SHORT} from the InputStream. */
+ private int readUnsignedShort() throws IOException {
+ return mTiffStream.readShort() & 0xffff;
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_LONG} from the InputStream. */
+ private long readUnsignedLong() throws IOException {
+ return readLong() & 0xffffffffL;
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_UNSIGNED_RATIONAL} from the InputStream. */
+ private Rational readUnsignedRational() throws IOException {
+ long nomi = readUnsignedLong();
+ long denomi = readUnsignedLong();
+ return new Rational(nomi, denomi);
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_LONG} from the InputStream. */
+ private int readLong() throws IOException {
+ return mTiffStream.readInt();
+ }
+
+ /** Reads value of type {@link ExifTag#TYPE_RATIONAL} from the InputStream. */
+ private Rational readRational() throws IOException {
+ int nomi = readLong();
+ int denomi = readLong();
+ return new Rational(nomi, denomi);
+ }
+
+ private static class ImageEvent {
+ int stripIndex;
+ int type;
+
+ ImageEvent(int type) {
+ this.stripIndex = 0;
+ this.type = type;
+ }
+
+ ImageEvent(int type, int stripIndex) {
+ this.type = type;
+ this.stripIndex = stripIndex;
+ }
+ }
+
+ private static class IfdEvent {
+ int ifd;
+ boolean isRequested;
+
+ IfdEvent(int ifd, boolean isInterestedIfd) {
+ this.ifd = ifd;
+ this.isRequested = isInterestedIfd;
+ }
+ }
+
+ private static class ExifTagEvent {
+ ExifTag tag;
+ boolean isRequested;
+
+ ExifTagEvent(ExifTag tag, boolean isRequireByUser) {
+ this.tag = tag;
+ this.isRequested = isRequireByUser;
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java b/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java
new file mode 100644
index 000000000..89d212661
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifReader.java
@@ -0,0 +1,81 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import com.android.dialer.common.LogUtil;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** This class reads the EXIF header of a JPEG file and stores it in {@link ExifData}. */
+class ExifReader {
+
+ private final ExifInterface mInterface;
+
+ ExifReader(ExifInterface iRef) {
+ mInterface = iRef;
+ }
+
+ /**
+ * Parses the inputStream and and returns the EXIF data in an {@link ExifData}.
+ *
+ * @throws ExifInvalidFormatException
+ * @throws java.io.IOException
+ */
+ protected ExifData read(InputStream inputStream) throws ExifInvalidFormatException, IOException {
+ ExifParser parser = ExifParser.parse(inputStream, mInterface);
+ ExifData exifData = new ExifData();
+ ExifTag tag;
+
+ int event = parser.next();
+ while (event != ExifParser.EVENT_END) {
+ switch (event) {
+ case ExifParser.EVENT_START_OF_IFD:
+ exifData.addIfdData(new IfdData(parser.getCurrentIfd()));
+ break;
+ case ExifParser.EVENT_NEW_TAG:
+ tag = parser.getTag();
+ if (!tag.hasValue()) {
+ parser.registerForTagValue(tag);
+ } else {
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ }
+ break;
+ case ExifParser.EVENT_VALUE_OF_REGISTERED_TAG:
+ tag = parser.getTag();
+ if (tag.getDataType() == ExifTag.TYPE_UNDEFINED) {
+ parser.readFullTagValue(tag);
+ }
+ exifData.getIfdData(tag.getIfd()).setTag(tag);
+ break;
+ case ExifParser.EVENT_COMPRESSED_IMAGE:
+ byte[] buf = new byte[parser.getCompressedImageSize()];
+ if (buf.length != parser.read(buf)) {
+ LogUtil.i("ExifReader.read", "Failed to read the compressed thumbnail");
+ }
+ break;
+ case ExifParser.EVENT_UNCOMPRESSED_STRIP:
+ buf = new byte[parser.getStripSize()];
+ if (buf.length != parser.read(buf)) {
+ LogUtil.i("ExifReader.read", "Failed to read the strip bytes");
+ }
+ break;
+ }
+ event = parser.next();
+ }
+ return exifData;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java b/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java
new file mode 100644
index 000000000..a254ae93b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/ExifTag.java
@@ -0,0 +1,619 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This class stores information of an EXIF tag. For more information about defined EXIF tags,
+ * please read the Jeita EXIF 2.2 standard. Tags should be instantiated using {@link
+ * ExifInterface#buildTag}.
+ *
+ * @see ExifInterface
+ */
+public class ExifTag {
+ /** The BYTE type in the EXIF standard. An 8-bit unsigned integer. */
+ static final short TYPE_UNSIGNED_BYTE = 1;
+ /**
+ * The ASCII type in the EXIF standard. An 8-bit byte containing one 7-bit ASCII code. The final
+ * byte is terminated with NULL.
+ */
+ static final short TYPE_ASCII = 2;
+ /** The SHORT type in the EXIF standard. A 16-bit (2-byte) unsigned integer */
+ static final short TYPE_UNSIGNED_SHORT = 3;
+ /** The LONG type in the EXIF standard. A 32-bit (4-byte) unsigned integer */
+ static final short TYPE_UNSIGNED_LONG = 4;
+ /**
+ * The RATIONAL type of EXIF standard. It consists of two LONGs. The first one is the numerator
+ * and the second one expresses the denominator.
+ */
+ static final short TYPE_UNSIGNED_RATIONAL = 5;
+ /**
+ * The UNDEFINED type in the EXIF standard. An 8-bit byte that can take any value depending on the
+ * field definition.
+ */
+ static final short TYPE_UNDEFINED = 7;
+ /**
+ * The SLONG type in the EXIF standard. A 32-bit (4-byte) signed integer (2's complement
+ * notation).
+ */
+ static final short TYPE_LONG = 9;
+ /**
+ * The SRATIONAL type of EXIF standard. It consists of two SLONGs. The first one is the numerator
+ * and the second one is the denominator.
+ */
+ static final short TYPE_RATIONAL = 10;
+
+ private static final Charset US_ASCII = Charset.forName("US-ASCII");
+ private static final int[] TYPE_TO_SIZE_MAP = new int[11];
+ private static final int UNSIGNED_SHORT_MAX = 65535;
+ private static final long UNSIGNED_LONG_MAX = 4294967295L;
+ private static final long LONG_MAX = Integer.MAX_VALUE;
+ private static final long LONG_MIN = Integer.MIN_VALUE;
+
+ static {
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_BYTE] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_ASCII] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_SHORT] = 2;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_UNSIGNED_RATIONAL] = 8;
+ TYPE_TO_SIZE_MAP[TYPE_UNDEFINED] = 1;
+ TYPE_TO_SIZE_MAP[TYPE_LONG] = 4;
+ TYPE_TO_SIZE_MAP[TYPE_RATIONAL] = 8;
+ }
+
+ static final int SIZE_UNDEFINED = 0;
+
+ // Exif TagId
+ private final short mTagId;
+ // Exif Tag Type
+ private final short mDataType;
+ // If tag has defined count
+ private boolean mHasDefinedDefaultComponentCount;
+ // Actual data count in tag (should be number of elements in value array)
+ private int mComponentCountActual;
+ // The ifd that this tag should be put in
+ private int mIfd;
+ // The value (array of elements of type Tag Type)
+ private Object mValue;
+ // Value offset in exif header.
+ private int mOffset;
+
+ /** Returns true if the given IFD is a valid IFD. */
+ static boolean isValidIfd(int ifdId) {
+ return ifdId == IfdId.TYPE_IFD_0
+ || ifdId == IfdId.TYPE_IFD_1
+ || ifdId == IfdId.TYPE_IFD_EXIF
+ || ifdId == IfdId.TYPE_IFD_INTEROPERABILITY
+ || ifdId == IfdId.TYPE_IFD_GPS;
+ }
+
+ /** Returns true if a given type is a valid tag type. */
+ static boolean isValidType(short type) {
+ return type == TYPE_UNSIGNED_BYTE
+ || type == TYPE_ASCII
+ || type == TYPE_UNSIGNED_SHORT
+ || type == TYPE_UNSIGNED_LONG
+ || type == TYPE_UNSIGNED_RATIONAL
+ || type == TYPE_UNDEFINED
+ || type == TYPE_LONG
+ || type == TYPE_RATIONAL;
+ }
+
+ // Use builtTag in ExifInterface instead of constructor.
+ ExifTag(short tagId, short type, int componentCount, int ifd, boolean hasDefinedComponentCount) {
+ mTagId = tagId;
+ mDataType = type;
+ mComponentCountActual = componentCount;
+ mHasDefinedDefaultComponentCount = hasDefinedComponentCount;
+ mIfd = ifd;
+ mValue = null;
+ }
+
+ /**
+ * Gets the element size of the given data type in bytes.
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ private static int getElementSize(short type) {
+ return TYPE_TO_SIZE_MAP[type];
+ }
+
+ /**
+ * Returns the ID of the IFD this tag belongs to.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ int getIfd() {
+ return mIfd;
+ }
+
+ void setIfd(int ifdId) {
+ mIfd = ifdId;
+ }
+
+ /** Gets the TID of this tag. */
+ short getTagId() {
+ return mTagId;
+ }
+
+ /**
+ * Gets the data type of this tag
+ *
+ * @see #TYPE_ASCII
+ * @see #TYPE_LONG
+ * @see #TYPE_RATIONAL
+ * @see #TYPE_UNDEFINED
+ * @see #TYPE_UNSIGNED_BYTE
+ * @see #TYPE_UNSIGNED_LONG
+ * @see #TYPE_UNSIGNED_RATIONAL
+ * @see #TYPE_UNSIGNED_SHORT
+ */
+ short getDataType() {
+ return mDataType;
+ }
+
+ /** Gets the total data size in bytes of the value of this tag. */
+ int getDataSize() {
+ return getComponentCount() * getElementSize(getDataType());
+ }
+
+ /** Gets the component count of this tag. */
+
+ // TODO: fix integer overflows with this
+ int getComponentCount() {
+ return mComponentCountActual;
+ }
+
+ /**
+ * Sets the component count of this tag. Call this function before setValue() if the length of
+ * value does not match the component count.
+ */
+ void forceSetComponentCount(int count) {
+ mComponentCountActual = count;
+ }
+
+ /**
+ * Returns true if this ExifTag contains value; otherwise, this tag will contain an offset value
+ * that is determined when the tag is written.
+ */
+ boolean hasValue() {
+ return mValue != null;
+ }
+
+ /**
+ * Sets integer values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_SHORT}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_SHORT}, {@link
+ * #TYPE_UNSIGNED_LONG}, or {@link #TYPE_LONG}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ boolean setValue(int[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_SHORT
+ && mDataType != TYPE_LONG
+ && mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_SHORT && checkOverflowForUnsignedShort(value)) {
+ return false;
+ } else if (mDataType == TYPE_UNSIGNED_LONG && checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+
+ long[] data = new long[value.length];
+ for (int i = 0; i < value.length; i++) {
+ data[i] = value[i];
+ }
+ mValue = data;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets long values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_LONG}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_LONG}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ boolean setValue(long[] value) {
+ if (checkBadComponentCount(value.length) || mDataType != TYPE_UNSIGNED_LONG) {
+ return false;
+ }
+ if (checkOverflowForUnsignedLong(value)) {
+ return false;
+ }
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets a string value into this tag. This method should be used for tags of type {@link
+ * #TYPE_ASCII}. The string is converted to an ASCII string. Characters that cannot be converted
+ * are replaced with '?'. The length of the string must be equal to either (component count -1) or
+ * (component count). The final byte will be set to the string null terminator '\0', overwriting
+ * the last character in the string if the value.length is equal to the component count. This
+ * method will fail if:
+ *
+ * <ul>
+ * <li>The data type is not {@link #TYPE_ASCII} or {@link #TYPE_UNDEFINED}.
+ * <li>The length of the string is not equal to (component count -1) or (component count) in the
+ * definition for this tag.
+ * </ul>
+ */
+ boolean setValue(String value) {
+ if (mDataType != TYPE_ASCII && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+
+ byte[] buf = value.getBytes(US_ASCII);
+ byte[] finalBuf = buf;
+ if (buf.length > 0) {
+ finalBuf =
+ (buf[buf.length - 1] == 0 || mDataType == TYPE_UNDEFINED)
+ ? buf
+ : Arrays.copyOf(buf, buf.length + 1);
+ } else if (mDataType == TYPE_ASCII && mComponentCountActual == 1) {
+ finalBuf = new byte[] {0};
+ }
+ int count = finalBuf.length;
+ if (checkBadComponentCount(count)) {
+ return false;
+ }
+ mComponentCountActual = count;
+ mValue = finalBuf;
+ return true;
+ }
+
+ /**
+ * Sets Rational values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_RATIONAL}, or {@link #TYPE_RATIONAL}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_RATIONAL} or {@link
+ * #TYPE_RATIONAL}.
+ * <li>The value overflows.
+ * <li>The value.length does NOT match the component count in the definition for this tag.
+ * </ul>
+ *
+ * @see Rational
+ */
+ boolean setValue(Rational[] value) {
+ if (checkBadComponentCount(value.length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_RATIONAL && mDataType != TYPE_RATIONAL) {
+ return false;
+ }
+ if (mDataType == TYPE_UNSIGNED_RATIONAL && checkOverflowForUnsignedRational(value)) {
+ return false;
+ } else if (mDataType == TYPE_RATIONAL && checkOverflowForRational(value)) {
+ return false;
+ }
+
+ mValue = value;
+ mComponentCountActual = value.length;
+ return true;
+ }
+
+ /**
+ * Sets byte values into this tag. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_BYTE} or {@link #TYPE_UNDEFINED}. This method will fail if:
+ *
+ * <ul>
+ * <li>The component type of this tag is not {@link #TYPE_UNSIGNED_BYTE} or {@link
+ * #TYPE_UNDEFINED} .
+ * <li>The length does NOT match the component count in the definition for this tag.
+ * </ul>
+ */
+ private boolean setValue(byte[] value, int offset, int length) {
+ if (checkBadComponentCount(length)) {
+ return false;
+ }
+ if (mDataType != TYPE_UNSIGNED_BYTE && mDataType != TYPE_UNDEFINED) {
+ return false;
+ }
+ mValue = new byte[length];
+ System.arraycopy(value, offset, mValue, 0, length);
+ mComponentCountActual = length;
+ return true;
+ }
+
+ /** Equivalent to setValue(value, 0, value.length). */
+ boolean setValue(byte[] value) {
+ return setValue(value, 0, value.length);
+ }
+
+ /**
+ * Gets the value as an array of ints. This method should be used for tags of type {@link
+ * #TYPE_UNSIGNED_SHORT}, {@link #TYPE_UNSIGNED_LONG}.
+ *
+ * @return the value as as an array of ints, or null if the tag's value does not exist or cannot
+ * be converted to an array of ints.
+ */
+ int[] getValueAsInts() {
+ if (mValue == null) {
+ return null;
+ } else if (mValue instanceof long[]) {
+ long[] val = (long[]) mValue;
+ int[] arr = new int[val.length];
+ for (int i = 0; i < val.length; i++) {
+ arr[i] = (int) val[i]; // Truncates
+ }
+ return arr;
+ }
+ return null;
+ }
+
+ /** Gets the tag's value or null if none exists. */
+ public Object getValue() {
+ return mValue;
+ }
+
+ /** Gets a string representation of the value. */
+ private String forceGetValueAsString() {
+ if (mValue == null) {
+ return "";
+ } else if (mValue instanceof byte[]) {
+ if (mDataType == TYPE_ASCII) {
+ return new String((byte[]) mValue, US_ASCII);
+ } else {
+ return Arrays.toString((byte[]) mValue);
+ }
+ } else if (mValue instanceof long[]) {
+ if (((long[]) mValue).length == 1) {
+ return String.valueOf(((long[]) mValue)[0]);
+ } else {
+ return Arrays.toString((long[]) mValue);
+ }
+ } else if (mValue instanceof Object[]) {
+ if (((Object[]) mValue).length == 1) {
+ Object val = ((Object[]) mValue)[0];
+ if (val == null) {
+ return "";
+ } else {
+ return val.toString();
+ }
+ } else {
+ return Arrays.toString((Object[]) mValue);
+ }
+ } else {
+ return mValue.toString();
+ }
+ }
+
+ /**
+ * Gets the value for type {@link #TYPE_ASCII}, {@link #TYPE_LONG}, {@link #TYPE_UNDEFINED},
+ * {@link #TYPE_UNSIGNED_BYTE}, {@link #TYPE_UNSIGNED_LONG}, or {@link #TYPE_UNSIGNED_SHORT}.
+ *
+ * @exception IllegalArgumentException if the data type is {@link #TYPE_RATIONAL} or {@link
+ * #TYPE_UNSIGNED_RATIONAL}.
+ */
+ long getValueAt(int index) {
+ if (mValue instanceof long[]) {
+ return ((long[]) mValue)[index];
+ } else if (mValue instanceof byte[]) {
+ return ((byte[]) mValue)[index];
+ }
+ throw new IllegalArgumentException(
+ "Cannot get integer value from " + convertTypeToString(mDataType));
+ }
+
+ /**
+ * Gets the {@link #TYPE_ASCII} data.
+ *
+ * @exception IllegalArgumentException If the type is NOT {@link #TYPE_ASCII}.
+ */
+ protected String getString() {
+ if (mDataType != TYPE_ASCII) {
+ throw new IllegalArgumentException(
+ "Cannot get ASCII value from " + convertTypeToString(mDataType));
+ }
+ return new String((byte[]) mValue, US_ASCII);
+ }
+
+ /**
+ * Gets the offset of this tag. This is only valid if this data size > 4 and contains an offset to
+ * the location of the actual value.
+ */
+ protected int getOffset() {
+ return mOffset;
+ }
+
+ /** Sets the offset of this tag. */
+ protected void setOffset(int offset) {
+ mOffset = offset;
+ }
+
+ void setHasDefinedCount(boolean d) {
+ mHasDefinedDefaultComponentCount = d;
+ }
+
+ boolean hasDefinedCount() {
+ return mHasDefinedDefaultComponentCount;
+ }
+
+ private boolean checkBadComponentCount(int count) {
+ return mHasDefinedDefaultComponentCount && (mComponentCountActual != count);
+ }
+
+ private static String convertTypeToString(short type) {
+ switch (type) {
+ case TYPE_UNSIGNED_BYTE:
+ return "UNSIGNED_BYTE";
+ case TYPE_ASCII:
+ return "ASCII";
+ case TYPE_UNSIGNED_SHORT:
+ return "UNSIGNED_SHORT";
+ case TYPE_UNSIGNED_LONG:
+ return "UNSIGNED_LONG";
+ case TYPE_UNSIGNED_RATIONAL:
+ return "UNSIGNED_RATIONAL";
+ case TYPE_UNDEFINED:
+ return "UNDEFINED";
+ case TYPE_LONG:
+ return "LONG";
+ case TYPE_RATIONAL:
+ return "RATIONAL";
+ default:
+ return "";
+ }
+ }
+
+ private boolean checkOverflowForUnsignedShort(int[] value) {
+ for (int v : value) {
+ if (v > UNSIGNED_SHORT_MAX || v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(long[] value) {
+ for (long v : value) {
+ if (v < 0 || v > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedLong(int[] value) {
+ for (int v : value) {
+ if (v < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForUnsignedRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < 0
+ || v.getDenominator() < 0
+ || v.getNumerator() > UNSIGNED_LONG_MAX
+ || v.getDenominator() > UNSIGNED_LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean checkOverflowForRational(Rational[] value) {
+ for (Rational v : value) {
+ if (v.getNumerator() < LONG_MIN
+ || v.getDenominator() < LONG_MIN
+ || v.getNumerator() > LONG_MAX
+ || v.getDenominator() > LONG_MAX) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof ExifTag) {
+ ExifTag tag = (ExifTag) obj;
+ if (tag.mTagId != this.mTagId
+ || tag.mComponentCountActual != this.mComponentCountActual
+ || tag.mDataType != this.mDataType) {
+ return false;
+ }
+ if (mValue != null) {
+ if (tag.mValue == null) {
+ return false;
+ } else if (mValue instanceof long[]) {
+ if (!(tag.mValue instanceof long[])) {
+ return false;
+ }
+ return Arrays.equals((long[]) mValue, (long[]) tag.mValue);
+ } else if (mValue instanceof Rational[]) {
+ if (!(tag.mValue instanceof Rational[])) {
+ return false;
+ }
+ return Arrays.equals((Rational[]) mValue, (Rational[]) tag.mValue);
+ } else if (mValue instanceof byte[]) {
+ if (!(tag.mValue instanceof byte[])) {
+ return false;
+ }
+ return Arrays.equals((byte[]) mValue, (byte[]) tag.mValue);
+ } else {
+ return mValue.equals(tag.mValue);
+ }
+ } else {
+ return tag.mValue == null;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mTagId,
+ mDataType,
+ mHasDefinedDefaultComponentCount,
+ mComponentCountActual,
+ mIfd,
+ mValue,
+ mOffset);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("tag id: %04X\n", mTagId)
+ + "ifd id: "
+ + mIfd
+ + "\ntype: "
+ + convertTypeToString(mDataType)
+ + "\ncount: "
+ + mComponentCountActual
+ + "\noffset: "
+ + mOffset
+ + "\nvalue: "
+ + forceGetValueAsString()
+ + "\n";
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/IfdData.java b/java/com/android/dialer/callcomposer/camera/exif/IfdData.java
new file mode 100644
index 000000000..b808defc6
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/IfdData.java
@@ -0,0 +1,126 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * This class stores all the tags in an IFD.
+ *
+ * @see ExifData
+ * @see ExifTag
+ */
+class IfdData {
+
+ private final int mIfdId;
+ private final Map<Short, ExifTag> mExifTags = new HashMap<>();
+ private static final int[] sIfds = {
+ IfdId.TYPE_IFD_0,
+ IfdId.TYPE_IFD_1,
+ IfdId.TYPE_IFD_EXIF,
+ IfdId.TYPE_IFD_INTEROPERABILITY,
+ IfdId.TYPE_IFD_GPS
+ };
+ /**
+ * Creates an IfdData with given IFD ID.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ IfdData(int ifdId) {
+ mIfdId = ifdId;
+ }
+
+ static int[] getIfds() {
+ return sIfds;
+ }
+
+ /** Get a array the contains all {@link ExifTag} in this IFD. */
+ private ExifTag[] getAllTags() {
+ return mExifTags.values().toArray(new ExifTag[mExifTags.size()]);
+ }
+
+ /**
+ * Gets the ID of this IFD.
+ *
+ * @see IfdId#TYPE_IFD_0
+ * @see IfdId#TYPE_IFD_1
+ * @see IfdId#TYPE_IFD_EXIF
+ * @see IfdId#TYPE_IFD_GPS
+ * @see IfdId#TYPE_IFD_INTEROPERABILITY
+ */
+ protected int getId() {
+ return mIfdId;
+ }
+
+ /** Gets the {@link ExifTag} with given tag id. Return null if there is no such tag. */
+ protected ExifTag getTag(short tagId) {
+ return mExifTags.get(tagId);
+ }
+
+ /** Adds or replaces a {@link ExifTag}. */
+ protected ExifTag setTag(ExifTag tag) {
+ tag.setIfd(mIfdId);
+ return mExifTags.put(tag.getTagId(), tag);
+ }
+
+ /** Gets the tags count in the IFD. */
+ private int getTagCount() {
+ return mExifTags.size();
+ }
+
+ /**
+ * Returns true if all tags in this two IFDs are equal. Note that tags of IFDs offset or thumbnail
+ * offset will be ignored.
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (obj instanceof IfdData) {
+ IfdData data = (IfdData) obj;
+ if (data.getId() == mIfdId && data.getTagCount() == getTagCount()) {
+ ExifTag[] tags = data.getAllTags();
+ for (ExifTag tag : tags) {
+ if (ExifInterface.isOffsetTag(tag.getTagId())) {
+ continue;
+ }
+ ExifTag tag2 = mExifTags.get(tag.getTagId());
+ if (!tag.equals(tag2)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mIfdId, mExifTags);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/IfdId.java b/java/com/android/dialer/callcomposer/camera/exif/IfdId.java
new file mode 100644
index 000000000..c61545752
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/IfdId.java
@@ -0,0 +1,28 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+/** The constants of the IFD ID defined in EXIF spec. */
+public interface IfdId {
+ int TYPE_IFD_0 = 0;
+ int TYPE_IFD_1 = 1;
+ int TYPE_IFD_EXIF = 2;
+ int TYPE_IFD_INTEROPERABILITY = 3;
+ int TYPE_IFD_GPS = 4;
+ /* This is used in ExifData to allocate enough IfdData */
+ int TYPE_IFD_COUNT = 5;
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java b/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java
new file mode 100644
index 000000000..3d98fcc0e
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java
@@ -0,0 +1,38 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+class JpegHeader {
+ static final short SOI = (short) 0xFFD8;
+ static final short APP1 = (short) 0xFFE1;
+ static final short EOI = (short) 0xFFD9;
+
+ /**
+ * SOF (start of frame). All value between SOF0 and SOF15 is SOF marker except for DHT, JPG, and
+ * DAC marker.
+ */
+ private static final short SOF0 = (short) 0xFFC0;
+
+ private static final short SOF15 = (short) 0xFFCF;
+ private static final short DHT = (short) 0xFFC4;
+ private static final short JPG = (short) 0xFFC8;
+ private static final short DAC = (short) 0xFFCC;
+
+ static boolean isSofMarker(short marker) {
+ return marker >= SOF0 && marker <= SOF15 && marker != DHT && marker != JPG && marker != DAC;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/camera/exif/Rational.java b/java/com/android/dialer/callcomposer/camera/exif/Rational.java
new file mode 100644
index 000000000..9afca8449
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/camera/exif/Rational.java
@@ -0,0 +1,70 @@
+/*
+ * 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.callcomposer.camera.exif;
+
+import java.util.Objects;
+
+/**
+ * The rational data type of EXIF tag. Contains a pair of longs representing the numerator and
+ * denominator of a Rational number.
+ */
+public class Rational {
+
+ private final long mNumerator;
+ private final long mDenominator;
+
+ /** Create a Rational with a given numerator and denominator. */
+ Rational(long nominator, long denominator) {
+ mNumerator = nominator;
+ mDenominator = denominator;
+ }
+
+ /** Gets the numerator of the rational. */
+ long getNumerator() {
+ return mNumerator;
+ }
+
+ /** Gets the denominator of the rational */
+ long getDenominator() {
+ return mDenominator;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof Rational) {
+ Rational data = (Rational) obj;
+ return mNumerator == data.mNumerator && mDenominator == data.mDenominator;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mNumerator, mDenominator);
+ }
+
+ @Override
+ public String toString() {
+ return mNumerator + "/" + mDenominator;
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml b/java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml
new file mode 100644
index 000000000..12694ee5f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest package="com.android.dialer.callcomposer.cameraui"/> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java b/java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java
new file mode 100644
index 000000000..85c64e477
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java
@@ -0,0 +1,107 @@
+/*
+ * 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.callcomposer.cameraui;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.hardware.Camera;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import com.android.dialer.callcomposer.camera.CameraManager;
+import com.android.dialer.callcomposer.camera.HardwareCameraPreview;
+import com.android.dialer.callcomposer.camera.SoftwareCameraPreview;
+import com.android.dialer.common.LogUtil;
+
+/** Used to display the view of the camera. */
+public class CameraMediaChooserView extends FrameLayout {
+ private static final String STATE_CAMERA_INDEX = "camera_index";
+ private static final String STATE_SUPER = "super";
+
+ // True if we have at least queued an update to the view tree to support software rendering
+ // fallback
+ private boolean mIsSoftwareFallbackActive;
+
+ public CameraMediaChooserView(final Context context, final AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Bundle bundle = new Bundle();
+ bundle.putParcelable(STATE_SUPER, super.onSaveInstanceState());
+ final int cameraIndex = CameraManager.get().getCameraIndex();
+ LogUtil.i("CameraMediaChooserView.onSaveInstanceState", "saving camera index:" + cameraIndex);
+ bundle.putInt(STATE_CAMERA_INDEX, cameraIndex);
+ return bundle;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(final Parcelable state) {
+ if (!(state instanceof Bundle)) {
+ return;
+ }
+
+ final Bundle bundle = (Bundle) state;
+ final int cameraIndex = bundle.getInt(STATE_CAMERA_INDEX);
+ super.onRestoreInstanceState(bundle.getParcelable(STATE_SUPER));
+
+ LogUtil.i(
+ "CameraMediaChooserView.onRestoreInstanceState", "restoring camera index:" + cameraIndex);
+ if (cameraIndex != -1) {
+ CameraManager.get().selectCameraByIndex(cameraIndex);
+ } else {
+ resetState();
+ }
+ }
+
+ public void resetState() {
+ CameraManager.get().selectCamera(Camera.CameraInfo.CAMERA_FACING_BACK);
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+ // If the canvas isn't hardware accelerated, we have to replace the HardwareCameraPreview
+ // with a SoftwareCameraPreview which supports software rendering
+ if (!canvas.isHardwareAccelerated() && !mIsSoftwareFallbackActive) {
+ mIsSoftwareFallbackActive = true;
+ // Post modifying the tree since we can't modify the view tree during a draw pass
+ post(
+ new Runnable() {
+ @Override
+ public void run() {
+ final HardwareCameraPreview cameraPreview =
+ (HardwareCameraPreview) findViewById(R.id.camera_preview);
+ if (cameraPreview == null) {
+ return;
+ }
+ final ViewGroup parent = ((ViewGroup) cameraPreview.getParent());
+ final int index = parent.indexOfChild(cameraPreview);
+ final SoftwareCameraPreview softwareCameraPreview =
+ new SoftwareCameraPreview(getContext());
+ // Be sure to remove the hardware view before adding the software view to
+ // prevent having 2 camera previews active at the same time
+ parent.removeView(cameraPreview);
+ parent.addView(softwareCameraPreview, index);
+ }
+ });
+ }
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png
new file mode 100644
index 000000000..b974c9f70
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png
new file mode 100644
index 000000000..98427587b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png
new file mode 100644
index 000000000..4ec9f75e8
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png
new file mode 100644
index 000000000..e2345dc86
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png
new file mode 100644
index 000000000..3bab00984
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png
Binary files differ
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml b/java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml
new file mode 100644
index 000000000..fda52c99c
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:drawable="@color/background_item_grey_pressed"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@color/background_item_grey_pressed"
+ android:state_activated="true"/>
+ <item
+ android:drawable="@android:color/transparent"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
new file mode 100644
index 000000000..75401b14b
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml
@@ -0,0 +1,121 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<com.android.dialer.callcomposer.cameraui.CameraMediaChooserView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/camera_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+
+ <FrameLayout
+ android:id="@+id/mediapicker_enabled"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" >
+
+ <!-- Default to using the hardware rendered camera preview, we will fall back to
+ SoftwareCameraPreview in CameraMediaChooserView if needed -->
+ <com.android.dialer.callcomposer.camera.HardwareCameraPreview
+ android:id="@+id/camera_preview"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center" />
+
+ <com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay
+ android:id="@+id/focus_visual"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <View
+ android:id="@+id/camera_shutter_visual"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/white"
+ android:visibility="gone" />
+
+ <!-- Need a background on this view in order for the ripple effect to have a place to draw -->
+ <FrameLayout
+ android:id="@+id/camera_button_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/transparent"
+ android:padding="16dp"
+ android:layout_gravity="bottom">
+
+ <ImageButton
+ android:id="@+id/camera_fullscreen"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="bottom|end"
+ android:layout_marginEnd="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:src="@drawable/quantum_ic_fullscreen_white_48"
+ android:background="?android:selectableItemBackgroundBorderless"/>
+
+ <ImageButton
+ android:id="@+id/camera_exit_fullscreen"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="bottom|end"
+ android:layout_marginEnd="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:src="@drawable/quantum_ic_fullscreen_exit_white_48"
+ android:visibility="gone"
+ android:background="?android:selectableItemBackgroundBorderless"/>
+
+ <ImageButton
+ android:id="@+id/camera_capture_button"
+ android:layout_width="@dimen/capture_button_size"
+ android:layout_height="@dimen/capture_button_size"
+ android:layout_gravity="bottom|center_horizontal"
+ android:layout_marginBottom="@dimen/capture_button_bottom_margin"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:src="@drawable/ic_capture"
+ android:scaleType="fitXY"
+ android:contentDescription="@string/camera_take_picture"/>
+
+ <ImageButton
+ android:id="@+id/swap_camera_button"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="start|bottom"
+ android:layout_marginStart="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:src="@drawable/front_back_switch_button_animation"
+ android:background="@drawable/transparent_button_background"
+ android:contentDescription="@string/camera_switch_camera_rear"/>
+
+ <ImageButton
+ android:id="@+id/camera_cancel_button"
+ android:layout_width="@dimen/camera_view_button_size"
+ android:layout_height="@dimen/camera_view_button_size"
+ android:layout_gravity="start|bottom"
+ android:layout_marginStart="@dimen/camera_view_button_margin"
+ android:layout_marginBottom="@dimen/camera_view_button_margin"
+ android:visibility="gone"
+ android:background="@drawable/transparent_button_background"
+ android:src="@drawable/quantum_ic_undo_white_48"
+ android:contentDescription="@string/camera_cancel_recording" />
+ </FrameLayout>
+ </FrameLayout>
+
+ <ProgressBar
+ android:id="@+id/loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="gone"/>
+</com.android.dialer.callcomposer.cameraui.CameraMediaChooserView> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml b/java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml
new file mode 100644
index 000000000..d5a839aca
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <color name="background_item_grey_pressed">#E0E0E0</color>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml b/java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml
new file mode 100644
index 000000000..09d4a58fd
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <dimen name="camera_view_button_margin">22dp</dimen>
+ <dimen name="camera_view_button_size">46dp</dimen>
+ <dimen name="capture_button_size">84dp</dimen>
+ <dimen name="capture_button_bottom_margin">4dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml b/java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml
new file mode 100644
index 000000000..999fe8f96
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Content description of button to switch to full screen camera -->
+ <string name="camera_switch_full_screen">Switch to full screen camera</string>
+ <!-- Content description of button when after swapped to front -->
+ <string name="camera_switch_camera_facing">Button is now front camera</string>
+ <!-- Content description of button when after swapped to back -->
+ <string name="camera_switch_camera_rear">Button is now back camera</string>
+ <!-- Content description of button to cancel recording video -->
+ <string name="camera_cancel_recording">Stop recording video</string>
+ <!-- Accessibility announcement for when we are using the front facing camera -->
+ <string name="using_front_camera">Using front camera</string>
+ <!-- Accessibility announcement for when we are using the back camera -->
+ <string name="using_back_camera">Using back camera</string>
+ <!-- Content description of button to take a photo -->
+ <string name="camera_take_picture">Take photo</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/nano/CallComposerContact.java b/java/com/android/dialer/callcomposer/nano/CallComposerContact.java
new file mode 100644
index 000000000..acb71a0aa
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/nano/CallComposerContact.java
@@ -0,0 +1,220 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.callcomposer.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class CallComposerContact
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallComposerContact> {
+
+ private static volatile CallComposerContact[] _emptyArray;
+ public static CallComposerContact[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallComposerContact[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional fixed64 photo_id = 1;
+ public long photoId;
+
+ // optional string photo_uri = 2;
+ public java.lang.String photoUri;
+
+ // optional string contact_uri = 3;
+ public java.lang.String contactUri;
+
+ // optional string name_or_number = 4;
+ public java.lang.String nameOrNumber;
+
+ // optional bool is_business = 5;
+ public boolean isBusiness;
+
+ // optional string number = 6;
+ public java.lang.String number;
+
+ // optional string display_number = 7;
+ public java.lang.String displayNumber;
+
+ // optional string number_label = 8;
+ public java.lang.String numberLabel;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.callcomposer.CallComposerContact)
+
+ public CallComposerContact() {
+ clear();
+ }
+
+ public CallComposerContact clear() {
+ photoId = 0L;
+ photoUri = "";
+ contactUri = "";
+ nameOrNumber = "";
+ isBusiness = false;
+ number = "";
+ displayNumber = "";
+ numberLabel = "";
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.photoId != 0L) {
+ output.writeFixed64(1, this.photoId);
+ }
+ if (this.photoUri != null && !this.photoUri.equals("")) {
+ output.writeString(2, this.photoUri);
+ }
+ if (this.contactUri != null && !this.contactUri.equals("")) {
+ output.writeString(3, this.contactUri);
+ }
+ if (this.nameOrNumber != null && !this.nameOrNumber.equals("")) {
+ output.writeString(4, this.nameOrNumber);
+ }
+ if (this.isBusiness != false) {
+ output.writeBool(5, this.isBusiness);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ output.writeString(6, this.number);
+ }
+ if (this.displayNumber != null && !this.displayNumber.equals("")) {
+ output.writeString(7, this.displayNumber);
+ }
+ if (this.numberLabel != null && !this.numberLabel.equals("")) {
+ output.writeString(8, this.numberLabel);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.photoId != 0L) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeFixed64Size(1, this.photoId);
+ }
+ if (this.photoUri != null && !this.photoUri.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(2, this.photoUri);
+ }
+ if (this.contactUri != null && !this.contactUri.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(3, this.contactUri);
+ }
+ if (this.nameOrNumber != null && !this.nameOrNumber.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 4, this.nameOrNumber);
+ }
+ if (this.isBusiness != false) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeBoolSize(5, this.isBusiness);
+ }
+ if (this.number != null && !this.number.equals("")) {
+ size += com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(6, this.number);
+ }
+ if (this.displayNumber != null && !this.displayNumber.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(
+ 7, this.displayNumber);
+ }
+ if (this.numberLabel != null && !this.numberLabel.equals("")) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeStringSize(8, this.numberLabel);
+ }
+ return size;
+ }
+
+ @Override
+ public CallComposerContact mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 9:
+ {
+ this.photoId = input.readFixed64();
+ break;
+ }
+ case 18:
+ {
+ this.photoUri = input.readString();
+ break;
+ }
+ case 26:
+ {
+ this.contactUri = input.readString();
+ break;
+ }
+ case 34:
+ {
+ this.nameOrNumber = input.readString();
+ break;
+ }
+ case 40:
+ {
+ this.isBusiness = input.readBool();
+ break;
+ }
+ case 50:
+ {
+ this.number = input.readString();
+ break;
+ }
+ case 58:
+ {
+ this.displayNumber = input.readString();
+ break;
+ }
+ case 66:
+ {
+ this.numberLabel = input.readString();
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallComposerContact parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallComposerContact(), data);
+ }
+
+ public static CallComposerContact parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+ return new CallComposerContact().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml b/java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml
new file mode 100644
index 000000000..b3c36e9e0
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+
+ <stroke
+ android:width="@dimen/call_composer_contact_photo_border_thickness"
+ android:color="@color/background_dialer_white"/>
+
+ <padding
+ android:bottom="@dimen/call_composer_contact_photo_border_thickness"
+ android:left="@dimen/call_composer_contact_photo_border_thickness"
+ android:right="@dimen/call_composer_contact_photo_border_thickness"
+ android:top="@dimen/call_composer_contact_photo_border_thickness"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml
new file mode 100644
index 000000000..57dce975e
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/gallery_item_corner_radius"/>
+ <solid android:color="@color/gallery_item_image_color"/>
+</shape>
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml
new file mode 100644
index 000000000..b6b91b5a6
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/gallery_item_corner_radius"/>
+ <solid android:color="#80000000"/>
+</shape>
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml
new file mode 100644
index 000000000..bbae1a821
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/gallery_item_corner_radius"/>
+ <solid android:color="@color/background_dialer_white"/>
+</shape>
diff --git a/java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml b/java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml
new file mode 100644
index 000000000..5050407c5
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:shape="oval">
+ <stroke
+ android:width="1dp"
+ android:color="@color/dialer_theme_color"/>
+ <solid
+ android:color="@color/background_dialer_white"/>
+ <size
+ android:height="@dimen/gallery_check_size"
+ android:width="@dimen/gallery_check_size"/>
+ </shape>
+ </item>
+ <item>
+ <bitmap
+ android:gravity="center"
+ android:src="@drawable/quantum_ic_check_black_24"
+ android:tint="@color/dialer_theme_color"/>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
new file mode 100644
index 000000000..518b53ffd
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/call_composer_background_color">
+
+ <LinearLayout
+ android:id="@+id/call_composer_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:gravity="bottom"
+ android:background="@android:color/transparent">
+
+ <RelativeLayout
+ android:id="@+id/contact_bar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:elevation="@dimen/call_composer_contact_container_elevation"
+ android:background="?android:attr/selectableItemBackground">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:layout_marginTop="@dimen/call_composer_contact_container_margin_top"
+ android:paddingTop="@dimen/call_composer_contact_container_padding_top"
+ android:paddingBottom="@dimen/call_composer_contact_container_padding_bottom"
+ android:background="@color/dialer_theme_color">
+
+ <TextView
+ android:id="@+id/contact_name"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textColor="@color/background_dialer_white"
+ android:textSize="@dimen/call_composer_name_text_size"/>
+
+ <TextView
+ android:id="@+id/phone_number"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textColor="@color/background_dialer_white"
+ android:textSize="@dimen/call_composer_number_text_size"/>
+ </LinearLayout>
+
+ <QuickContactBadge
+ android:id="@+id/contact_photo"
+ android:layout_width="@dimen/call_composer_contact_photo_size"
+ android:layout_height="@dimen/call_composer_contact_photo_size"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/call_composer_contact_border"/>
+ </RelativeLayout>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/call_composer_view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_view_pager_height"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:id="@+id/media_actions"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_media_bar_height"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ android:background="@color/dialer_secondary_color"
+ android:clickable="true">
+
+ <ImageView
+ android:id="@+id/call_composer_camera"
+ android:layout_width="@dimen/call_composer_media_actions_width"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_camera_alt_white_24"
+ android:background="?android:attr/selectableItemBackgroundBorderless"/>
+
+ <ImageView
+ android:id="@+id/call_composer_photo"
+ android:layout_width="@dimen/call_composer_media_actions_width"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_photo_white_24"
+ android:background="?android:attr/selectableItemBackgroundBorderless"/>
+
+ <ImageView
+ android:id="@+id/call_composer_message"
+ android:layout_width="@dimen/call_composer_media_actions_width"
+ android:layout_height="match_parent"
+ android:scaleType="center"
+ android:src="@drawable/ic_message_24dp"
+ android:background="?android:attr/selectableItemBackgroundBorderless"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/send_and_call_button"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_media_bar_height"
+ android:visibility="invisible"
+ android:background="@color/compose_and_call_background">
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:drawableStart="@drawable/quantum_ic_call_white_18"
+ android:drawablePadding="@dimen/send_and_call_drawable_padding"
+ android:textAllCaps="true"
+ android:text="@string/send_and_call"
+ android:textSize="@dimen/send_and_call_text_size"
+ android:fontFamily="sans-serif-medium"
+ android:textColor="@color/background_dialer_white"/>
+ </FrameLayout>
+ </FrameLayout>
+ </LinearLayout>
+
+ <Toolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:visibility="invisible"
+ android:titleTextAppearance="@style/call_composer_toolbar_title_text"
+ android:subtitleTextAppearance="@style/call_composer_toolbar_subtitle_text"
+ android:navigationIcon="@drawable/quantum_ic_close_white_24"
+ android:background="@color/dialer_theme_color"/>
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml
new file mode 100644
index 000000000..200a3dce7
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_height="match_parent"
+ android:layout_width="match_parent">
+
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ layout="@layout/camera_view"/>
+
+ <include
+ android:id="@+id/permission_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ layout="@layout/permission_view"/>
+</FrameLayout>
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
new file mode 100644
index 000000000..58893ba50
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/background_dialer_white">
+
+ <GridView
+ android:id="@+id/gallery_grid_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/gallery_item_padding"
+ android:paddingRight="@dimen/gallery_item_padding"
+ android:paddingTop="@dimen/gallery_item_padding"
+ android:numColumns="3"/>
+
+ <include
+ android:id="@+id/permission_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:visibility="gone"
+ layout="@layout/permission_view"/>
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
new file mode 100644
index 000000000..97f232b3a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/call_composer_view_pager_height"
+ android:orientation="vertical"
+ android:gravity="bottom"
+ android:background="@color/background_dialer_white">
+
+ <TextView
+ android:id="@+id/message_urgent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/urgent"
+ style="@style/message_composer_textview"/>
+
+ <TextView
+ android:id="@+id/message_chat"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/want_to_chat"
+ style="@style/message_composer_textview"/>
+
+ <TextView
+ android:id="@+id/message_question"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/quick_question"
+ style="@style/message_composer_textview"/>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/message_composer_divider_height"
+ android:background="@color/call_composer_divider"/>
+
+ <RelativeLayout
+ android:orientation="horizontal"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <EditText
+ android:id="@+id/custom_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/message_composer_item_padding"
+ android:textSize="@dimen/message_compose_item_text_size"
+ android:hint="@string/custom_message_hint"
+ android:textColor="@color/dialer_primary_text_color"
+ android:textColorHint="@color/dialer_edit_text_hint_color"
+ android:background="@color/background_dialer_white"
+ android:textCursorDrawable="@drawable/searchedittext_custom_cursor"
+ android:layout_toLeftOf="@+id/remaining_characters"/>
+
+ <TextView
+ android:id="@+id/remaining_characters"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/message_composer_item_padding"
+ android:layout_alignParentRight="true"
+ android:layout_centerVertical="true"
+ android:textSize="@dimen/message_compose_remaining_char_text_size"
+ android:textColor="@color/dialer_edit_text_hint_color"/>
+ </RelativeLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml b/java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml
new file mode 100644
index 000000000..6c68517bd
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<com.android.dialer.callcomposer.GalleryGridItemView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:padding="@dimen/gallery_item_padding"
+ android:clickable="true">
+
+ <ImageView
+ android:id="@+id/image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/gallery_grid_item_view_background"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+
+ <FrameLayout
+ android:id="@+id/checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/gallery_grid_checkbox_background"
+ android:outlineProvider="background"
+ android:visibility="gone">
+
+ <ImageView
+ android:layout_width="@dimen/gallery_check_size"
+ android:layout_height="@dimen/gallery_check_size"
+ android:layout_gravity="center"
+ android:src="@drawable/gallery_item_selected_drawable"/>
+ </FrameLayout>
+
+ <ImageView
+ android:id="@+id/gallery"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:src="@drawable/quantum_ic_photo_library_white_24"
+ android:scaleType="center"
+ android:background="@drawable/gallery_background"
+ android:outlineProvider="background"
+ android:visibility="gone"/>
+</com.android.dialer.callcomposer.GalleryGridItemView> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/layout/permission_view.xml b/java/com/android/dialer/callcomposer/res/layout/permission_view.xml
new file mode 100644
index 000000000..4daa11d62
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/layout/permission_view.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:clickable="true"
+ android:background="@color/background_dialer_white">
+
+ <ImageView
+ android:id="@+id/permission_icon"
+ android:layout_width="@dimen/permission_image_size"
+ android:layout_height="@dimen/permission_image_size"
+ android:layout_margin="@dimen/permission_item_margin"/>
+
+ <TextView
+ android:id="@+id/permission_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/permission_item_margin"
+ style="@style/TextAppearanceMedium"/>
+
+ <TextView
+ android:id="@+id/allow"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/min_touch_target_size"
+ android:minWidth="@dimen/min_touch_target_size"
+ android:gravity="center"
+ android:text="@string/allow"
+ android:textAllCaps="true"
+ android:textSize="@dimen/allow_permission_text_size"
+ android:textColor="@color/dialer_theme_color"
+ android:background="?android:attr/selectableItemBackground"
+ android:padding="@dimen/permission_allow_padding"
+ android:theme="@style/Theme.AppCompat.Light"/>
+</LinearLayout> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/colors.xml b/java/com/android/dialer/callcomposer/res/values/colors.xml
new file mode 100644
index 000000000..89e55b79a
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <!-- 50% black -->
+ <color name="call_composer_background_color">#7F000000</color>
+ <color name="call_composer_divider">#12000000</color>
+ <color name="compose_and_call_background">#00BC35</color>
+ <color name="gallery_item_image_color">#607D8B</color>
+ <color name="gallery_item_background_color">#ECEFF1</color>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/dimens.xml b/java/com/android/dialer/callcomposer/res/values/dimens.xml
new file mode 100644
index 000000000..3ebda7a0f
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/dimens.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <dimen name="call_composer_view_pager_height">258dp</dimen>
+
+ <!-- Toolbar -->
+ <dimen name="toolbar_title_text_size">16sp</dimen>
+ <dimen name="toolbar_subtitle_text_size">14sp</dimen>
+
+ <!-- Contact bar -->
+ <dimen name="call_composer_contact_photo_border_thickness">2dp</dimen>
+ <dimen name="call_composer_contact_photo_size">116dp</dimen>
+ <dimen name="call_composer_contact_container_margin_top">58dp</dimen>
+ <dimen name="call_composer_contact_container_padding_top">58dp</dimen>
+ <dimen name="call_composer_contact_container_padding_bottom">18dp</dimen>
+ <dimen name="call_composer_name_text_size">32sp</dimen>
+ <dimen name="call_composer_number_text_size">16sp</dimen>
+ <dimen name="call_composer_contact_container_elevation">2dp</dimen>
+
+ <!-- Media bar -->
+ <dimen name="call_composer_media_actions_width">80dp</dimen>
+ <dimen name="call_composer_media_bar_height">48dp</dimen>
+
+ <!-- Send and Call button -->
+ <dimen name="send_and_call_icon_size">18dp</dimen>
+ <dimen name="send_and_call_text_size">16sp</dimen>
+ <dimen name="send_and_call_padding">8dp</dimen>
+ <dimen name="send_and_call_drawable_padding">4dp</dimen>
+
+ <!-- Message Composer -->
+ <dimen name="message_composer_item_padding">16dp</dimen>
+ <dimen name="message_compose_item_text_size">16sp</dimen>
+ <dimen name="message_compose_remaining_char_text_size">12sp</dimen>
+ <dimen name="message_composer_divider_height">1dp</dimen>
+ <integer name="call_composer_message_limit">60</integer>
+
+ <!-- Gallery Composer -->
+ <dimen name="gallery_item_selected_padding">6dp</dimen>
+ <dimen name="gallery_item_padding">3dp</dimen>
+ <dimen name="gallery_check_size">48dp</dimen>
+ <dimen name="gallery_item_corner_radius">2dp</dimen>
+
+ <!-- Permissions view -->
+ <dimen name="permission_image_size">72dp</dimen>
+ <dimen name="allow_permission_text_size">16sp</dimen>
+ <dimen name="permission_item_margin">8dp</dimen>
+ <dimen name="permission_allow_padding">16dp</dimen>
+ <dimen name="min_touch_target_size">48dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/strings.xml b/java/com/android/dialer/callcomposer/res/values/strings.xml
new file mode 100644
index 000000000..35a8cf9da
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
+ <string name="urgent">Urgent! Please pick up!</string>
+ <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
+ <string name="want_to_chat">Want to chat?</string>
+ <!-- A default message to send with a phone call. [CHAR LIMIT=27] -->
+ <string name="quick_question">Quick question…</string>
+ <!-- Hint in a text field to compose a custom message to send with a phone call [CHAR LIMIT=27] -->
+ <string name="custom_message_hint">Write a custom message</string>
+ <!-- Text for a button to make a phone call combined with a picture or text message [CHAR LIMIT=26] -->
+ <string name="send_and_call">Send and call</string>
+ <!-- Accessibility description for each image in the gallery. For example, "image January 17 2015 1 59 pm". -->
+ <string name="gallery_item_description">image <xliff:g id="date">%1$tB %1$te %1$tY %1$tl %1$tM %1$tp</xliff:g></string>
+ <!-- Accessibility description for each image in the gallery when no date is present. -->
+ <string name="gallery_item_description_no_date">image</string>
+ <!-- Content description of button to switch camera to picture more -->
+ <string name="camera_switch_to_still_mode">Take a photo</string>
+ <!-- Error toast message shown when a camera image failed to attach to the message -->
+ <string name="camera_media_failure">Couldn\'t load camera image</string>
+ <!-- Text for a button to ask for device permissions -->
+ <string name="allow">Allow</string>
+ <!-- Text presented to the user explaining that we need Camera permission to take photos -->
+ <string name="camera_permission_text">To take a photo, give access to Camera</string>
+ <!-- Text presented to the user explaining that we need device storage permission to view photos -->
+ <string name="gallery_permission_text">To share an image, give access to Media</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/res/values/styles.xml b/java/com/android/dialer/callcomposer/res/values/styles.xml
new file mode 100644
index 000000000..891f6397d
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/res/values/styles.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <style name="Theme.AppCompat.CallComposer" parent="Theme.AppCompat.NoActionBar">
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowFrame">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:listViewStyle">@style/ListViewStyle</item>
+ <!-- We need to use a light ripple behind ActionBar items in order for them to
+ be visible when using some of the darker ActionBar tints -->
+ <item name="android:actionBarItemBackground">@drawable/item_background_material_borderless_dark</item>
+ </style>
+
+ <style name="message_composer_textview">
+ <item name="android:textSize">@dimen/message_compose_item_text_size</item>
+ <item name="android:textColor">@color/dialer_primary_text_color</item>
+ <item name="android:padding">@dimen/message_composer_item_padding</item>
+ <item name="android:background">@drawable/item_background_material_light</item>
+ </style>
+
+ <style name="call_composer_toolbar_title_text">
+ <item name="android:textSize">@dimen/toolbar_title_text_size</item>
+ <item name="android:textColor">@color/background_dialer_white</item>
+ </style>
+
+ <style name="call_composer_toolbar_subtitle_text">
+ <item name="android:textSize">@dimen/toolbar_subtitle_text_size</item>
+ <item name="android:textColor">@color/background_dialer_white</item>
+ </style>
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java b/java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java
new file mode 100644
index 000000000..83580fd38
--- /dev/null
+++ b/java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java
@@ -0,0 +1,124 @@
+/*
+ * 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.callcomposer.util;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DialerUtils;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+
+/** Task for copying and resizing images to be shared with RCS process. */
+@TargetApi(VERSION_CODES.M)
+public class CopyAndResizeImageTask extends FallibleAsyncTask<Void, Void, File> {
+ public static final int MAX_OUTPUT_RESOLUTION = 1024;
+ private static final String MIME_TYPE = "image/jpeg";
+
+ private final Context context;
+ private final Uri uri;
+ private final Callback callback;
+
+ public CopyAndResizeImageTask(
+ @NonNull Context context, @NonNull Uri uri, @NonNull Callback callback) {
+ this.context = Assert.isNotNull(context);
+ this.uri = Assert.isNotNull(uri);
+ this.callback = Assert.isNotNull(callback);
+ }
+
+ @Nullable
+ @Override
+ protected File doInBackgroundFallible(Void... params) throws Throwable {
+ Bitmap bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri));
+ bitmap = resizeForEnrichedCalling(bitmap);
+
+ File outputFile = DialerUtils.createShareableFile(context);
+ try (OutputStream outputStream = new FileOutputStream(outputFile)) {
+ // Encode images to jpeg as it is better for camera pictures which we expect to be sending
+ bitmap.compress(CompressFormat.JPEG, 90, outputStream);
+ return outputFile;
+ }
+ }
+
+ @Override
+ protected void onPostExecute(FallibleTaskResult<File> result) {
+ if (result.isFailure()) {
+ callback.onCopyFailed(result.getThrowable());
+ } else {
+ callback.onCopySuccessful(result.getResult(), MIME_TYPE);
+ }
+ }
+
+ public static Bitmap resizeForEnrichedCalling(Bitmap image) {
+ Assert.isWorkerThread();
+
+ int width = image.getWidth();
+ int height = image.getHeight();
+
+ LogUtil.i(
+ "CopyAndResizeImageTask.resizeForEnrichedCalling",
+ "starting height: %d, width: %d",
+ height,
+ width);
+
+ if (width <= MAX_OUTPUT_RESOLUTION && height <= MAX_OUTPUT_RESOLUTION) {
+ LogUtil.i("CopyAndResizeImageTask.resizeForEnrichedCalling", "no resizing needed");
+ return image;
+ }
+
+ if (width > height) {
+ // landscape
+ float ratio = width / (float) MAX_OUTPUT_RESOLUTION;
+ width = MAX_OUTPUT_RESOLUTION;
+ height = (int) (height / ratio);
+ } else if (height > width) {
+ // portrait
+ float ratio = height / (float) MAX_OUTPUT_RESOLUTION;
+ height = MAX_OUTPUT_RESOLUTION;
+ width = (int) (width / ratio);
+ } else {
+ // square
+ height = MAX_OUTPUT_RESOLUTION;
+ width = MAX_OUTPUT_RESOLUTION;
+ }
+
+ LogUtil.i(
+ "CopyAndResizeImageTask.resizeForEnrichedCalling",
+ "ending height: %d, width: %d",
+ height,
+ width);
+
+ return Bitmap.createScaledBitmap(image, width, height, true);
+ }
+
+ /** Callback for callers to know when the task has finished */
+ public interface Callback {
+ void onCopySuccessful(File file, String mimeType);
+
+ void onCopyFailed(Throwable throwable);
+ }
+}
diff --git a/java/com/android/dialer/callintent/CallIntentBuilder.java b/java/com/android/dialer/callintent/CallIntentBuilder.java
new file mode 100644
index 000000000..a2fb564ab
--- /dev/null
+++ b/java/com/android/dialer/callintent/CallIntentBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * 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.callintent;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.CallUtil;
+
+/** Creates an intent to start a new outgoing call. */
+public class CallIntentBuilder {
+ private final Uri uri;
+ private final CallSpecificAppData callSpecificAppData;
+ @Nullable private PhoneAccountHandle phoneAccountHandle;
+ private boolean isVideoCall;
+ private String callSubject;
+
+ public CallIntentBuilder(@NonNull Uri uri, @NonNull CallSpecificAppData callSpecificAppData) {
+ this.uri = Assert.isNotNull(uri);
+ this.callSpecificAppData = Assert.isNotNull(callSpecificAppData);
+ Assert.checkArgument(
+ callSpecificAppData.callInitiationType != CallInitiationType.Type.UNKNOWN_INITIATION);
+ }
+
+ public CallIntentBuilder(@NonNull Uri uri, int callInitiationType) {
+ this(uri, createCallSpecificAppData(callInitiationType));
+ }
+
+ public CallIntentBuilder(
+ @NonNull String number, @NonNull CallSpecificAppData callSpecificAppData) {
+ this(CallUtil.getCallUri(Assert.isNotNull(number)), callSpecificAppData);
+ }
+
+ public CallIntentBuilder(@NonNull String number, int callInitiationType) {
+ this(CallUtil.getCallUri(Assert.isNotNull(number)), callInitiationType);
+ }
+
+ public CallSpecificAppData getCallSpecificAppData() {
+ return callSpecificAppData;
+ }
+
+ public CallIntentBuilder setPhoneAccountHandle(@Nullable PhoneAccountHandle accountHandle) {
+ this.phoneAccountHandle = accountHandle;
+ return this;
+ }
+
+ public CallIntentBuilder setIsVideoCall(boolean isVideoCall) {
+ this.isVideoCall = isVideoCall;
+ return this;
+ }
+
+ public CallIntentBuilder setCallSubject(String callSubject) {
+ this.callSubject = callSubject;
+ return this;
+ }
+
+ public Intent build() {
+ Intent intent = new Intent(Intent.ACTION_CALL, uri);
+ intent.putExtra(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
+ isVideoCall ? VideoProfile.STATE_BIDIRECTIONAL : VideoProfile.STATE_AUDIO_ONLY);
+
+ Bundle extras = new Bundle();
+ extras.putLong(Constants.EXTRA_CALL_CREATED_TIME_MILLIS, SystemClock.elapsedRealtime());
+ CallIntentParser.putCallSpecificAppData(extras, callSpecificAppData);
+ intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
+
+ if (phoneAccountHandle != null) {
+ intent.putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ }
+
+ if (!TextUtils.isEmpty(callSubject)) {
+ intent.putExtra(TelecomManager.EXTRA_CALL_SUBJECT, callSubject);
+ }
+
+ return intent;
+ }
+
+ private static @NonNull CallSpecificAppData createCallSpecificAppData(int callInitiationType) {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = callInitiationType;
+ return callSpecificAppData;
+ }
+}
diff --git a/java/com/android/dialer/callintent/CallIntentParser.java b/java/com/android/dialer/callintent/CallIntentParser.java
new file mode 100644
index 000000000..40c8ee348
--- /dev/null
+++ b/java/com/android/dialer/callintent/CallIntentParser.java
@@ -0,0 +1,54 @@
+/*
+ * 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.callintent;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+import com.google.protobuf.nano.MessageNano;
+
+/** Parses data for a call extra to get any dialer specific app data. */
+public class CallIntentParser {
+ @Nullable
+ public static CallSpecificAppData getCallSpecificAppData(@Nullable Bundle extras) {
+ if (extras == null) {
+ return null;
+ }
+
+ byte[] flatArray = extras.getByteArray(Constants.EXTRA_CALL_SPECIFIC_APP_DATA);
+ if (flatArray == null) {
+ return null;
+ }
+ try {
+ return CallSpecificAppData.parseFrom(flatArray);
+ } catch (InvalidProtocolBufferNanoException e) {
+ Assert.fail("unexpected exception: " + e);
+ return null;
+ }
+ }
+
+ public static void putCallSpecificAppData(
+ @NonNull Bundle extras, @NonNull CallSpecificAppData callSpecificAppData) {
+ extras.putByteArray(
+ Constants.EXTRA_CALL_SPECIFIC_APP_DATA, MessageNano.toByteArray(callSpecificAppData));
+ }
+
+ private CallIntentParser() {}
+}
diff --git a/java/com/android/dialer/callintent/Constants.java b/java/com/android/dialer/callintent/Constants.java
new file mode 100644
index 000000000..dd5d48108
--- /dev/null
+++ b/java/com/android/dialer/callintent/Constants.java
@@ -0,0 +1,31 @@
+/*
+ * 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.callintent;
+
+/** Constants used to construct and parse call intents. These should never be made public. */
+/* package */ class Constants {
+ // This is a Dialer extra that is set for outgoing calls and used by the InCallUI.
+ /* package */ static final String EXTRA_CALL_SPECIFIC_APP_DATA =
+ "com.android.dialer.callintent.CALL_SPECIFIC_APP_DATA";
+
+ // This is a hidden system extra. For outgoing calls Dialer sets it and parses it but for incoming
+ // calls Telecom sets it and Dialer parses it.
+ /* package */ static final String EXTRA_CALL_CREATED_TIME_MILLIS =
+ "android.telecom.extra.CALL_CREATED_TIME_MILLIS";
+
+ private Constants() {}
+}
diff --git a/java/com/android/dialer/callintent/nano/CallInitiationType.java b/java/com/android/dialer/callintent/nano/CallInitiationType.java
new file mode 100644
index 000000000..4badd6e57
--- /dev/null
+++ b/java/com/android/dialer/callintent/nano/CallInitiationType.java
@@ -0,0 +1,101 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.callintent.nano;
+
+@SuppressWarnings("hiding")
+public final class CallInitiationType extends
+ com.google.protobuf.nano.ExtendableMessageNano<CallInitiationType> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_INITIATION = 0;
+ public static final int INCOMING_INITIATION = 1;
+ public static final int DIALPAD = 2;
+ public static final int SPEED_DIAL = 3;
+ public static final int REMOTE_DIRECTORY = 4;
+ public static final int SMART_DIAL = 5;
+ public static final int REGULAR_SEARCH = 6;
+ public static final int CALL_LOG = 7;
+ public static final int CALL_LOG_FILTER = 8;
+ public static final int VOICEMAIL_LOG = 9;
+ public static final int CALL_DETAILS = 10;
+ public static final int QUICK_CONTACTS = 11;
+ public static final int EXTERNAL_INITIATION = 12;
+ public static final int LAUNCHER_SHORTCUT = 13;
+ public static final int CALL_COMPOSER = 14;
+ public static final int MISSED_CALL_NOTIFICATION = 15;
+ public static final int CALL_SUBJECT_DIALOG = 16;
+ }
+
+ private static volatile CallInitiationType[] _emptyArray;
+ public static CallInitiationType[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallInitiationType[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.callintent.CallInitiationType)
+
+ public CallInitiationType() {
+ clear();
+ }
+
+ public CallInitiationType clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public CallInitiationType mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallInitiationType parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallInitiationType(), data);
+ }
+
+ public static CallInitiationType parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new CallInitiationType().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/callintent/nano/CallSpecificAppData.java b/java/com/android/dialer/callintent/nano/CallSpecificAppData.java
new file mode 100644
index 000000000..fd00b0a68
--- /dev/null
+++ b/java/com/android/dialer/callintent/nano/CallSpecificAppData.java
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.callintent.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class CallSpecificAppData
+ extends com.google.protobuf.nano.ExtendableMessageNano<CallSpecificAppData> {
+
+ private static volatile CallSpecificAppData[] _emptyArray;
+
+ public static CallSpecificAppData[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new CallSpecificAppData[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // optional int32 call_initiation_type = 1;
+ public int callInitiationType;
+
+ // optional int32 position_of_selected_search_result = 2;
+ public int positionOfSelectedSearchResult;
+
+ // optional int32 characters_in_search_string = 3;
+ public int charactersInSearchString;
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.callintent.CallSpecificAppData)
+
+ public CallSpecificAppData() {
+ clear();
+ }
+
+ public CallSpecificAppData clear() {
+ callInitiationType = 0;
+ positionOfSelectedSearchResult = 0;
+ charactersInSearchString = 0;
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public void writeTo(com.google.protobuf.nano.CodedOutputByteBufferNano output)
+ throws java.io.IOException {
+ if (this.callInitiationType != 0) {
+ output.writeInt32(1, this.callInitiationType);
+ }
+ if (this.positionOfSelectedSearchResult != 0) {
+ output.writeInt32(2, this.positionOfSelectedSearchResult);
+ }
+ if (this.charactersInSearchString != 0) {
+ output.writeInt32(3, this.charactersInSearchString);
+ }
+ super.writeTo(output);
+ }
+
+ @Override
+ protected int computeSerializedSize() {
+ int size = super.computeSerializedSize();
+ if (this.callInitiationType != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(
+ 1, this.callInitiationType);
+ }
+ if (this.positionOfSelectedSearchResult != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(
+ 2, this.positionOfSelectedSearchResult);
+ }
+ if (this.charactersInSearchString != 0) {
+ size +=
+ com.google.protobuf.nano.CodedOutputByteBufferNano.computeInt32Size(
+ 3, this.charactersInSearchString);
+ }
+ return size;
+ }
+
+ @Override
+ public CallSpecificAppData mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ case 8:
+ {
+ this.callInitiationType = input.readInt32();
+ break;
+ }
+ case 16:
+ {
+ this.positionOfSelectedSearchResult = input.readInt32();
+ break;
+ }
+ case 24:
+ {
+ this.charactersInSearchString = input.readInt32();
+ break;
+ }
+ }
+ }
+ }
+
+ public static CallSpecificAppData parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new CallSpecificAppData(), data);
+ }
+
+ public static CallSpecificAppData parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input) throws java.io.IOException {
+ return new CallSpecificAppData().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/common/AndroidManifest.xml b/java/com/android/dialer/common/AndroidManifest.xml
new file mode 100644
index 000000000..ae43d6693
--- /dev/null
+++ b/java/com/android/dialer/common/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.common">
+</manifest>
diff --git a/java/com/android/dialer/common/Assert.java b/java/com/android/dialer/common/Assert.java
new file mode 100644
index 000000000..00b4f2595
--- /dev/null
+++ b/java/com/android/dialer/common/Assert.java
@@ -0,0 +1,185 @@
+/*
+ * 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.common;
+
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/** Assertions which will result in program termination unless disabled by flags. */
+public class Assert {
+
+ private static boolean areThreadAssertsEnabled = true;
+
+ public static void setAreThreadAssertsEnabled(boolean areThreadAssertsEnabled) {
+ Assert.areThreadAssertsEnabled = areThreadAssertsEnabled;
+ }
+
+ /**
+ * Called when a truly exceptional case occurs.
+ *
+ * @throws AssertionError
+ */
+ public static void fail() {
+ throw new AssertionError("Fail");
+ }
+
+ /**
+ * Called when a truly exceptional case occurs.
+ *
+ * @param reason the optional reason to supply as the exception message
+ * @throws AssertionError
+ */
+ public static void fail(String reason) {
+ throw new AssertionError(reason);
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ public static void checkArgument(boolean expression) {
+ checkArgument(expression, null);
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalArgumentException if {@code expression} is false
+ */
+ public static void checkArgument(
+ boolean expression, @Nullable String messageTemplate, Object... args) {
+ if (!expression) {
+ throw new IllegalArgumentException(format(messageTemplate, args));
+ }
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ public static void checkState(boolean expression) {
+ checkState(expression, null);
+ }
+
+ /**
+ * Ensures the truth of an expression involving the state of the calling instance, but not
+ * involving any parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalStateException if {@code expression} is false
+ */
+ public static void checkState(
+ boolean expression, @Nullable String messageTemplate, Object... args) {
+ if (!expression) {
+ throw new IllegalStateException(format(messageTemplate, args));
+ }
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling method is not null.
+ *
+ * @param reference an object reference
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ @NonNull
+ public static <T> T isNotNull(@Nullable T reference) {
+ return isNotNull(reference, null);
+ }
+
+ /**
+ * Ensures that an object reference passed as a parameter to the calling method is not null.
+ *
+ * @param reference an object reference
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @return the non-null reference that was validated
+ * @throws NullPointerException if {@code reference} is null
+ */
+ @NonNull
+ public static <T> T isNotNull(
+ @Nullable T reference, @Nullable String messageTemplate, Object... args) {
+ if (reference == null) {
+ throw new NullPointerException(format(messageTemplate, args));
+ }
+ return reference;
+ }
+
+ /**
+ * Ensures that the current thread is the main thread.
+ *
+ * @throws IllegalStateException if called on a background thread
+ */
+ public static void isMainThread() {
+ isMainThread(null);
+ }
+
+ /**
+ * Ensures that the current thread is the main thread.
+ *
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalStateException if called on a background thread
+ */
+ public static void isMainThread(@Nullable String messageTemplate, Object... args) {
+ if (!areThreadAssertsEnabled) {
+ return;
+ }
+ checkState(Looper.getMainLooper().equals(Looper.myLooper()), messageTemplate, args);
+ }
+
+ /**
+ * Ensures that the current thread is a worker thread.
+ *
+ * @throws IllegalStateException if called on the main thread
+ */
+ public static void isWorkerThread() {
+ isWorkerThread(null);
+ }
+
+ /**
+ * Ensures that the current thread is a worker thread.
+ *
+ * @param messageTemplate the message to log, possible with format arguments.
+ * @param args optional arguments to be used in the formatted string.
+ * @throws IllegalStateException if called on the main thread
+ */
+ public static void isWorkerThread(@Nullable String messageTemplate, Object... args) {
+ if (!areThreadAssertsEnabled) {
+ return;
+ }
+ checkState(!Looper.getMainLooper().equals(Looper.myLooper()), messageTemplate, args);
+ }
+
+ private static String format(@Nullable String messageTemplate, Object... args) {
+ if (messageTemplate == null) {
+ return null;
+ }
+ return String.format(messageTemplate, args);
+ }
+}
diff --git a/java/com/android/dialer/common/AsyncTaskExecutor.java b/java/com/android/dialer/common/AsyncTaskExecutor.java
new file mode 100644
index 000000000..caadfe7ce
--- /dev/null
+++ b/java/com/android/dialer/common/AsyncTaskExecutor.java
@@ -0,0 +1,51 @@
+/*
+ * 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.common;
+
+import android.os.AsyncTask;
+import android.support.annotation.MainThread;
+import java.util.concurrent.Executor;
+
+/**
+ * Interface used to submit {@link AsyncTask} objects to run in the background.
+ *
+ * <p>This interface has a direct parallel with the {@link Executor} interface. It exists to
+ * decouple the mechanics of AsyncTask submission from the description of how that AsyncTask will
+ * execute.
+ *
+ * <p>One immediate benefit of this approach is that testing becomes much easier, since it is easy
+ * to introduce a mock or fake AsyncTaskExecutor in unit/integration tests, and thus inspect which
+ * tasks have been submitted and control their execution in an orderly manner.
+ *
+ * <p>Another benefit in due course will be the management of the submitted tasks. An extension to
+ * this interface is planned to allow Activities to easily cancel all the submitted tasks that are
+ * still pending in the onDestroy() method of the Activity.
+ */
+public interface AsyncTaskExecutor {
+
+ /**
+ * Executes the given AsyncTask with the default Executor.
+ *
+ * <p>This method <b>must only be called from the ui thread</b>.
+ *
+ * <p>The identifier supplied is any Object that can be used to identify the task later. Most
+ * commonly this will be an enum which the tests can also refer to. {@code null} is also accepted,
+ * though of course this won't help in identifying the task later.
+ */
+ @MainThread
+ <T> AsyncTask<T, ?, ?> submit(Object identifier, AsyncTask<T, ?, ?> task, T... params);
+}
diff --git a/java/com/android/dialer/common/AsyncTaskExecutors.java b/java/com/android/dialer/common/AsyncTaskExecutors.java
new file mode 100644
index 000000000..77bebdb36
--- /dev/null
+++ b/java/com/android/dialer/common/AsyncTaskExecutors.java
@@ -0,0 +1,91 @@
+/*
+ * 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.common;
+
+import android.os.AsyncTask;
+import android.support.annotation.MainThread;
+import java.util.concurrent.Executor;
+
+/**
+ * Factory methods for creating AsyncTaskExecutors.
+ *
+ * <p>All of the factory methods on this class check first to see if you have set a static {@link
+ * AsyncTaskExecutorFactory} set through the {@link #setFactoryForTest(AsyncTaskExecutorFactory)}
+ * method, and if so delegate to that instead, which is one way of injecting dependencies for
+ * testing classes whose construction cannot be controlled such as {@link android.app.Activity}.
+ */
+public final class AsyncTaskExecutors {
+
+ /**
+ * A single instance of the {@link AsyncTaskExecutorFactory}, to which we delegate if it is
+ * non-null, for injecting when testing.
+ */
+ private static AsyncTaskExecutorFactory mInjectedAsyncTaskExecutorFactory = null;
+
+ /**
+ * Creates an AsyncTaskExecutor that submits tasks to run with {@link AsyncTask#SERIAL_EXECUTOR}.
+ */
+ public static AsyncTaskExecutor createAsyncTaskExecutor() {
+ synchronized (AsyncTaskExecutors.class) {
+ if (mInjectedAsyncTaskExecutorFactory != null) {
+ return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+ }
+ return new SimpleAsyncTaskExecutor(AsyncTask.SERIAL_EXECUTOR);
+ }
+ }
+
+ /**
+ * Creates an AsyncTaskExecutor that submits tasks to run with {@link
+ * AsyncTask#THREAD_POOL_EXECUTOR}.
+ */
+ public static AsyncTaskExecutor createThreadPoolExecutor() {
+ synchronized (AsyncTaskExecutors.class) {
+ if (mInjectedAsyncTaskExecutorFactory != null) {
+ return mInjectedAsyncTaskExecutorFactory.createAsyncTaskExeuctor();
+ }
+ return new SimpleAsyncTaskExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ public static void setFactoryForTest(AsyncTaskExecutorFactory factory) {
+ synchronized (AsyncTaskExecutors.class) {
+ mInjectedAsyncTaskExecutorFactory = factory;
+ }
+ }
+
+ /** Interface for creating AsyncTaskExecutor objects. */
+ public interface AsyncTaskExecutorFactory {
+
+ AsyncTaskExecutor createAsyncTaskExeuctor();
+ }
+
+ private static class SimpleAsyncTaskExecutor implements AsyncTaskExecutor {
+
+ private final Executor mExecutor;
+
+ public SimpleAsyncTaskExecutor(Executor executor) {
+ mExecutor = executor;
+ }
+
+ @Override
+ @MainThread
+ public <T> AsyncTask<T, ?, ?> submit(Object identifer, AsyncTask<T, ?, ?> task, T... params) {
+ Assert.isMainThread();
+ return task.executeOnExecutor(mExecutor, params);
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
new file mode 100644
index 000000000..f9d7cea90
--- /dev/null
+++ b/java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java
@@ -0,0 +1,79 @@
+/*
+ * 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.common;
+
+import android.support.annotation.Nullable;
+import javax.annotation.Generated;
+
+
+ final class AutoValue_FallibleAsyncTask_FallibleTaskResult<ResultT> extends FallibleAsyncTask.FallibleTaskResult<ResultT> {
+
+ private final Throwable throwable;
+ private final ResultT result;
+
+ AutoValue_FallibleAsyncTask_FallibleTaskResult(
+ @Nullable Throwable throwable,
+ @Nullable ResultT result) {
+ this.throwable = throwable;
+ this.result = result;
+ }
+
+ @Nullable
+ @Override
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ @Nullable
+ @Override
+ public ResultT getResult() {
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "FallibleTaskResult{"
+ + "throwable=" + throwable + ", "
+ + "result=" + result
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof FallibleAsyncTask.FallibleTaskResult) {
+ FallibleAsyncTask.FallibleTaskResult<?> that = (FallibleAsyncTask.FallibleTaskResult<?>) o;
+ return ((this.throwable == null) ? (that.getThrowable() == null) : this.throwable.equals(that.getThrowable()))
+ && ((this.result == null) ? (that.getResult() == null) : this.result.equals(that.getResult()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (throwable == null) ? 0 : this.throwable.hashCode();
+ h *= 1000003;
+ h ^= (result == null) ? 0 : this.result.hashCode();
+ return h;
+ }
+
+}
+
diff --git a/java/com/android/dialer/common/ConfigProvider.java b/java/com/android/dialer/common/ConfigProvider.java
new file mode 100644
index 000000000..c0791e979
--- /dev/null
+++ b/java/com/android/dialer/common/ConfigProvider.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.dialer.common;
+
+/** Gets config values from the container application. */
+public interface ConfigProvider {
+
+ String getString(String key, String defaultValue);
+
+ long getLong(String key, long defaultValue);
+
+ boolean getBoolean(String key, boolean defaultValue);
+}
diff --git a/java/com/android/dialer/common/ConfigProviderBindings.java b/java/com/android/dialer/common/ConfigProviderBindings.java
new file mode 100644
index 000000000..92e6cc3ff
--- /dev/null
+++ b/java/com/android/dialer/common/ConfigProviderBindings.java
@@ -0,0 +1,68 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/** Accessor for getting a {@link ConfigProvider}. */
+public class ConfigProviderBindings {
+
+ private static ConfigProvider configProvider;
+
+ public static ConfigProvider get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ if (configProvider != null) {
+ return configProvider;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof ConfigProviderFactory) {
+ configProvider = ((ConfigProviderFactory) application).getConfigProvider();
+ }
+
+ if (configProvider == null) {
+ configProvider = new ConfigProviderStub();
+ }
+
+ return configProvider;
+ }
+
+ @VisibleForTesting
+ public static void setForTesting(@Nullable ConfigProvider configProviderForTesting) {
+ configProvider = configProviderForTesting;
+ }
+
+ private static class ConfigProviderStub implements 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;
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/ConfigProviderFactory.java b/java/com/android/dialer/common/ConfigProviderFactory.java
new file mode 100644
index 000000000..aeb4f303a
--- /dev/null
+++ b/java/com/android/dialer/common/ConfigProviderFactory.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.common;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows dialer code to get
+ * references to a config provider.
+ */
+public interface ConfigProviderFactory {
+
+ ConfigProvider getConfigProvider();
+}
diff --git a/java/com/android/dialer/common/DpUtil.java b/java/com/android/dialer/common/DpUtil.java
new file mode 100644
index 000000000..0388824cd
--- /dev/null
+++ b/java/com/android/dialer/common/DpUtil.java
@@ -0,0 +1,31 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+
+/** Utility for dp to px conversion */
+public class DpUtil {
+
+ public static float pxToDp(Context context, float px) {
+ return px / context.getResources().getDisplayMetrics().density;
+ }
+
+ public static float dpToPx(Context context, float dp) {
+ return dp * context.getResources().getDisplayMetrics().density;
+ }
+}
diff --git a/java/com/android/dialer/common/FallibleAsyncTask.java b/java/com/android/dialer/common/FallibleAsyncTask.java
new file mode 100644
index 000000000..fbdbda75f
--- /dev/null
+++ b/java/com/android/dialer/common/FallibleAsyncTask.java
@@ -0,0 +1,94 @@
+/*
+ * 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.common;
+
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.FallibleAsyncTask.FallibleTaskResult;
+
+
+/**
+ * A task that runs work in the background, passing Throwables from {@link
+ * #doInBackground(Object[])} to {@link #onPostExecute(Object)} through a {@link
+ * FallibleTaskResult}.
+ *
+ * @param <ParamsT> the type of the parameters sent to the task upon execution
+ * @param <ProgressT> the type of the progress units published during the background computation
+ * @param <ResultT> the type of the result of the background computation
+ */
+public abstract class FallibleAsyncTask<ParamsT, ProgressT, ResultT>
+ extends AsyncTask<ParamsT, ProgressT, FallibleTaskResult<ResultT>> {
+
+ @Override
+ protected final FallibleTaskResult<ResultT> doInBackground(ParamsT... params) {
+ try {
+ return FallibleTaskResult.createSuccessResult(doInBackgroundFallible(params));
+ } catch (Throwable t) {
+ return FallibleTaskResult.createFailureResult(t);
+ }
+ }
+
+ /** Performs background work that may result in a Throwable. */
+ @Nullable
+ protected abstract ResultT doInBackgroundFallible(ParamsT... params) throws Throwable;
+
+ /**
+ * Holds the result of processing from {@link #doInBackground(Object[])}.
+ *
+ * @param <ResultT> the type of the result of the background computation
+ */
+
+ protected abstract static class FallibleTaskResult<ResultT> {
+
+ /** Creates an instance of FallibleTaskResult for the given throwable. */
+ private static <ResultT> FallibleTaskResult<ResultT> createFailureResult(@NonNull Throwable t) {
+ return new AutoValue_FallibleAsyncTask_FallibleTaskResult<>(t, null);
+ }
+
+ /** Creates an instance of FallibleTaskResult for the given result. */
+ private static <ResultT> FallibleTaskResult<ResultT> createSuccessResult(
+ @Nullable ResultT result) {
+ return new AutoValue_FallibleAsyncTask_FallibleTaskResult<>(null, result);
+ }
+
+ /**
+ * Returns the Throwable thrown in {@link #doInBackground(Object[])}, or {@code null} if
+ * background work completed without throwing.
+ */
+ @Nullable
+ public abstract Throwable getThrowable();
+
+ /**
+ * Returns the result of {@link #doInBackground(Object[])}, which may be {@code null}, or {@code
+ * null} if the background work threw a Throwable.
+ *
+ * <p>Use {@link #isFailure()} to determine if a {@code null} return is the result of a
+ * Throwable from the background work.
+ */
+ @Nullable
+ public abstract ResultT getResult();
+
+ /**
+ * Returns {@code true} if this object is the result of background work that threw a Throwable.
+ */
+ public boolean isFailure() {
+ //noinspection ThrowableResultOfMethodCallIgnored
+ return getThrowable() != null;
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/FragmentUtils.java b/java/com/android/dialer/common/FragmentUtils.java
new file mode 100644
index 000000000..cb036959d
--- /dev/null
+++ b/java/com/android/dialer/common/FragmentUtils.java
@@ -0,0 +1,98 @@
+/*
+ * 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.common;
+
+import android.support.annotation.CheckResult;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentActivity;
+
+/** Utility methods for working with Fragments */
+public class FragmentUtils {
+
+ private static Object parentForTesting;
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ public static void setParentForTesting(Object parentForTesting) {
+ FragmentUtils.parentForTesting = parentForTesting;
+ }
+
+ /**
+ * @return The parent of frag that implements the callbackInterface or null if no such parent can
+ * be found
+ */
+ @CheckResult(suggest = "#checkParent(Fragment, Class)}")
+ @Nullable
+ public static <T> T getParent(@NonNull Fragment fragment, @NonNull Class<T> callbackInterface) {
+ if (callbackInterface.isInstance(parentForTesting)) {
+ @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+ T parent = (T) parentForTesting;
+ return parent;
+ }
+
+ Fragment parentFragment = fragment.getParentFragment();
+ if (callbackInterface.isInstance(parentFragment)) {
+ @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+ T parent = (T) parentFragment;
+ return parent;
+ } else {
+ FragmentActivity activity = fragment.getActivity();
+ if (callbackInterface.isInstance(activity)) {
+ @SuppressWarnings("unchecked") // Casts are checked using runtime methods
+ T parent = (T) activity;
+ return parent;
+ }
+ }
+ return null;
+ }
+
+ /** Returns the parent or throws. Should perform check elsewhere(e.g. onAttach, newInstance). */
+ @NonNull
+ public static <T> T getParentUnsafe(
+ @NonNull Fragment fragment, @NonNull Class<T> callbackInterface) {
+ return Assert.isNotNull(getParent(fragment, callbackInterface));
+ }
+
+ /**
+ * Ensures fragment has a parent that implements the corresponding interface
+ *
+ * @param frag The Fragment whose parents are to be checked
+ * @param callbackInterface The interface class that a parent should implement
+ * @throws IllegalStateException if no parents are found that implement callbackInterface
+ */
+ public static void checkParent(@NonNull Fragment frag, @NonNull Class<?> callbackInterface)
+ throws IllegalStateException {
+ if (parentForTesting != null) {
+ return;
+ }
+ if (FragmentUtils.getParent(frag, callbackInterface) == null) {
+ String parent =
+ frag.getParentFragment() == null
+ ? frag.getActivity().getClass().getName()
+ : frag.getParentFragment().getClass().getName();
+ throw new IllegalStateException(
+ frag.getClass().getName()
+ + " must be added to a parent"
+ + " that implements "
+ + callbackInterface.getName()
+ + ". Instead found "
+ + parent);
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/LogUtil.java b/java/com/android/dialer/common/LogUtil.java
new file mode 100644
index 000000000..32d7b960b
--- /dev/null
+++ b/java/com/android/dialer/common/LogUtil.java
@@ -0,0 +1,214 @@
+/*
+ * 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.common;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+/** Provides logging functions. */
+public class LogUtil {
+
+ public static final String TAG = "Dialer";
+ private static final String SEPARATOR = " - ";
+
+ private LogUtil() {}
+
+ /**
+ * Log at a verbose level. Verbose logs should generally be filtered out, but may be useful when
+ * additional information is needed (e.g. to see how a particular flow evolved). These logs will
+ * not generally be available on production builds.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#v(String, String)}
+ */
+ public static void v(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.VERBOSE, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log at a debug level. Debug logs should provide known-useful information to aid in
+ * troubleshooting or evaluating flow. These logs will not generally be available on production
+ * builds.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'
+ * @param msg The message you would like logged, possibly with format arguments
+ * @param args Optional arguments to be used in the formatted string
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#d(String, String)}
+ */
+ public static void d(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.DEBUG, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log at an info level. Info logs provide information that would be useful to have on production
+ * builds for troubleshooting.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#i(String, String)}
+ */
+ public static void i(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.INFO, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log entry into a method at the info level.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ */
+ public static void enterBlock(String tag) {
+ println(android.util.Log.INFO, TAG, tag, "enter");
+ }
+
+ /**
+ * Log at a warn level. Warn logs indicate a possible error (e.g. a default switch branch was hit,
+ * or a null object was expected to be non-null), but recovery is possible. This may be used when
+ * it is not guaranteed that an indeterminate or bad state was entered, just that something may
+ * have gone wrong.
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#w(String, String)}
+ */
+ public static void w(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.WARN, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log at an error level. Error logs are used when it is known that an error occurred and is
+ * possibly fatal. This is used to log information that will be useful for troubleshooting a crash
+ * or other severe condition (e.g. error codes, state values, etc.).
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged, possibly with format arguments.
+ * @param args Optional arguments to be used in the formatted string.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#e(String, String)}
+ */
+ public static void e(@NonNull String tag, @Nullable String msg, @Nullable Object... args) {
+ println(android.util.Log.ERROR, TAG, tag, msg, args);
+ }
+
+ /**
+ * Log an exception at an error level. Error logs are used when it is known that an error occurred
+ * and is possibly fatal. This is used to log information that will be useful for troubleshooting
+ * a crash or other severe condition (e.g. error codes, state values, etc.).
+ *
+ * @param tag An identifier to allow searching for related logs. Generally of the form
+ * 'Class.method'.
+ * @param msg The message you would like logged.
+ * @param throwable The exception to log.
+ * @see {@link String#format(String, Object...)}
+ * @see {@link android.util.Log#e(String, String)}
+ */
+ public static void e(@NonNull String tag, @Nullable String msg, @NonNull Throwable throwable) {
+ if (!TextUtils.isEmpty(msg)) {
+ println(android.util.Log.ERROR, TAG, tag, msg);
+ }
+ println(android.util.Log.ERROR, TAG, tag, android.util.Log.getStackTraceString(throwable));
+ }
+
+ /**
+ * Used for log statements where we don't want to log various strings (e.g., usernames) with
+ * default logging to avoid leaking PII in logcat.
+ *
+ * @return text as is if {@value #TAG}'s log level is set to DEBUG or VERBOSE or on non-release
+ * builds; returns a redacted version otherwise.
+ */
+ public static String sanitizePii(@Nullable Object object) {
+ if (object == null) {
+ return "null";
+ }
+ if (isDebugEnabled()) {
+ return object.toString();
+ }
+ return "Redacted-" + object.toString().length() + "-chars";
+ }
+
+ /** Anonymizes char to prevent logging personally identifiable information. */
+ public static char sanitizeDialPadChar(char ch) {
+ if (isDebugEnabled()) {
+ return ch;
+ }
+ if (is12Key(ch)) {
+ return '*';
+ }
+ return ch;
+ }
+
+ /** Anonymizes the phone number to prevent logging personally identifiable information. */
+ public static String sanitizePhoneNumber(@Nullable String phoneNumber) {
+ if (isDebugEnabled()) {
+ return phoneNumber;
+ }
+ if (phoneNumber == null) {
+ return null;
+ }
+ StringBuilder stringBuilder = new StringBuilder(phoneNumber.length());
+ for (char c : phoneNumber.toCharArray()) {
+ stringBuilder.append(sanitizeDialPadChar(c));
+ }
+ return stringBuilder.toString();
+ }
+
+ public static boolean isVerboseEnabled() {
+ return android.util.Log.isLoggable(TAG, android.util.Log.VERBOSE);
+ }
+
+ public static boolean isDebugEnabled() {
+ return android.util.Log.isLoggable(TAG, android.util.Log.DEBUG);
+ }
+
+ private static boolean is12Key(char ch) {
+ return PhoneNumberUtils.is12Key(ch);
+ }
+
+ private static void println(
+ int level,
+ @NonNull String tag,
+ @NonNull String localTag,
+ @Nullable String msg,
+ @Nullable Object... args) {
+ // Formatted message is computed lazily if required.
+ String formattedMsg;
+ // Either null is passed as a single argument or more than one argument is passed.
+ boolean hasArgs = args == null || args.length > 0;
+ if ((level >= android.util.Log.INFO) || android.util.Log.isLoggable(tag, level)) {
+ formattedMsg = localTag;
+ if (!TextUtils.isEmpty(msg)) {
+ formattedMsg += SEPARATOR + (hasArgs ? String.format(msg, args) : msg);
+ }
+ android.util.Log.println(level, tag, formattedMsg);
+ }
+ }
+}
diff --git a/java/com/android/dialer/common/MathUtil.java b/java/com/android/dialer/common/MathUtil.java
new file mode 100644
index 000000000..e811a46e2
--- /dev/null
+++ b/java/com/android/dialer/common/MathUtil.java
@@ -0,0 +1,57 @@
+/*
+ * 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.common;
+
+/** Utility class for common math operations */
+public class MathUtil {
+
+ /**
+ * Interpolates between two integer values based on percentage.
+ *
+ * @param begin Begin value
+ * @param end End value
+ * @param percent Percentage value, between 0 and 1
+ * @return Interpolated result
+ */
+ public static int lerp(int begin, int end, float percent) {
+ return (int) (begin * (1 - percent) + end * percent);
+ }
+
+ /**
+ * Interpolates between two float values based on percentage.
+ *
+ * @param begin Begin value
+ * @param end End value
+ * @param percent Percentage value, between 0 and 1
+ * @return Interpolated result
+ */
+ public static float lerp(float begin, float end, float percent) {
+ return begin * (1 - percent) + end * percent;
+ }
+
+ /**
+ * Clamps a value between two bounds inclusively.
+ *
+ * @param value Value to be clamped
+ * @param min Lower bound
+ * @param max Upper bound
+ * @return Clamped value
+ */
+ public static float clamp(float value, float min, float max) {
+ return Math.max(min, Math.min(value, max));
+ }
+}
diff --git a/java/com/android/dialer/common/NetworkUtil.java b/java/com/android/dialer/common/NetworkUtil.java
new file mode 100644
index 000000000..47d84243e
--- /dev/null
+++ b/java/com/android/dialer/common/NetworkUtil.java
@@ -0,0 +1,192 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.StringDef;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/** Utility class for dealing with network */
+public class NetworkUtil {
+
+ /* Returns the current network type. */
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ @NetworkType
+ public static String getCurrentNetworkType(@Nullable Context context) {
+ if (context == null) {
+ return NetworkType.NONE;
+ }
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ return getNetworkType(connectivityManager.getActiveNetworkInfo());
+ }
+
+ /* Returns the current network info. */
+ @Nullable
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ public static NetworkInfo getCurrentNetworkInfo(@Nullable Context context) {
+ if (context == null) {
+ return null;
+ }
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ return connectivityManager.getActiveNetworkInfo();
+ }
+
+ /**
+ * Returns the current network type as a string. For mobile network types the subtype name of the
+ * network is appended.
+ */
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ public static String getCurrentNetworkTypeName(@Nullable Context context) {
+ if (context == null) {
+ return NetworkType.NONE;
+ }
+ ConnectivityManager connectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo netInfo = connectivityManager.getActiveNetworkInfo();
+ @NetworkType String networkType = getNetworkType(netInfo);
+ if (isNetworkTypeMobile(networkType)) {
+ return networkType + " (" + netInfo.getSubtypeName() + ")";
+ }
+ return networkType;
+ }
+
+ @NetworkType
+ public static String getNetworkType(@Nullable NetworkInfo netInfo) {
+ if (netInfo == null || !netInfo.isConnected()) {
+ return NetworkType.NONE;
+ }
+ switch (netInfo.getType()) {
+ case ConnectivityManager.TYPE_WIFI:
+ return NetworkType.WIFI;
+ case ConnectivityManager.TYPE_MOBILE:
+ return getMobileNetworkType(netInfo.getSubtype());
+ default:
+ return NetworkType.UNKNOWN;
+ }
+ }
+
+ public static boolean isNetworkTypeMobile(@NetworkType String networkType) {
+ return Objects.equals(networkType, NetworkType.MOBILE_2G)
+ || Objects.equals(networkType, NetworkType.MOBILE_3G)
+ || Objects.equals(networkType, NetworkType.MOBILE_4G);
+ }
+
+ @RequiresPermission("android.permission.ACCESS_NETWORK_STATE")
+ public static String getCurrentNetworkName(Context context) {
+ @NetworkType String networkType = getCurrentNetworkType(context);
+ switch (networkType) {
+ case NetworkType.WIFI:
+ return getWifiNetworkName(context);
+ case NetworkType.MOBILE_2G:
+ case NetworkType.MOBILE_3G:
+ case NetworkType.MOBILE_4G:
+ case NetworkType.MOBILE_UNKNOWN:
+ return getMobileNetworkName(context);
+ default:
+ return "";
+ }
+ }
+
+ private static String getWifiNetworkName(Context context) {
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ String name = null;
+ if (context.checkSelfPermission("android.permission.ACCESS_WIFI_STATE")
+ == PackageManager.PERMISSION_GRANTED) {
+ //noinspection MissingPermission
+ WifiInfo wifiInfo = wifiMgr.getConnectionInfo();
+ if (wifiInfo == null) {
+ return "";
+ }
+ name = wifiInfo.getSSID();
+ }
+ return TextUtils.isEmpty(name)
+ ? context.getString(R.string.network_name_wifi)
+ : name.replaceAll("\"", "");
+ }
+
+ private static String getMobileNetworkName(Context context) {
+ TelephonyManager telephonyMgr =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ String name = telephonyMgr.getNetworkOperatorName();
+ return TextUtils.isEmpty(name)
+ ? context.getString(R.string.network_name_mobile)
+ : name.replaceAll("\"", "");
+ }
+
+ @NetworkType
+ private static String getMobileNetworkType(int networkSubtype) {
+ switch (networkSubtype) {
+ case TelephonyManager.NETWORK_TYPE_1xRTT:
+ case TelephonyManager.NETWORK_TYPE_CDMA:
+ case TelephonyManager.NETWORK_TYPE_EDGE:
+ case TelephonyManager.NETWORK_TYPE_GPRS:
+ case TelephonyManager.NETWORK_TYPE_IDEN:
+ return NetworkType.MOBILE_2G;
+ case TelephonyManager.NETWORK_TYPE_EHRPD:
+ case TelephonyManager.NETWORK_TYPE_EVDO_0:
+ case TelephonyManager.NETWORK_TYPE_EVDO_A:
+ case TelephonyManager.NETWORK_TYPE_EVDO_B:
+ case TelephonyManager.NETWORK_TYPE_HSDPA:
+ case TelephonyManager.NETWORK_TYPE_HSPA:
+ case TelephonyManager.NETWORK_TYPE_HSPAP:
+ case TelephonyManager.NETWORK_TYPE_HSUPA:
+ case TelephonyManager.NETWORK_TYPE_UMTS:
+ return NetworkType.MOBILE_3G;
+ case TelephonyManager.NETWORK_TYPE_LTE:
+ return NetworkType.MOBILE_4G;
+ default:
+ return NetworkType.MOBILE_UNKNOWN;
+ }
+ }
+
+ /** Network types. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ value = {
+ NetworkType.NONE,
+ NetworkType.WIFI,
+ NetworkType.MOBILE_2G,
+ NetworkType.MOBILE_3G,
+ NetworkType.MOBILE_4G,
+ NetworkType.MOBILE_UNKNOWN,
+ NetworkType.UNKNOWN
+ }
+ )
+ public @interface NetworkType {
+
+ String NONE = "NONE";
+ String WIFI = "WIFI";
+ String MOBILE_2G = "MOBILE_2G";
+ String MOBILE_3G = "MOBILE_3G";
+ String MOBILE_4G = "MOBILE_4G";
+ String MOBILE_UNKNOWN = "MOBILE_UNKNOWN";
+ String UNKNOWN = "UNKNOWN";
+ }
+}
diff --git a/java/com/android/dialer/common/UiUtil.java b/java/com/android/dialer/common/UiUtil.java
new file mode 100644
index 000000000..4c4ebea11
--- /dev/null
+++ b/java/com/android/dialer/common/UiUtil.java
@@ -0,0 +1,41 @@
+/*
+ * 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.common;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+/** Utility class for commons functions used with Android UI. */
+public class UiUtil {
+
+ /** Hides the android keyboard. */
+ public static void hideKeyboardFrom(Context context, View view) {
+ InputMethodManager imm =
+ (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ /** Opens the android keyboard. */
+ public static void openKeyboardFrom(Context context, View view) {
+ InputMethodManager inputMethodManager =
+ (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.toggleSoftInputFromWindow(
+ view.getApplicationWindowToken(), InputMethodManager.SHOW_FORCED, 0);
+ }
+}
diff --git a/java/com/android/dialer/common/res/values/strings.xml b/java/com/android/dialer/common/res/values/strings.xml
new file mode 100644
index 000000000..8e9616178
--- /dev/null
+++ b/java/com/android/dialer/common/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="network_name_wifi">Wifi</string>
+ <string name="network_name_mobile">Mobile</string>
+</resources>
diff --git a/java/com/android/dialer/compat/ActivityCompat.java b/java/com/android/dialer/compat/ActivityCompat.java
new file mode 100644
index 000000000..e59b11593
--- /dev/null
+++ b/java/com/android/dialer/compat/ActivityCompat.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.dialer.compat;
+
+import android.app.Activity;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+
+/** Utility for calling methods introduced after Marshmallow for Activities. */
+public class ActivityCompat {
+
+ public static boolean isInMultiWindowMode(Activity activity) {
+ return VERSION.SDK_INT >= VERSION_CODES.N && activity.isInMultiWindowMode();
+ }
+}
diff --git a/java/com/android/dialer/compat/AppCompatConstants.java b/java/com/android/dialer/compat/AppCompatConstants.java
new file mode 100644
index 000000000..4a51d3f9e
--- /dev/null
+++ b/java/com/android/dialer/compat/AppCompatConstants.java
@@ -0,0 +1,33 @@
+/*
+ * 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.compat;
+
+import android.provider.CallLog.Calls;
+
+public final class AppCompatConstants {
+
+ public static final int CALLS_INCOMING_TYPE = Calls.INCOMING_TYPE;
+ public static final int CALLS_OUTGOING_TYPE = Calls.OUTGOING_TYPE;
+ public static final int CALLS_MISSED_TYPE = Calls.MISSED_TYPE;
+ public static final int CALLS_VOICEMAIL_TYPE = Calls.VOICEMAIL_TYPE;
+ // Added to android.provider.CallLog.Calls in N+.
+ public static final int CALLS_REJECTED_TYPE = 5;
+ // Added to android.provider.CallLog.Calls in N+.
+ public static final int CALLS_BLOCKED_TYPE = 6;
+ // Added to android.provider.CallLog.Calls in N+.
+ public static final int CALLS_ANSWERED_EXTERNALLY_TYPE = Calls.ANSWERED_EXTERNALLY_TYPE;
+}
diff --git a/java/com/android/dialer/compat/CompatUtils.java b/java/com/android/dialer/compat/CompatUtils.java
new file mode 100644
index 000000000..673cb709b
--- /dev/null
+++ b/java/com/android/dialer/compat/CompatUtils.java
@@ -0,0 +1,222 @@
+/*
+ * 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.compat;
+
+import android.os.Build;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.Log;
+import java.lang.reflect.InvocationTargetException;
+
+public final class CompatUtils {
+
+ private static final String TAG = CompatUtils.class.getSimpleName();
+
+ /** PrioritizedMimeType is added in API level 23. */
+ public static boolean hasPrioritizedMimeType() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is compatible with multi-SIM and the phone account APIs. Can also
+ * force the version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if multi-SIM capability is available, {@code false} otherwise.
+ */
+ public static boolean isMSIMCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+ >= Build.VERSION_CODES.LOLLIPOP_MR1;
+ }
+
+ /**
+ * Determines if this version is compatible with video calling. Can also force the version to be
+ * lower through SdkVersionOverride.
+ *
+ * @return {@code true} if video calling is allowed, {@code false} otherwise.
+ */
+ public static boolean isVideoCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is capable of using presence checking for video calling. Support for
+ * video call presence indication is added in SDK 24.
+ *
+ * @return {@code true} if video presence checking is allowed, {@code false} otherwise.
+ */
+ public static boolean isVideoPresenceCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) > Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is compatible with call subject. Can also force the version to be
+ * lower through SdkVersionOverride.
+ *
+ * @return {@code true} if call subject is a feature on this device, {@code false} otherwise.
+ */
+ public static boolean isCallSubjectCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if this version is compatible with a default dialer. Can also force the version to
+ * be lower through {@link SdkVersionOverride}.
+ *
+ * @return {@code true} if default dialer is a feature on this device, {@code false} otherwise.
+ */
+ public static boolean isDefaultDialerCompatible() {
+ return isMarshmallowCompatible();
+ }
+
+ /**
+ * Determines if this version is compatible with Lollipop Mr1-specific APIs. Can also force the
+ * version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if runtime sdk is compatible with Lollipop MR1, {@code false} otherwise.
+ */
+ public static boolean isLollipopMr1Compatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP_MR1)
+ >= Build.VERSION_CODES.LOLLIPOP_MR1;
+ }
+
+ /**
+ * Determines if this version is compatible with Marshmallow-specific APIs. Can also force the
+ * version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if runtime sdk is compatible with Marshmallow, {@code false} otherwise.
+ */
+ public static boolean isMarshmallowCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP) >= Build.VERSION_CODES.M;
+ }
+
+ /**
+ * Determines if the given class is available. Can be used to check if system apis exist at
+ * runtime.
+ *
+ * @param className the name of the class to look for.
+ * @return {@code true} if the given class is available, {@code false} otherwise or if className
+ * is empty.
+ */
+ public static boolean isClassAvailable(@Nullable String className) {
+ if (TextUtils.isEmpty(className)) {
+ return false;
+ }
+ try {
+ Class.forName(className);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when checking if class:" + className + " exists at " + "runtime",
+ t);
+ return false;
+ }
+ }
+
+ /**
+ * Determines if the given class's method is available to call. Can be used to check if system
+ * apis exist at runtime.
+ *
+ * @param className the name of the class to look for
+ * @param methodName the name of the method to look for
+ * @param parameterTypes the needed parameter types for the method to look for
+ * @return {@code true} if the given class is available, {@code false} otherwise or if className
+ * or methodName are empty.
+ */
+ public static boolean isMethodAvailable(
+ @Nullable String className, @Nullable String methodName, Class<?>... parameterTypes) {
+ if (TextUtils.isEmpty(className) || TextUtils.isEmpty(methodName)) {
+ return false;
+ }
+
+ try {
+ Class.forName(className).getMethod(methodName, parameterTypes);
+ return true;
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ Log.v(TAG, "Could not find method: " + className + "#" + methodName);
+ return false;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when checking if method: "
+ + className
+ + "#"
+ + methodName
+ + " exists at runtime",
+ t);
+ return false;
+ }
+ }
+
+ /**
+ * Invokes a given class's method using reflection. Can be used to call system apis that exist at
+ * runtime but not in the SDK.
+ *
+ * @param instance The instance of the class to invoke the method on.
+ * @param methodName The name of the method to invoke.
+ * @param parameterTypes The needed parameter types for the method.
+ * @param parameters The parameter values to pass into the method.
+ * @return The result of the invocation or {@code null} if instance or methodName are empty, or if
+ * the reflection fails.
+ */
+ @Nullable
+ public static Object invokeMethod(
+ @Nullable Object instance,
+ @Nullable String methodName,
+ Class<?>[] parameterTypes,
+ Object[] parameters) {
+ if (instance == null || TextUtils.isEmpty(methodName)) {
+ return null;
+ }
+
+ String className = instance.getClass().getName();
+ try {
+ return Class.forName(className)
+ .getMethod(methodName, parameterTypes)
+ .invoke(instance, parameters);
+ } catch (ClassNotFoundException
+ | NoSuchMethodException
+ | IllegalArgumentException
+ | IllegalAccessException
+ | InvocationTargetException e) {
+ Log.v(TAG, "Could not invoke method: " + className + "#" + methodName);
+ return null;
+ } catch (Throwable t) {
+ Log.e(
+ TAG,
+ "Unexpected exception when invoking method: "
+ + className
+ + "#"
+ + methodName
+ + " at runtime",
+ t);
+ return null;
+ }
+ }
+
+ /**
+ * Determines if this version is compatible with Lollipop-specific APIs. Can also force the
+ * version to be lower through SdkVersionOverride.
+ *
+ * @return {@code true} if call subject is a feature on this device, {@code false} otherwise.
+ */
+ public static boolean isLollipopCompatible() {
+ return SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.LOLLIPOP)
+ >= Build.VERSION_CODES.LOLLIPOP;
+ }
+}
diff --git a/java/com/android/dialer/compat/PathInterpolatorCompat.java b/java/com/android/dialer/compat/PathInterpolatorCompat.java
new file mode 100644
index 000000000..7139bc4af
--- /dev/null
+++ b/java/com/android/dialer/compat/PathInterpolatorCompat.java
@@ -0,0 +1,120 @@
+/*
+ * 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.compat;
+
+import android.graphics.Path;
+import android.graphics.PathMeasure;
+import android.os.Build;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+public class PathInterpolatorCompat {
+
+ public static Interpolator create(
+ float controlX1, float controlY1, float controlX2, float controlY2) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return new PathInterpolator(controlX1, controlY1, controlX2, controlY2);
+ }
+ return new PathInterpolatorBase(controlX1, controlY1, controlX2, controlY2);
+ }
+
+ private static class PathInterpolatorBase implements Interpolator {
+
+ /** Governs the accuracy of the approximation of the {@link Path}. */
+ private static final float PRECISION = 0.002f;
+
+ private final float[] mX;
+ private final float[] mY;
+
+ public PathInterpolatorBase(Path path) {
+ final PathMeasure pathMeasure = new PathMeasure(path, false /* forceClosed */);
+
+ final float pathLength = pathMeasure.getLength();
+ final int numPoints = (int) (pathLength / PRECISION) + 1;
+
+ mX = new float[numPoints];
+ mY = new float[numPoints];
+
+ final float[] position = new float[2];
+ for (int i = 0; i < numPoints; ++i) {
+ final float distance = (i * pathLength) / (numPoints - 1);
+ pathMeasure.getPosTan(distance, position, null /* tangent */);
+
+ mX[i] = position[0];
+ mY[i] = position[1];
+ }
+ }
+
+ public PathInterpolatorBase(float controlX, float controlY) {
+ this(createQuad(controlX, controlY));
+ }
+
+ public PathInterpolatorBase(
+ float controlX1, float controlY1, float controlX2, float controlY2) {
+ this(createCubic(controlX1, controlY1, controlX2, controlY2));
+ }
+
+ private static Path createQuad(float controlX, float controlY) {
+ final Path path = new Path();
+ path.moveTo(0.0f, 0.0f);
+ path.quadTo(controlX, controlY, 1.0f, 1.0f);
+ return path;
+ }
+
+ private static Path createCubic(
+ float controlX1, float controlY1, float controlX2, float controlY2) {
+ final Path path = new Path();
+ path.moveTo(0.0f, 0.0f);
+ path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0f, 1.0f);
+ return path;
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ if (t <= 0.0f) {
+ return 0.0f;
+ } else if (t >= 1.0f) {
+ return 1.0f;
+ }
+
+ // Do a binary search for the correct x to interpolate between.
+ int startIndex = 0;
+ int endIndex = mX.length - 1;
+ while (endIndex - startIndex > 1) {
+ int midIndex = (startIndex + endIndex) / 2;
+ if (t < mX[midIndex]) {
+ endIndex = midIndex;
+ } else {
+ startIndex = midIndex;
+ }
+ }
+
+ final float xRange = mX[endIndex] - mX[startIndex];
+ if (xRange == 0) {
+ return mY[startIndex];
+ }
+
+ final float tInRange = t - mX[startIndex];
+ final float fraction = tInRange / xRange;
+
+ final float startY = mY[startIndex];
+ final float endY = mY[endIndex];
+
+ return startY + (fraction * (endY - startY));
+ }
+ }
+}
diff --git a/java/com/android/dialer/compat/SdkVersionOverride.java b/java/com/android/dialer/compat/SdkVersionOverride.java
new file mode 100644
index 000000000..1d253a355
--- /dev/null
+++ b/java/com/android/dialer/compat/SdkVersionOverride.java
@@ -0,0 +1,43 @@
+/*
+ * 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.compat;
+
+import android.os.Build.VERSION;
+
+/**
+ * Class used to override the current sdk version to test specific branches of compatibility logic.
+ * When such branching occurs, use {@link #getSdkVersion(int)} rather than explicitly calling {@link
+ * VERSION#SDK_INT}. This allows the sdk version to be forced to a specific value.
+ */
+public class SdkVersionOverride {
+
+ /** Flag used to determine if override sdk versions are returned. */
+ private static final boolean ALLOW_OVERRIDE_VERSION = false;
+
+ private SdkVersionOverride() {}
+
+ /**
+ * Gets the sdk version
+ *
+ * @param overrideVersion the version to attempt using
+ * @return overrideVersion if the {@link #ALLOW_OVERRIDE_VERSION} flag is set to {@code true},
+ * otherwise the current version
+ */
+ public static int getSdkVersion(int overrideVersion) {
+ return ALLOW_OVERRIDE_VERSION ? overrideVersion : VERSION.SDK_INT;
+ }
+}
diff --git a/java/com/android/dialer/constants/Constants.java b/java/com/android/dialer/constants/Constants.java
new file mode 100644
index 000000000..77773018a
--- /dev/null
+++ b/java/com/android/dialer/constants/Constants.java
@@ -0,0 +1,47 @@
+/*
+ * 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.constants;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.dialer.proguard.UsedByReflection;
+import com.android.dialer.constants.ConstantsImpl;
+
+/**
+ * Utility to access constants that are different across build variants (Google Dialer, AOSP,
+ * etc...). This functionality depends on a an implementation being present in the app that has the
+ * same package and the class name ending in "Impl". For example,
+ * com.android.dialer.constants.ConstantsImpl. This class is found by the module using reflection.
+ */
+@UsedByReflection(value = "Constants.java")
+public abstract class Constants {
+ private static Constants instance = new ConstantsImpl();
+ private static boolean didInitializeInstance;
+
+ @NonNull
+ public static synchronized Constants get() {
+ return instance;
+ }
+
+ @NonNull
+ public abstract String getFilteredNumberProviderAuthority();
+
+ @NonNull
+ public abstract String getFileProviderAuthority();
+
+ protected Constants() {}
+}
diff --git a/java/com/android/dialer/constants/ScheduledJobIds.java b/java/com/android/dialer/constants/ScheduledJobIds.java
new file mode 100644
index 000000000..88e9a3d4f
--- /dev/null
+++ b/java/com/android/dialer/constants/ScheduledJobIds.java
@@ -0,0 +1,31 @@
+/*
+ * 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.constants;
+
+/**
+ * Registry of scheduled job ids used by the dialer UID.
+ *
+ * <p>Any dialer jobs which use the android JobScheduler should register their IDs here, to avoid
+ * the same ID accidentally being reused.
+ */
+public final class ScheduledJobIds {
+ public static final int SPAM_JOB_WIFI = 50;
+ public static final int SPAM_JOB_ANY_NETWORK = 51;
+
+ // This job refreshes dynamic launcher shortcuts.
+ public static final int SHORTCUT_PERIODIC_JOB = 100;
+}
diff --git a/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
new file mode 100644
index 000000000..6b78b986c
--- /dev/null
+++ b/java/com/android/dialer/constants/aospdialer/ConstantsImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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.constants;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.proguard.UsedByReflection;
+
+/** Provider config values for AOSP Dialer. */
+@UsedByReflection(value = "Constants.java")
+public class ConstantsImpl extends Constants {
+
+ @Override
+ @NonNull
+ public String getFilteredNumberProviderAuthority() {
+ return "com.android.dialer.blocking.filterednumberprovider";
+ }
+
+ @Override
+ @NonNull
+ public String getFileProviderAuthority() {
+ return "com.android.dialer.files";
+ }
+}
diff --git a/java/com/android/dialer/database/CallLogQueryHandler.java b/java/com/android/dialer/database/CallLogQueryHandler.java
new file mode 100644
index 000000000..ffca69f40
--- /dev/null
+++ b/java/com/android/dialer/database/CallLogQueryHandler.java
@@ -0,0 +1,369 @@
+/*
+ * 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.database;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.VoicemailContract.Status;
+import android.provider.VoicemailContract.Voicemails;
+import com.android.contacts.common.database.NoNullCursorAsyncQueryHandler;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.AppCompatConstants;
+import com.android.dialer.compat.SdkVersionOverride;
+import com.android.dialer.phonenumbercache.CallLogQuery;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Handles asynchronous queries to the call log. */
+public class CallLogQueryHandler extends NoNullCursorAsyncQueryHandler {
+
+ /**
+ * Call type similar to Calls.INCOMING_TYPE used to specify all types instead of one particular
+ * type. Exception: excludes Calls.VOICEMAIL_TYPE.
+ */
+ public static final int CALL_TYPE_ALL = -1;
+
+ private static final String TAG = "CallLogQueryHandler";
+ private static final int NUM_LOGS_TO_DISPLAY = 1000;
+ /** The token for the query to fetch the old entries from the call log. */
+ private static final int QUERY_CALLLOG_TOKEN = 54;
+ /** The token for the query to mark all missed calls as old after seeing the call log. */
+ private static final int UPDATE_MARK_AS_OLD_TOKEN = 55;
+ /** The token for the query to mark all missed calls as read after seeing the call log. */
+ private static final int UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN = 56;
+ /** The token for the query to fetch voicemail status messages. */
+ private static final int QUERY_VOICEMAIL_STATUS_TOKEN = 57;
+ /** The token for the query to fetch the number of unread voicemails. */
+ private static final int QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN = 58;
+ /** The token for the query to fetch the number of missed calls. */
+ private static final int QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN = 59;
+
+ private final int mLogLimit;
+ private final WeakReference<Listener> mListener;
+
+ private final Context mContext;
+
+ public CallLogQueryHandler(Context context, ContentResolver contentResolver, Listener listener) {
+ this(context, contentResolver, listener, -1);
+ }
+
+ public CallLogQueryHandler(
+ Context context, ContentResolver contentResolver, Listener listener, int limit) {
+ super(contentResolver);
+ mContext = context.getApplicationContext();
+ mListener = new WeakReference<Listener>(listener);
+ mLogLimit = limit;
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ // Provide our special handler that catches exceptions
+ return new CatchingWorkerHandler(looper);
+ }
+
+ /**
+ * Fetches the list of calls from the call log for a given type. This call ignores the new or old
+ * state.
+ *
+ * <p>It will asynchronously update the content of the list view when the fetch completes.
+ */
+ public void fetchCalls(int callType, long newerThan) {
+ cancelFetch();
+ if (PermissionsUtil.hasPhonePermissions(mContext)) {
+ fetchCalls(QUERY_CALLLOG_TOKEN, callType, false /* newOnly */, newerThan);
+ } else {
+ updateAdapterData(null);
+ }
+ }
+
+ public void fetchCalls(int callType) {
+ fetchCalls(callType, 0);
+ }
+
+ public void fetchVoicemailStatus() {
+ if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
+ startQuery(
+ QUERY_VOICEMAIL_STATUS_TOKEN,
+ null,
+ Status.CONTENT_URI,
+ VoicemailStatusQuery.getProjection(),
+ null,
+ null,
+ null);
+ }
+ }
+
+ public void fetchVoicemailUnreadCount() {
+ if (TelecomUtil.hasReadWriteVoicemailPermissions(mContext)) {
+ // Only count voicemails that have not been read and have not been deleted.
+ startQuery(
+ QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN,
+ null,
+ Voicemails.CONTENT_URI,
+ new String[] {Voicemails._ID},
+ Voicemails.IS_READ + "=0" + " AND " + Voicemails.DELETED + "=0",
+ null,
+ null);
+ }
+ }
+
+ /** Fetches the list of calls in the call log. */
+ private void fetchCalls(int token, int callType, boolean newOnly, long newerThan) {
+ StringBuilder where = new StringBuilder();
+ List<String> selectionArgs = new ArrayList<>();
+
+ // Always hide blocked calls.
+ where.append("(").append(Calls.TYPE).append(" != ?)");
+ selectionArgs.add(Integer.toString(AppCompatConstants.CALLS_BLOCKED_TYPE));
+
+ // Ignore voicemails marked as deleted
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+ where.append(" AND (").append(Voicemails.DELETED).append(" = 0)");
+ }
+
+ if (newOnly) {
+ where.append(" AND (").append(Calls.NEW).append(" = 1)");
+ }
+
+ if (callType > CALL_TYPE_ALL) {
+ where.append(" AND (").append(Calls.TYPE).append(" = ?)");
+ selectionArgs.add(Integer.toString(callType));
+ } else {
+ where.append(" AND NOT ");
+ where.append("(" + Calls.TYPE + " = " + AppCompatConstants.CALLS_VOICEMAIL_TYPE + ")");
+ }
+
+ if (newerThan > 0) {
+ where.append(" AND (").append(Calls.DATE).append(" > ?)");
+ selectionArgs.add(Long.toString(newerThan));
+ }
+
+ final int limit = (mLogLimit == -1) ? NUM_LOGS_TO_DISPLAY : mLogLimit;
+ final String selection = where.length() > 0 ? where.toString() : null;
+ Uri uri =
+ TelecomUtil.getCallLogUri(mContext)
+ .buildUpon()
+ .appendQueryParameter(Calls.LIMIT_PARAM_KEY, Integer.toString(limit))
+ .build();
+ startQuery(
+ token,
+ null,
+ uri,
+ CallLogQuery.getProjection(),
+ selection,
+ selectionArgs.toArray(new String[selectionArgs.size()]),
+ Calls.DEFAULT_SORT_ORDER);
+ }
+
+ /** Cancel any pending fetch request. */
+ private void cancelFetch() {
+ cancelOperation(QUERY_CALLLOG_TOKEN);
+ }
+
+ /** Updates all new calls to mark them as old. */
+ public void markNewCallsAsOld() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+ // Mark all "new" calls as not new anymore.
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1");
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.NEW, "0");
+
+ startUpdate(
+ UPDATE_MARK_AS_OLD_TOKEN,
+ null,
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ where.toString(),
+ null);
+ }
+
+ /** Updates all missed calls to mark them as read. */
+ public void markMissedCallsAsRead() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+
+ ContentValues values = new ContentValues(1);
+ values.put(Calls.IS_READ, "1");
+
+ startUpdate(
+ UPDATE_MARK_MISSED_CALL_AS_READ_TOKEN,
+ null,
+ Calls.CONTENT_URI,
+ values,
+ getUnreadMissedCallsQuery(),
+ null);
+ }
+
+ /** Fetch all missed calls received since last time the tab was opened. */
+ public void fetchMissedCallsUnreadCount() {
+ if (!PermissionsUtil.hasPhonePermissions(mContext)) {
+ return;
+ }
+
+ startQuery(
+ QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN,
+ null,
+ Calls.CONTENT_URI,
+ new String[] {Calls._ID},
+ getUnreadMissedCallsQuery(),
+ null,
+ null);
+ }
+
+ @Override
+ protected synchronized void onNotNullableQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (cursor == null) {
+ return;
+ }
+ try {
+ if (token == QUERY_CALLLOG_TOKEN) {
+ if (updateAdapterData(cursor)) {
+ cursor = null;
+ }
+ } else if (token == QUERY_VOICEMAIL_STATUS_TOKEN) {
+ updateVoicemailStatus(cursor);
+ } else if (token == QUERY_VOICEMAIL_UNREAD_COUNT_TOKEN) {
+ updateVoicemailUnreadCount(cursor);
+ } else if (token == QUERY_MISSED_CALLS_UNREAD_COUNT_TOKEN) {
+ updateMissedCallsUnreadCount(cursor);
+ } else {
+ LogUtil.w(
+ "CallLogQueryHandler.onNotNullableQueryComplete",
+ "unknown query completed: ignoring: " + token);
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Updates the adapter in the call log fragment to show the new cursor data. Returns true if the
+ * listener took ownership of the cursor.
+ */
+ private boolean updateAdapterData(Cursor cursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ return listener.onCallsFetched(cursor);
+ }
+ return false;
+ }
+
+ /** @return Query string to get all unread missed calls. */
+ private String getUnreadMissedCallsQuery() {
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.IS_READ).append(" = 0 OR ").append(Calls.IS_READ).append(" IS NULL");
+ where.append(" AND ");
+ where.append(Calls.TYPE).append(" = ").append(Calls.MISSED_TYPE);
+ return where.toString();
+ }
+
+ private void updateVoicemailStatus(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onVoicemailStatusFetched(statusCursor);
+ }
+ }
+
+ private void updateVoicemailUnreadCount(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onVoicemailUnreadCountFetched(statusCursor);
+ }
+ }
+
+ private void updateMissedCallsUnreadCount(Cursor statusCursor) {
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onMissedCallsUnreadCountFetched(statusCursor);
+ }
+ }
+
+ /** Listener to completion of various queries. */
+ public interface Listener {
+
+ /** Called when {@link CallLogQueryHandler#fetchVoicemailStatus()} completes. */
+ void onVoicemailStatusFetched(Cursor statusCursor);
+
+ /** Called when {@link CallLogQueryHandler#fetchVoicemailUnreadCount()} completes. */
+ void onVoicemailUnreadCountFetched(Cursor cursor);
+
+ /** Called when {@link CallLogQueryHandler#fetchMissedCallsUnreadCount()} completes. */
+ void onMissedCallsUnreadCountFetched(Cursor cursor);
+
+ /**
+ * Called when {@link CallLogQueryHandler#fetchCalls(int)} complete. Returns true if takes
+ * ownership of cursor.
+ */
+ boolean onCallsFetched(Cursor combinedCursor);
+ }
+
+ /**
+ * Simple handler that wraps background calls to catch {@link SQLiteException}, such as when the
+ * disk is full.
+ */
+ protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
+
+ public CatchingWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ try {
+ // Perform same query while catching any exceptions
+ super.handleMessage(msg);
+ } catch (SQLiteDiskIOException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "exception on background worker thread", e);
+ } catch (SQLiteFullException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "exception on background worker thread", e);
+ } catch (SQLiteDatabaseCorruptException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "exception on background worker thread", e);
+ } catch (IllegalArgumentException e) {
+ LogUtil.e("CallLogQueryHandler.handleMessage", "contactsProvider not present on device", e);
+ } catch (SecurityException e) {
+ // Shouldn't happen if we are protecting the entry points correctly,
+ // but just in case.
+ LogUtil.e(
+ "CallLogQueryHandler.handleMessage", "no permission to access ContactsProvider.", e);
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/database/Database.java b/java/com/android/dialer/database/Database.java
new file mode 100644
index 000000000..d13f15e48
--- /dev/null
+++ b/java/com/android/dialer/database/Database.java
@@ -0,0 +1,49 @@
+/*
+ * 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.database;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the database bindings. */
+public class Database {
+
+ private static DatabaseBindings databaseBindings;
+
+ private Database() {}
+
+ public static DatabaseBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (databaseBindings != null) {
+ return databaseBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof DatabaseBindingsFactory) {
+ databaseBindings = ((DatabaseBindingsFactory) application).newDatabaseBindings();
+ }
+
+ if (databaseBindings == null) {
+ databaseBindings = new DatabaseBindingsStub();
+ }
+ return databaseBindings;
+ }
+
+ public static void setForTesting(DatabaseBindings databaseBindings) {
+ Database.databaseBindings = databaseBindings;
+ }
+}
diff --git a/java/com/android/dialer/database/DatabaseBindings.java b/java/com/android/dialer/database/DatabaseBindings.java
new file mode 100644
index 000000000..f07b265b3
--- /dev/null
+++ b/java/com/android/dialer/database/DatabaseBindings.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.database;
+
+import android.content.Context;
+
+/** This interface allows the container application to customize the database module. */
+public interface DatabaseBindings {
+
+ DialerDatabaseHelper getDatabaseHelper(Context context);
+}
diff --git a/java/com/android/dialer/database/DatabaseBindingsFactory.java b/java/com/android/dialer/database/DatabaseBindingsFactory.java
new file mode 100644
index 000000000..7fa175ed5
--- /dev/null
+++ b/java/com/android/dialer/database/DatabaseBindingsFactory.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.database;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the dialer module
+ * to get references to the DatabaseBindings.
+ */
+public interface DatabaseBindingsFactory {
+
+ DatabaseBindings newDatabaseBindings();
+}
diff --git a/java/com/android/dialer/database/DatabaseBindingsStub.java b/java/com/android/dialer/database/DatabaseBindingsStub.java
new file mode 100644
index 000000000..df8186ab0
--- /dev/null
+++ b/java/com/android/dialer/database/DatabaseBindingsStub.java
@@ -0,0 +1,35 @@
+/*
+ * 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.database;
+
+import android.content.Context;
+
+/** Default implementation for database bindings. */
+public class DatabaseBindingsStub implements DatabaseBindings {
+
+ private DialerDatabaseHelper dialerDatabaseHelper;
+
+ @Override
+ public DialerDatabaseHelper getDatabaseHelper(Context context) {
+ if (dialerDatabaseHelper == null) {
+ dialerDatabaseHelper =
+ new DialerDatabaseHelper(
+ context, DialerDatabaseHelper.DATABASE_NAME, DialerDatabaseHelper.DATABASE_VERSION);
+ }
+ return dialerDatabaseHelper;
+ }
+}
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
new file mode 100644
index 000000000..234958b62
--- /dev/null
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -0,0 +1,1242 @@
+/*
+ * 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.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.StopWatch;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.smartdial.SmartDialNameMatcher;
+import com.android.dialer.smartdial.SmartDialPrefix;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Database helper for smart dial. Designed as a singleton to make sure there is only one access
+ * point to the database. Provides methods to maintain, update, and query the database.
+ */
+public class DialerDatabaseHelper extends SQLiteOpenHelper {
+
+ /**
+ * SmartDial DB version ranges:
+ *
+ * <pre>
+ * 0-98 KitKat
+ * </pre>
+ */
+ public static final int DATABASE_VERSION = 10;
+
+ public static final String DATABASE_NAME = "dialer.db";
+ public static final Uri SMART_DIAL_UPDATED_URI =
+ Uri.parse("content://com.android.dialer/smart_dial_updated");
+ private static final String TAG = "DialerDatabaseHelper";
+ private static final boolean DEBUG = false;
+ /** Saves the last update time of smart dial databases to shared preferences. */
+ private static final String DATABASE_LAST_CREATED_SHARED_PREF = "com.android.dialer";
+
+ private static final String LAST_UPDATED_MILLIS = "last_updated_millis";
+ private static final String DATABASE_VERSION_PROPERTY = "database_version";
+ private static final int MAX_ENTRIES = 20;
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+ private final AtomicBoolean mInUpdate = new AtomicBoolean(false);
+ private boolean mIsTestInstance = false;
+
+ protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) {
+ super(context, databaseName, null, dbVersion);
+ mContext = Objects.requireNonNull(context, "Context must not be null");
+ }
+
+ public void setIsTestInstance(boolean isTestInstance) {
+ mIsTestInstance = isTestInstance;
+ }
+
+ /**
+ * Creates tables in the database when database is created for the first time.
+ *
+ * @param db The database.
+ */
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ setupTables(db);
+ }
+
+ private void setupTables(SQLiteDatabase db) {
+ dropTables(db);
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + SmartDialDbColumns.DATA_ID
+ + " INTEGER, "
+ + SmartDialDbColumns.NUMBER
+ + " TEXT,"
+ + SmartDialDbColumns.CONTACT_ID
+ + " INTEGER,"
+ + SmartDialDbColumns.LOOKUP_KEY
+ + " TEXT,"
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + " TEXT, "
+ + SmartDialDbColumns.PHOTO_ID
+ + " INTEGER, "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " LONG, "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + " LONG, "
+ + SmartDialDbColumns.TIMES_USED
+ + " INTEGER, "
+ + SmartDialDbColumns.STARRED
+ + " INTEGER, "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + " INTEGER, "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + " INTEGER, "
+ + SmartDialDbColumns.IS_PRIMARY
+ + " INTEGER, "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + " INTEGER NOT NULL DEFAULT 0"
+ + ");");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + PrefixColumns.PREFIX
+ + " TEXT COLLATE NOCASE, "
+ + PrefixColumns.CONTACT_ID
+ + " INTEGER"
+ + ");");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.PROPERTIES
+ + " ("
+ + PropertiesColumns.PROPERTY_KEY
+ + " TEXT PRIMARY KEY, "
+ + PropertiesColumns.PROPERTY_VALUE
+ + " TEXT "
+ + ");");
+
+ // This will need to also be updated in setupTablesForFilteredNumberTest and onUpgrade.
+ // Hardcoded so we know on glance what columns are updated in setupTables,
+ // and to be able to guarantee the state of the DB at each upgrade step.
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.FILTERED_NUMBER_TABLE
+ + " ("
+ + FilteredNumberColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + FilteredNumberColumns.NORMALIZED_NUMBER
+ + " TEXT UNIQUE,"
+ + FilteredNumberColumns.NUMBER
+ + " TEXT,"
+ + FilteredNumberColumns.COUNTRY_ISO
+ + " TEXT,"
+ + FilteredNumberColumns.TIMES_FILTERED
+ + " INTEGER,"
+ + FilteredNumberColumns.LAST_TIME_FILTERED
+ + " LONG,"
+ + FilteredNumberColumns.CREATION_TIME
+ + " LONG,"
+ + FilteredNumberColumns.TYPE
+ + " INTEGER,"
+ + FilteredNumberColumns.SOURCE
+ + " INTEGER"
+ + ");");
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ if (!mIsTestInstance) {
+ resetSmartDialLastUpdatedTime();
+ }
+ }
+
+ public void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PREFIX_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.SMARTDIAL_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PROPERTIES);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber) {
+ // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read
+ // our own from the database.
+
+ int oldVersion;
+
+ oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0);
+
+ if (oldVersion == 0) {
+ LogUtil.e(
+ "DialerDatabaseHelper.onUpgrade", "malformed database version..recreating database");
+ }
+
+ if (oldVersion < 4) {
+ setupTables(db);
+ return;
+ }
+
+ if (oldVersion < 7) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.FILTERED_NUMBER_TABLE
+ + " ("
+ + FilteredNumberColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + FilteredNumberColumns.NORMALIZED_NUMBER
+ + " TEXT UNIQUE,"
+ + FilteredNumberColumns.NUMBER
+ + " TEXT,"
+ + FilteredNumberColumns.COUNTRY_ISO
+ + " TEXT,"
+ + FilteredNumberColumns.TIMES_FILTERED
+ + " INTEGER,"
+ + FilteredNumberColumns.LAST_TIME_FILTERED
+ + " LONG,"
+ + FilteredNumberColumns.CREATION_TIME
+ + " LONG,"
+ + FilteredNumberColumns.TYPE
+ + " INTEGER,"
+ + FilteredNumberColumns.SOURCE
+ + " INTEGER"
+ + ");");
+ oldVersion = 7;
+ }
+
+ if (oldVersion < 8) {
+ upgradeToVersion8(db);
+ oldVersion = 8;
+ }
+
+ if (oldVersion < 10) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE);
+ oldVersion = 10;
+ }
+
+ if (oldVersion != DATABASE_VERSION) {
+ throw new IllegalStateException(
+ "error upgrading the database to version " + DATABASE_VERSION);
+ }
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ }
+
+ public void upgradeToVersion8(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE smartdial_table ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+ }
+
+ /** Stores a key-value pair in the {@link Tables#PROPERTIES} table. */
+ public void setProperty(String key, String value) {
+ setProperty(getWritableDatabase(), key, value);
+ }
+
+ public void setProperty(SQLiteDatabase db, String key, String value) {
+ final ContentValues values = new ContentValues();
+ values.put(PropertiesColumns.PROPERTY_KEY, key);
+ values.put(PropertiesColumns.PROPERTY_VALUE, value);
+ db.replace(Tables.PROPERTIES, null, values);
+ }
+
+ /** Returns the value from the {@link Tables#PROPERTIES} table. */
+ public String getProperty(String key, String defaultValue) {
+ return getProperty(getReadableDatabase(), key, defaultValue);
+ }
+
+ public String getProperty(SQLiteDatabase db, String key, String defaultValue) {
+ try {
+ String value = null;
+ final Cursor cursor =
+ db.query(
+ Tables.PROPERTIES,
+ new String[] {PropertiesColumns.PROPERTY_VALUE},
+ PropertiesColumns.PROPERTY_KEY + "=?",
+ new String[] {key},
+ null,
+ null,
+ null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ value = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return value != null ? value : defaultValue;
+ } catch (SQLiteException e) {
+ return defaultValue;
+ }
+ }
+
+ public int getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue) {
+ final String stored = getProperty(db, key, "");
+ try {
+ return Integer.parseInt(stored);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ private void resetSmartDialLastUpdatedTime() {
+ final SharedPreferences databaseLastUpdateSharedPref =
+ mContext.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
+ final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
+ editor.putLong(LAST_UPDATED_MILLIS, 0);
+ editor.apply();
+ }
+
+ /** Starts the database upgrade process in the background. */
+ public void startSmartDialUpdateThread() {
+ if (PermissionsUtil.hasContactsPermissions(mContext)) {
+ new SmartDialUpdateAsyncTask().execute();
+ }
+ }
+
+ /**
+ * Removes rows in the smartdial database that matches the contacts that have been deleted by
+ * other apps since last update.
+ *
+ * @param db Database to operate on.
+ * @param deletedContactCursor Cursor containing rows of deleted contacts
+ */
+ @VisibleForTesting
+ void removeDeletedContacts(SQLiteDatabase db, Cursor deletedContactCursor) {
+ if (deletedContactCursor == null) {
+ return;
+ }
+
+ db.beginTransaction();
+ try {
+ while (deletedContactCursor.moveToNext()) {
+ final Long deleteContactId =
+ deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID);
+ db.delete(
+ Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + deleteContactId, null);
+ db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + deleteContactId, null);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ deletedContactCursor.close();
+ db.endTransaction();
+ }
+ }
+
+ private Cursor getDeletedContactCursor(String lastUpdateMillis) {
+ return mContext
+ .getContentResolver()
+ .query(
+ DeleteContactQuery.URI,
+ DeleteContactQuery.PROJECTION,
+ DeleteContactQuery.SELECT_UPDATED_CLAUSE,
+ new String[] {lastUpdateMillis},
+ null);
+ }
+
+ /**
+ * Removes potentially corrupted entries in the database. These contacts may be added before the
+ * previous instance of the dialer was destroyed for some reason. For data integrity, we delete
+ * all of them.
+ *
+ * @param db Database pointer to the dialer database.
+ * @param last_update_time Time stamp of last successful update of the dialer database.
+ */
+ private void removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time) {
+ db.delete(
+ Tables.PREFIX_TABLE,
+ PrefixColumns.CONTACT_ID
+ + " IN "
+ + "(SELECT "
+ + SmartDialDbColumns.CONTACT_ID
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " > "
+ + last_update_time
+ + ")",
+ null);
+ db.delete(
+ Tables.SMARTDIAL_TABLE,
+ SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " + last_update_time,
+ null);
+ }
+
+ /**
+ * Removes rows in the smartdial database that matches updated contacts.
+ *
+ * @param db Database pointer to the smartdial database
+ * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
+ */
+ @VisibleForTesting
+ void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) {
+ db.beginTransaction();
+ try {
+ updatedContactCursor.moveToPosition(-1);
+ while (updatedContactCursor.moveToNext()) {
+ final Long contactId = updatedContactCursor.getLong(UpdatedContactQuery.UPDATED_CONTACT_ID);
+
+ db.delete(Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + contactId, null);
+ db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + contactId, null);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Inserts updated contacts as rows to the smartdial table.
+ *
+ * @param db Database pointer to the smartdial database.
+ * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
+ * @param currentMillis Current time to be recorded in the smartdial table as update timestamp.
+ */
+ @VisibleForTesting
+ protected void insertUpdatedContactsAndNumberPrefix(
+ SQLiteDatabase db, Cursor updatedContactCursor, Long currentMillis) {
+ db.beginTransaction();
+ try {
+ final String sqlInsert =
+ "INSERT INTO "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.DATA_ID
+ + ", "
+ + SmartDialDbColumns.NUMBER
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.LOOKUP_KEY
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.PHOTO_ID
+ + ", "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + ", "
+ + SmartDialDbColumns.TIMES_USED
+ + ", "
+ + SmartDialDbColumns.STARRED
+ + ", "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + ", "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + ", "
+ + SmartDialDbColumns.IS_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + ", "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + ") "
+ + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ final SQLiteStatement insert = db.compileStatement(sqlInsert);
+
+ final String numberSqlInsert =
+ "INSERT INTO "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ", "
+ + PrefixColumns.PREFIX
+ + ") "
+ + " VALUES (?, ?)";
+ final SQLiteStatement numberInsert = db.compileStatement(numberSqlInsert);
+
+ updatedContactCursor.moveToPosition(-1);
+ while (updatedContactCursor.moveToNext()) {
+ insert.clearBindings();
+
+ // Handle string columns which can possibly be null first. In the case of certain
+ // null columns (due to malformed rows possibly inserted by third-party apps
+ // or sync adapters), skip the phone number row.
+ final String number = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
+ if (TextUtils.isEmpty(number)) {
+ continue;
+ } else {
+ insert.bindString(2, number);
+ }
+
+ final String lookupKey = updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY);
+ if (TextUtils.isEmpty(lookupKey)) {
+ continue;
+ } else {
+ insert.bindString(4, lookupKey);
+ }
+
+ final String displayName = updatedContactCursor.getString(PhoneQuery.PHONE_DISPLAY_NAME);
+ if (displayName == null) {
+ insert.bindString(5, mContext.getResources().getString(R.string.missing_name));
+ } else {
+ insert.bindString(5, displayName);
+ }
+ insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID));
+ insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
+ insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID));
+ insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED));
+ insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED));
+ insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED));
+ insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY));
+ insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP));
+ insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY));
+ insert.bindLong(13, updatedContactCursor.getInt(PhoneQuery.PHONE_CARRIER_PRESENCE));
+ insert.bindLong(14, currentMillis);
+ insert.executeInsert();
+ final String contactPhoneNumber = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
+ final ArrayList<String> numberPrefixes =
+ SmartDialPrefix.parseToNumberTokens(contactPhoneNumber);
+
+ for (String numberPrefix : numberPrefixes) {
+ numberInsert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
+ numberInsert.bindString(2, numberPrefix);
+ numberInsert.executeInsert();
+ numberInsert.clearBindings();
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Inserts prefixes of contact names to the prefix table.
+ *
+ * @param db Database pointer to the smartdial database.
+ * @param nameCursor Cursor pointing to the list of distinct updated contacts.
+ */
+ @VisibleForTesting
+ void insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor) {
+ final int columnIndexName = nameCursor.getColumnIndex(SmartDialDbColumns.DISPLAY_NAME_PRIMARY);
+ final int columnIndexContactId = nameCursor.getColumnIndex(SmartDialDbColumns.CONTACT_ID);
+
+ db.beginTransaction();
+ try {
+ final String sqlInsert =
+ "INSERT INTO "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ", "
+ + PrefixColumns.PREFIX
+ + ") "
+ + " VALUES (?, ?)";
+ final SQLiteStatement insert = db.compileStatement(sqlInsert);
+
+ while (nameCursor.moveToNext()) {
+ /** Computes a list of prefixes of a given contact name. */
+ final ArrayList<String> namePrefixes =
+ SmartDialPrefix.generateNamePrefixes(nameCursor.getString(columnIndexName));
+
+ for (String namePrefix : namePrefixes) {
+ insert.bindLong(1, nameCursor.getLong(columnIndexContactId));
+ insert.bindString(2, namePrefix);
+ insert.executeInsert();
+ insert.clearBindings();
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Updates the smart dial and prefix database. This method queries the Delta API to get changed
+ * contacts since last update, and updates the records in smartdial database and prefix database
+ * accordingly. It also queries the deleted contact database to remove newly deleted contacts
+ * since last update.
+ */
+ public void updateSmartDialDatabase() {
+ final SQLiteDatabase db = getWritableDatabase();
+
+ synchronized (mLock) {
+ LogUtil.v("DialerDatabaseHelper.updateSmartDialDatabase", "starting to update database");
+ final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;
+
+ /** Gets the last update time on the database. */
+ final SharedPreferences databaseLastUpdateSharedPref =
+ mContext.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
+ final String lastUpdateMillis =
+ String.valueOf(databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));
+
+ LogUtil.v(
+ "DialerDatabaseHelper.updateSmartDialDatabase", "last updated at " + lastUpdateMillis);
+
+ /** Sets the time after querying the database as the current update time. */
+ final Long currentMillis = System.currentTimeMillis();
+
+ if (DEBUG) {
+ stopWatch.lap("Queried the Contacts database");
+ }
+
+ /** Prevents the app from reading the dialer database when updating. */
+ mInUpdate.getAndSet(true);
+
+ /** Removes contacts that have been deleted. */
+ removeDeletedContacts(db, getDeletedContactCursor(lastUpdateMillis));
+ removePotentiallyCorruptedContacts(db, lastUpdateMillis);
+
+ if (DEBUG) {
+ stopWatch.lap("Finished deleting deleted entries");
+ }
+
+ /**
+ * If the database did not exist before, jump through deletion as there is nothing to delete.
+ */
+ if (!lastUpdateMillis.equals("0")) {
+ /**
+ * Removes contacts that have been updated. Updated contact information will be inserted
+ * later. Note that this has to use a separate result set from updatePhoneCursor, since it
+ * is possible for a contact to be updated (e.g. phone number deleted), but have no results
+ * show up in updatedPhoneCursor (since all of its phone numbers have been deleted).
+ */
+ final Cursor updatedContactCursor =
+ mContext
+ .getContentResolver()
+ .query(
+ UpdatedContactQuery.URI,
+ UpdatedContactQuery.PROJECTION,
+ UpdatedContactQuery.SELECT_UPDATED_CLAUSE,
+ new String[] {lastUpdateMillis},
+ null);
+ if (updatedContactCursor == null) {
+ LogUtil.e(
+ "DialerDatabaseHelper.updateSmartDialDatabase",
+ "smartDial query received null for cursor");
+ return;
+ }
+ try {
+ removeUpdatedContacts(db, updatedContactCursor);
+ } finally {
+ updatedContactCursor.close();
+ }
+ if (DEBUG) {
+ stopWatch.lap("Finished deleting entries belonging to updated contacts");
+ }
+ }
+
+ /**
+ * Queries the contact database to get all phone numbers that have been updated since the last
+ * update time.
+ */
+ final Cursor updatedPhoneCursor =
+ mContext
+ .getContentResolver()
+ .query(
+ PhoneQuery.URI,
+ PhoneQuery.PROJECTION,
+ PhoneQuery.SELECTION,
+ new String[] {lastUpdateMillis},
+ null);
+ if (updatedPhoneCursor == null) {
+ LogUtil.e(
+ "DialerDatabaseHelper.updateSmartDialDatabase",
+ "smartDial query received null for cursor");
+ return;
+ }
+
+ try {
+ /** Inserts recently updated phone numbers to the smartdial database. */
+ insertUpdatedContactsAndNumberPrefix(db, updatedPhoneCursor, currentMillis);
+ if (DEBUG) {
+ stopWatch.lap("Finished building the smart dial table");
+ }
+ } finally {
+ updatedPhoneCursor.close();
+ }
+
+ /**
+ * Gets a list of distinct contacts which have been updated, and adds the name prefixes of
+ * these contacts to the prefix table.
+ */
+ final Cursor nameCursor =
+ db.rawQuery(
+ "SELECT DISTINCT "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " = "
+ + Long.toString(currentMillis),
+ new String[] {});
+ if (nameCursor != null) {
+ try {
+ if (DEBUG) {
+ stopWatch.lap("Queried the smart dial table for contact names");
+ }
+
+ /** Inserts prefixes of names into the prefix table. */
+ insertNamePrefixes(db, nameCursor);
+ if (DEBUG) {
+ stopWatch.lap("Finished building the name prefix table");
+ }
+ } finally {
+ nameCursor.close();
+ }
+ }
+
+ /** Creates index on contact_id for fast JOIN operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.CONTACT_ID
+ + ");");
+ /** Creates index on last_smartdial_update_time for fast SELECT operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + ");");
+ /** Creates index on sorting fields for fast sort operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_sort_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.STARRED
+ + ", "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + ", "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + ", "
+ + SmartDialDbColumns.TIMES_USED
+ + ", "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.IS_PRIMARY
+ + ");");
+ /** Creates index on prefix for fast SELECT operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS nameprefix_index ON "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.PREFIX
+ + ");");
+ /** Creates index on contact_id for fast JOIN operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ");");
+
+ if (DEBUG) {
+ stopWatch.lap(TAG + "Finished recreating index");
+ }
+
+ /** Updates the database index statistics. */
+ db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);
+ db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);
+ db.execSQL("ANALYZE smartdial_contact_id_index");
+ db.execSQL("ANALYZE smartdial_last_update_index");
+ db.execSQL("ANALYZE nameprefix_index");
+ db.execSQL("ANALYZE nameprefix_contact_id_index");
+ if (DEBUG) {
+ stopWatch.stopAndLog(TAG + "Finished updating index stats", 0);
+ }
+
+ mInUpdate.getAndSet(false);
+
+ final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
+ editor.putLong(LAST_UPDATED_MILLIS, currentMillis);
+ editor.apply();
+
+ // Notify content observers that smart dial database has been updated.
+ mContext.getContentResolver().notifyChange(SMART_DIAL_UPDATED_URI, null, false);
+ }
+ }
+
+ /**
+ * Returns a list of candidate contacts where the query is a prefix of the dialpad index of the
+ * contact's name or phone number.
+ *
+ * @param query The prefix of a contact's dialpad index.
+ * @return A list of top candidate contacts that will be suggested to user to match their input.
+ */
+ public ArrayList<ContactNumber> getLooseMatches(String query, SmartDialNameMatcher nameMatcher) {
+ final boolean inUpdate = mInUpdate.get();
+ if (inUpdate) {
+ return new ArrayList<>();
+ }
+
+ final SQLiteDatabase db = getReadableDatabase();
+
+ /** Uses SQL query wildcard '%' to represent prefix matching. */
+ final String looseQuery = query + "%";
+
+ final ArrayList<ContactNumber> result = new ArrayList<>();
+
+ final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;
+
+ final String currentTimeStamp = Long.toString(System.currentTimeMillis());
+
+ /** Queries the database to find contacts that have an index matching the query prefix. */
+ final Cursor cursor =
+ db.rawQuery(
+ "SELECT "
+ + SmartDialDbColumns.DATA_ID
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.PHOTO_ID
+ + ", "
+ + SmartDialDbColumns.NUMBER
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.LOOKUP_KEY
+ + ", "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.CONTACT_ID
+ + " IN "
+ + " (SELECT "
+ + PrefixColumns.CONTACT_ID
+ + " FROM "
+ + Tables.PREFIX_TABLE
+ + " WHERE "
+ + Tables.PREFIX_TABLE
+ + "."
+ + PrefixColumns.PREFIX
+ + " LIKE '"
+ + looseQuery
+ + "')"
+ + " ORDER BY "
+ + SmartDialSortingOrder.SORT_ORDER,
+ new String[] {currentTimeStamp});
+ if (cursor == null) {
+ return result;
+ }
+ try {
+ if (DEBUG) {
+ stopWatch.lap("Prefix query completed");
+ }
+
+ /** Gets the column ID from the cursor. */
+ final int columnDataId = 0;
+ final int columnDisplayNamePrimary = 1;
+ final int columnPhotoId = 2;
+ final int columnNumber = 3;
+ final int columnId = 4;
+ final int columnLookupKey = 5;
+ final int columnCarrierPresence = 6;
+ if (DEBUG) {
+ stopWatch.lap("Found column IDs");
+ }
+
+ final Set<ContactMatch> duplicates = new HashSet<>();
+ int counter = 0;
+ if (DEBUG) {
+ stopWatch.lap("Moved cursor to start");
+ }
+ /** Iterates the cursor to find top contact suggestions without duplication. */
+ while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {
+ final long dataID = cursor.getLong(columnDataId);
+ final String displayName = cursor.getString(columnDisplayNamePrimary);
+ final String phoneNumber = cursor.getString(columnNumber);
+ final long id = cursor.getLong(columnId);
+ final long photoId = cursor.getLong(columnPhotoId);
+ final String lookupKey = cursor.getString(columnLookupKey);
+ final int carrierPresence = cursor.getInt(columnCarrierPresence);
+
+ /**
+ * If a contact already exists and another phone number of the contact is being processed,
+ * skip the second instance.
+ */
+ final ContactMatch contactMatch = new ContactMatch(lookupKey, id);
+ if (duplicates.contains(contactMatch)) {
+ continue;
+ }
+
+ /**
+ * If the contact has either the name or number that matches the query, add to the result.
+ */
+ final boolean nameMatches = nameMatcher.matches(displayName);
+ final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null);
+ if (nameMatches || numberMatches) {
+ /** If a contact has not been added, add it to the result and the hash set. */
+ duplicates.add(contactMatch);
+ result.add(
+ new ContactNumber(
+ id, dataID, displayName, phoneNumber, lookupKey, photoId, carrierPresence));
+ counter++;
+ if (DEBUG) {
+ stopWatch.lap("Added one result: Name: " + displayName);
+ }
+ }
+ }
+
+ if (DEBUG) {
+ stopWatch.stopAndLog(TAG + "Finished loading cursor", 0);
+ }
+ } finally {
+ cursor.close();
+ }
+ return result;
+ }
+
+ public interface Tables {
+
+ /** Saves a list of numbers to be blocked. */
+ String FILTERED_NUMBER_TABLE = "filtered_numbers_table";
+ /** Saves the necessary smart dial information of all contacts. */
+ String SMARTDIAL_TABLE = "smartdial_table";
+ /** Saves all possible prefixes to refer to a contacts. */
+ String PREFIX_TABLE = "prefix_table";
+ /** Saves all archived voicemail information. */
+ String VOICEMAIL_ARCHIVE_TABLE = "voicemail_archive_table";
+ /** Database properties for internal use */
+ String PROPERTIES = "properties";
+ }
+
+ public interface SmartDialDbColumns {
+
+ String _ID = "id";
+ String DATA_ID = "data_id";
+ String NUMBER = "phone_number";
+ String CONTACT_ID = "contact_id";
+ String LOOKUP_KEY = "lookup_key";
+ String DISPLAY_NAME_PRIMARY = "display_name";
+ String PHOTO_ID = "photo_id";
+ String LAST_TIME_USED = "last_time_used";
+ String TIMES_USED = "times_used";
+ String STARRED = "starred";
+ String IS_SUPER_PRIMARY = "is_super_primary";
+ String IN_VISIBLE_GROUP = "in_visible_group";
+ String IS_PRIMARY = "is_primary";
+ String CARRIER_PRESENCE = "carrier_presence";
+ String LAST_SMARTDIAL_UPDATE_TIME = "last_smartdial_update_time";
+ }
+
+ public interface PrefixColumns extends BaseColumns {
+
+ String PREFIX = "prefix";
+ String CONTACT_ID = "contact_id";
+ }
+
+ public interface PropertiesColumns {
+
+ String PROPERTY_KEY = "property_key";
+ String PROPERTY_VALUE = "property_value";
+ }
+
+ /** Query options for querying the contact database. */
+ public interface PhoneQuery {
+
+ Uri URI =
+ Phone.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
+ .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true")
+ .build();
+
+ String[] PROJECTION =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.DISPLAY_NAME_PRIMARY, // 6
+ Phone.PHOTO_ID, // 7
+ Data.LAST_TIME_USED, // 8
+ Data.TIMES_USED, // 9
+ Contacts.STARRED, // 10
+ Data.IS_SUPER_PRIMARY, // 11
+ Contacts.IN_VISIBLE_GROUP, // 12
+ Data.IS_PRIMARY, // 13
+ Data.CARRIER_PRESENCE, // 14
+ };
+
+ int PHONE_ID = 0;
+ int PHONE_TYPE = 1;
+ int PHONE_LABEL = 2;
+ int PHONE_NUMBER = 3;
+ int PHONE_CONTACT_ID = 4;
+ int PHONE_LOOKUP_KEY = 5;
+ int PHONE_DISPLAY_NAME = 6;
+ int PHONE_PHOTO_ID = 7;
+ int PHONE_LAST_TIME_USED = 8;
+ int PHONE_TIMES_USED = 9;
+ int PHONE_STARRED = 10;
+ int PHONE_IS_SUPER_PRIMARY = 11;
+ int PHONE_IN_VISIBLE_GROUP = 12;
+ int PHONE_IS_PRIMARY = 13;
+ int PHONE_CARRIER_PRESENCE = 14;
+
+ /** Selects only rows that have been updated after a certain time stamp. */
+ String SELECT_UPDATED_CLAUSE = Phone.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
+
+ /**
+ * Ignores contacts that have an unreasonably long lookup key. These are likely to be the result
+ * of multiple (> 50) merged raw contacts, and are likely to cause OutOfMemoryExceptions within
+ * SQLite, or cause memory allocation problems later on when iterating through the cursor set
+ * (see b/13133579)
+ */
+ String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE = "length(" + Phone.LOOKUP_KEY + ") < 1000";
+
+ String SELECTION = SELECT_UPDATED_CLAUSE + " AND " + SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE;
+ }
+
+ /**
+ * Query for all contacts that have been updated since the last time the smart dial database was
+ * updated.
+ */
+ public interface UpdatedContactQuery {
+
+ Uri URI = ContactsContract.Contacts.CONTENT_URI;
+
+ String[] PROJECTION =
+ new String[] {
+ ContactsContract.Contacts._ID // 0
+ };
+
+ int UPDATED_CONTACT_ID = 0;
+
+ String SELECT_UPDATED_CLAUSE =
+ ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
+ }
+
+ /** Query options for querying the deleted contact database. */
+ public interface DeleteContactQuery {
+
+ Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
+
+ String[] PROJECTION =
+ new String[] {
+ ContactsContract.DeletedContacts.CONTACT_ID, // 0
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, // 1
+ };
+
+ int DELETED_CONTACT_ID = 0;
+ int DELETED_TIMESTAMP = 1;
+
+ /** Selects only rows that have been deleted after a certain time stamp. */
+ String SELECT_UPDATED_CLAUSE =
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?";
+ }
+
+ /**
+ * Gets the sorting order for the smartdial table. This computes a SQL "ORDER BY" argument by
+ * composing contact status and recent contact details together.
+ */
+ private interface SmartDialSortingOrder {
+
+ /** Current contacts - those contacted within the last 3 days (in milliseconds) */
+ long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000;
+ /** Recent contacts - those contacted within the last 30 days (in milliseconds) */
+ long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000;
+
+ /** Time since last contact. */
+ String TIME_SINCE_LAST_USED_MS =
+ "( ?1 - " + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.LAST_TIME_USED + ")";
+
+ /**
+ * Contacts that have been used in the past 3 days rank higher than contacts that have been used
+ * in the past 30 days, which rank higher than contacts that have not been used in recent 30
+ * days.
+ */
+ String SORT_BY_DATA_USAGE =
+ "(CASE WHEN "
+ + TIME_SINCE_LAST_USED_MS
+ + " < "
+ + LAST_TIME_USED_CURRENT_MS
+ + " THEN 0 "
+ + " WHEN "
+ + TIME_SINCE_LAST_USED_MS
+ + " < "
+ + LAST_TIME_USED_RECENT_MS
+ + " THEN 1 "
+ + " ELSE 2 END)";
+
+ /**
+ * This sort order is similar to that used by the ContactsProvider when returning a list of
+ * frequently called contacts.
+ */
+ String SORT_ORDER =
+ Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.STARRED
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + " DESC, "
+ + SORT_BY_DATA_USAGE
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.TIMES_USED
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IS_PRIMARY
+ + " DESC";
+ }
+
+ /**
+ * Simple data format for a contact, containing only information needed for showing up in smart
+ * dial interface.
+ */
+ public static class ContactNumber {
+
+ public final long id;
+ public final long dataId;
+ public final String displayName;
+ public final String phoneNumber;
+ public final String lookupKey;
+ public final long photoId;
+ public final int carrierPresence;
+
+ public ContactNumber(
+ long id,
+ long dataID,
+ String displayName,
+ String phoneNumber,
+ String lookupKey,
+ long photoId,
+ int carrierPresence) {
+ this.dataId = dataID;
+ this.id = id;
+ this.displayName = displayName;
+ this.phoneNumber = phoneNumber;
+ this.lookupKey = lookupKey;
+ this.photoId = photoId;
+ this.carrierPresence = carrierPresence;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ id, dataId, displayName, phoneNumber, lookupKey, photoId, carrierPresence);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof ContactNumber) {
+ final ContactNumber that = (ContactNumber) object;
+ return Objects.equals(this.id, that.id)
+ && Objects.equals(this.dataId, that.dataId)
+ && Objects.equals(this.displayName, that.displayName)
+ && Objects.equals(this.phoneNumber, that.phoneNumber)
+ && Objects.equals(this.lookupKey, that.lookupKey)
+ && Objects.equals(this.photoId, that.photoId)
+ && Objects.equals(this.carrierPresence, that.carrierPresence);
+ }
+ return false;
+ }
+ }
+
+ /** Data format for finding duplicated contacts. */
+ private static class ContactMatch {
+
+ private final String lookupKey;
+ private final long id;
+
+ public ContactMatch(String lookupKey, long id) {
+ this.lookupKey = lookupKey;
+ this.id = id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lookupKey, id);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof ContactMatch) {
+ final ContactMatch that = (ContactMatch) object;
+ return Objects.equals(this.lookupKey, that.lookupKey) && Objects.equals(this.id, that.id);
+ }
+ return false;
+ }
+ }
+
+ private class SmartDialUpdateAsyncTask extends AsyncTask<Object, Object, Object> {
+
+ @Override
+ protected Object doInBackground(Object... objects) {
+ updateSmartDialDatabase();
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/dialer/database/FilteredNumberContract.java b/java/com/android/dialer/database/FilteredNumberContract.java
new file mode 100644
index 000000000..3efbaafb1
--- /dev/null
+++ b/java/com/android/dialer/database/FilteredNumberContract.java
@@ -0,0 +1,137 @@
+/*
+ * 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.database;
+
+import android.net.Uri;
+import android.provider.BaseColumns;
+import com.android.dialer.constants.Constants;
+
+/**
+ * The contract between the filtered number provider and applications. Contains definitions for the
+ * supported URIs and columns. Currently only accessible within Dialer.
+ */
+public final class FilteredNumberContract {
+
+ public static final String AUTHORITY = Constants.get().getFilteredNumberProviderAuthority();
+
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+ /** The type of filtering to be applied, e.g. block the number or whitelist the number. */
+ public interface FilteredNumberTypes {
+
+ int UNDEFINED = 0;
+ /** Dialer will disconnect the call without sending the caller to voicemail. */
+ int BLOCKED_NUMBER = 1;
+ }
+
+ /** The original source of the filtered number, e.g. the user manually added it. */
+ public interface FilteredNumberSources {
+
+ int UNDEFINED = 0;
+ /** The user manually added this number through Dialer (e.g. from the call log or InCallUI). */
+ int USER = 1;
+ }
+
+ public interface FilteredNumberColumns {
+
+ // TYPE: INTEGER
+ String _ID = "_id";
+ /**
+ * Represents the number to be filtered, normalized to compare phone numbers for equality.
+ *
+ * <p>TYPE: TEXT
+ */
+ String NORMALIZED_NUMBER = "normalized_number";
+ /**
+ * Represents the number to be filtered, for formatting and used with country iso for contact
+ * lookups.
+ *
+ * <p>TYPE: TEXT
+ */
+ String NUMBER = "number";
+ /**
+ * The country code representing the country detected when the phone number was added to the
+ * database. Most numbers don't have the country code, so a best guess is provided by the
+ * country detector system. The country iso is also needed in order to format phone numbers
+ * correctly.
+ *
+ * <p>TYPE: TEXT
+ */
+ String COUNTRY_ISO = "country_iso";
+ /**
+ * The number of times the number has been filtered by Dialer. When this number is incremented,
+ * LAST_TIME_FILTERED should also be updated to the current time.
+ *
+ * <p>TYPE: INTEGER
+ */
+ String TIMES_FILTERED = "times_filtered";
+ /**
+ * Set to the current time when the phone number is filtered. When this is updated,
+ * TIMES_FILTERED should also be incremented.
+ *
+ * <p>TYPE: LONG
+ */
+ String LAST_TIME_FILTERED = "last_time_filtered";
+ // TYPE: LONG
+ String CREATION_TIME = "creation_time";
+ /**
+ * Indicates the type of filtering to be applied.
+ *
+ * <p>TYPE: INTEGER See {@link FilteredNumberTypes}
+ */
+ String TYPE = "type";
+ /**
+ * Integer representing the original source of the filtered number.
+ *
+ * <p>TYPE: INTEGER See {@link FilteredNumberSources}
+ */
+ String SOURCE = "source";
+ }
+
+ /**
+ * Constants for the table of filtered numbers.
+ *
+ * <h3>Operations</h3>
+ *
+ * <dl>
+ * <dt><b>Insert</b>
+ * <dd>Required fields: NUMBER, NORMALIZED_NUMBER, TYPE, SOURCE. A default value will be used for
+ * the other fields if left null.
+ * <dt><b>Update</b>
+ * <dt><b>Delete</b>
+ * <dt><b>Query</b>
+ * <dd>{@link #CONTENT_URI} can be used for any query, append an ID to retrieve a specific
+ * filtered number entry.
+ * </dl>
+ */
+ public static class FilteredNumber implements BaseColumns {
+
+ public static final String FILTERED_NUMBERS_TABLE = "filtered_numbers_table";
+
+ /**
+ * The MIME type of a {@link android.content.ContentProvider#getType(Uri)} single filtered
+ * number.
+ */
+ public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/filtered_numbers_table";
+
+ public static final Uri CONTENT_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, FILTERED_NUMBERS_TABLE);
+
+ /** This utility class cannot be instantiated. */
+ private FilteredNumber() {}
+ }
+}
diff --git a/java/com/android/dialer/database/VoicemailStatusQuery.java b/java/com/android/dialer/database/VoicemailStatusQuery.java
new file mode 100644
index 000000000..d9e1b721b
--- /dev/null
+++ b/java/com/android/dialer/database/VoicemailStatusQuery.java
@@ -0,0 +1,91 @@
+/*
+ * 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.database;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** The query for the call voicemail status table. */
+public class VoicemailStatusQuery {
+
+ // TODO: Column indices should be removed in favor of Cursor#getColumnIndex
+ public static final int SOURCE_PACKAGE_INDEX = 0;
+ public static final int SETTINGS_URI_INDEX = 1;
+ public static final int VOICEMAIL_ACCESS_URI_INDEX = 2;
+ public static final int CONFIGURATION_STATE_INDEX = 3;
+ public static final int DATA_CHANNEL_STATE_INDEX = 4;
+ public static final int NOTIFICATION_CHANNEL_STATE_INDEX = 5;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int QUOTA_OCCUPIED_INDEX = 6;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int QUOTA_TOTAL_INDEX = 7;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ // The PHONE_ACCOUNT columns were added in M, but aren't queryable until N MR1
+ public static final int PHONE_ACCOUNT_COMPONENT_NAME = 8;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ public static final int PHONE_ACCOUNT_ID = 9;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ public static final int SOURCE_TYPE_INDEX = 10;
+
+ private static final String[] PROJECTION_M =
+ new String[] {
+ Status.SOURCE_PACKAGE, // 0
+ Status.SETTINGS_URI, // 1
+ Status.VOICEMAIL_ACCESS_URI, // 2
+ Status.CONFIGURATION_STATE, // 3
+ Status.DATA_CHANNEL_STATE, // 4
+ Status.NOTIFICATION_CHANNEL_STATE // 5
+ };
+
+ @RequiresApi(VERSION_CODES.N)
+ private static final String[] PROJECTION_N;
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ private static final String[] PROJECTION_NMR1;
+
+ static {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(PROJECTION_M));
+ projectionList.add(Status.QUOTA_OCCUPIED); // 6
+ projectionList.add(Status.QUOTA_TOTAL); // 7
+ PROJECTION_N = projectionList.toArray(new String[projectionList.size()]);
+
+ projectionList.add(Status.PHONE_ACCOUNT_COMPONENT_NAME); // 8
+ projectionList.add(Status.PHONE_ACCOUNT_ID); // 9
+ projectionList.add(Status.SOURCE_TYPE); // 10
+ PROJECTION_NMR1 = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ public static String[] getProjection() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ return PROJECTION_NMR1;
+ }
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PROJECTION_N;
+ }
+ return PROJECTION_M;
+ }
+}
diff --git a/java/com/android/dialer/debug/AndroidManifest.xml b/java/com/android/dialer/debug/AndroidManifest.xml
new file mode 100644
index 000000000..053d7e789
--- /dev/null
+++ b/java/com/android/dialer/debug/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.debug">
+</manifest>
diff --git a/java/com/android/dialer/debug/bindings/impl/DebugBindings.java b/java/com/android/dialer/debug/bindings/impl/DebugBindings.java
new file mode 100644
index 000000000..a8b44605c
--- /dev/null
+++ b/java/com/android/dialer/debug/bindings/impl/DebugBindings.java
@@ -0,0 +1,32 @@
+/*
+ * 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.debug.bindings;
+
+import android.content.Context;
+import com.android.dialer.debug.impl.DebugConnectionService;
+
+/** Hooks into the debug module. */
+public class DebugBindings {
+
+ public static void registerConnectionService(Context context) {
+ DebugConnectionService.register(context);
+ }
+
+ public static void addNewIncomingCall(Context context, String phoneNumber) {
+ DebugConnectionService.addNewIncomingCall(context, phoneNumber);
+ }
+}
diff --git a/java/com/android/dialer/debug/impl/AndroidManifest.xml b/java/com/android/dialer/debug/impl/AndroidManifest.xml
new file mode 100644
index 000000000..b8756614b
--- /dev/null
+++ b/java/com/android/dialer/debug/impl/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.debug.impl">
+
+ <application>
+
+ <service
+ android:exported="true"
+ android:name=".DebugConnectionService"
+ android:permission="android.permission.BIND_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecomm.ConnectionService"/>
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/debug/impl/DebugConnection.java b/java/com/android/dialer/debug/impl/DebugConnection.java
new file mode 100644
index 000000000..2ef83aa76
--- /dev/null
+++ b/java/com/android/dialer/debug/impl/DebugConnection.java
@@ -0,0 +1,55 @@
+/*
+ * 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.debug.impl;
+
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import com.android.dialer.common.LogUtil;
+
+class DebugConnection extends Connection {
+
+ @Override
+ public void onAnswer() {
+ LogUtil.i("DebugConnection.onAnswer", null);
+ setActive();
+ }
+
+ @Override
+ public void onReject() {
+ LogUtil.i("DebugConnection.onReject", null);
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ }
+
+ @Override
+ public void onHold() {
+ LogUtil.i("DebugConnection.onHold", null);
+ setOnHold();
+ }
+
+ @Override
+ public void onUnhold() {
+ LogUtil.i("DebugConnection.onUnhold", null);
+ setActive();
+ }
+
+ @Override
+ public void onDisconnect() {
+ LogUtil.i("DebugConnection.onDisconnect", null);
+ setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+ destroy();
+ }
+}
diff --git a/java/com/android/dialer/debug/impl/DebugConnectionService.java b/java/com/android/dialer/debug/impl/DebugConnectionService.java
new file mode 100644
index 000000000..69aab1e13
--- /dev/null
+++ b/java/com/android/dialer/debug/impl/DebugConnectionService.java
@@ -0,0 +1,103 @@
+/*
+ * 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.debug.impl;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Simple connection provider to create an incoming call. This is useful for emulators. */
+public class DebugConnectionService extends ConnectionService {
+
+ private static final String PHONE_ACCOUNT_ID = "DEBUG_DIALER";
+
+ public static void register(Context context) {
+ LogUtil.i(
+ "DebugConnectionService.register",
+ context.getSystemService(Context.TELECOM_SERVICE).toString());
+ context.getSystemService(TelecomManager.class).registerPhoneAccount(buildPhoneAccount(context));
+ }
+
+ public static void addNewIncomingCall(Context context, String phoneNumber) {
+ LogUtil.i("DebugConnectionService.addNewIncomingCall", null);
+ Bundle bundle = new Bundle();
+ bundle.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, phoneNumber);
+ try {
+ context
+ .getSystemService(TelecomManager.class)
+ .addNewIncomingCall(getConnectionServiceHandle(context), bundle);
+ } catch (SecurityException e) {
+ LogUtil.i(
+ "DebugConnectionService.addNewIncomingCall",
+ "unable to add call. Make sure to enable the service in Phone app -> Settings -> Calls ->"
+ + " Calling accounts.");
+ }
+ }
+
+ private static PhoneAccount buildPhoneAccount(Context context) {
+ PhoneAccount.Builder builder =
+ new PhoneAccount.Builder(
+ getConnectionServiceHandle(context), "DebugDialerConnectionService");
+ List<String> uriSchemes = new ArrayList<>();
+ uriSchemes.add(PhoneAccount.SCHEME_TEL);
+
+ return builder
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setShortDescription("Debug Dialer Connection Serivce")
+ .setSupportedUriSchemes(uriSchemes)
+ .build();
+ }
+
+ private static PhoneAccountHandle getConnectionServiceHandle(Context context) {
+ ComponentName componentName = new ComponentName(context, DebugConnectionService.class);
+ return new PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID);
+ }
+
+ private static Uri getPhoneNumber(ConnectionRequest request) {
+ String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ return null;
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ LogUtil.i("DebugConnectionService.onCreateIncomingConnection", null);
+ DebugConnection connection = new DebugConnection();
+ connection.setRinging();
+ connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED);
+ connection.setConnectionCapabilities(
+ Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
+ return connection;
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/AndroidManifest.xml b/java/com/android/dialer/dialpadview/AndroidManifest.xml
new file mode 100644
index 000000000..011a004b0
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.dialpadview">
+</manifest>
diff --git a/java/com/android/dialer/dialpadview/DialpadKeyButton.java b/java/com/android/dialer/dialpadview/DialpadKeyButton.java
new file mode 100644
index 000000000..24ca9cc86
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadKeyButton.java
@@ -0,0 +1,231 @@
+/*
+ * 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.dialpadview;
+
+import android.content.Context;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.FrameLayout;
+
+/**
+ * Custom class for dialpad buttons.
+ *
+ * <p>When touch exploration mode is enabled for accessibility, this class implements the
+ * lift-to-type interaction model:
+ *
+ * <ul>
+ * <li>Hovering over the button will cause it to gain accessibility focus
+ * <li>Removing the hover pointer while inside the bounds of the button will perform a click action
+ * <li>If long-click is supported, hovering over the button for a longer period of time will switch
+ * to the long-click action
+ * <li>Moving the hover pointer outside of the bounds of the button will restore to the normal click
+ * action
+ * <ul>
+ */
+public class DialpadKeyButton extends FrameLayout {
+
+ /** Timeout before switching to long-click accessibility mode. */
+ private static final int LONG_HOVER_TIMEOUT = ViewConfiguration.getLongPressTimeout() * 2;
+
+ /** Accessibility manager instance used to check touch exploration state. */
+ private AccessibilityManager mAccessibilityManager;
+
+ /** Bounds used to filter HOVER_EXIT events. */
+ private RectF mHoverBounds = new RectF();
+
+ /** Whether this view is currently in the long-hover state. */
+ private boolean mLongHovered;
+
+ /** Alternate content description for long-hover state. */
+ private CharSequence mLongHoverContentDesc;
+
+ /** Backup of standard content description. Used for accessibility. */
+ private CharSequence mBackupContentDesc;
+
+ /** Backup of clickable property. Used for accessibility. */
+ private boolean mWasClickable;
+
+ /** Backup of long-clickable property. Used for accessibility. */
+ private boolean mWasLongClickable;
+
+ /** Runnable used to trigger long-click mode for accessibility. */
+ private Runnable mLongHoverRunnable;
+
+ private OnPressedListener mOnPressedListener;
+
+ public DialpadKeyButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initForAccessibility(context);
+ }
+
+ public DialpadKeyButton(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initForAccessibility(context);
+ }
+
+ public void setOnPressedListener(OnPressedListener onPressedListener) {
+ mOnPressedListener = onPressedListener;
+ }
+
+ private void initForAccessibility(Context context) {
+ mAccessibilityManager =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ }
+
+ public void setLongHoverContentDescription(CharSequence contentDescription) {
+ mLongHoverContentDesc = contentDescription;
+
+ if (mLongHovered) {
+ super.setContentDescription(mLongHoverContentDesc);
+ }
+ }
+
+ @Override
+ public void setContentDescription(CharSequence contentDescription) {
+ if (mLongHovered) {
+ mBackupContentDesc = contentDescription;
+ } else {
+ super.setContentDescription(contentDescription);
+ }
+ }
+
+ @Override
+ public void setPressed(boolean pressed) {
+ super.setPressed(pressed);
+ if (mOnPressedListener != null) {
+ mOnPressedListener.onPressed(this, pressed);
+ }
+ }
+
+ @Override
+ public void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ mHoverBounds.left = getPaddingLeft();
+ mHoverBounds.right = w - getPaddingRight();
+ mHoverBounds.top = getPaddingTop();
+ mHoverBounds.bottom = h - getPaddingBottom();
+ }
+
+ @Override
+ public boolean performAccessibilityAction(int action, Bundle arguments) {
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ simulateClickForAccessibility();
+ return true;
+ }
+
+ return super.performAccessibilityAction(action, arguments);
+ }
+
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ // When touch exploration is turned on, lifting a finger while inside
+ // the button's hover target bounds should perform a click action.
+ if (mAccessibilityManager.isEnabled() && mAccessibilityManager.isTouchExplorationEnabled()) {
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ // Lift-to-type temporarily disables double-tap activation.
+ mWasClickable = isClickable();
+ mWasLongClickable = isLongClickable();
+ if (mWasLongClickable && mLongHoverContentDesc != null) {
+ if (mLongHoverRunnable == null) {
+ mLongHoverRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ setLongHovered(true);
+ announceForAccessibility(mLongHoverContentDesc);
+ }
+ };
+ }
+ postDelayed(mLongHoverRunnable, LONG_HOVER_TIMEOUT);
+ }
+
+ setClickable(false);
+ setLongClickable(false);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ if (mHoverBounds.contains(event.getX(), event.getY())) {
+ if (mLongHovered) {
+ performLongClick();
+ } else {
+ simulateClickForAccessibility();
+ }
+ }
+
+ cancelLongHover();
+ setClickable(mWasClickable);
+ setLongClickable(mWasLongClickable);
+ break;
+ }
+ }
+
+ return super.onHoverEvent(event);
+ }
+
+ /**
+ * When accessibility is on, simulate press and release to preserve the semantic meaning of
+ * performClick(). Required for Braille support.
+ */
+ private void simulateClickForAccessibility() {
+ // Checking the press state prevents double activation.
+ if (isPressed()) {
+ return;
+ }
+
+ setPressed(true);
+
+ // Stay consistent with performClick() by sending the event after
+ // setting the pressed state but before performing the action.
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+
+ setPressed(false);
+ }
+
+ private void setLongHovered(boolean enabled) {
+ if (mLongHovered != enabled) {
+ mLongHovered = enabled;
+
+ // Switch between normal and alternate description, if available.
+ if (enabled) {
+ mBackupContentDesc = getContentDescription();
+ super.setContentDescription(mLongHoverContentDesc);
+ } else {
+ super.setContentDescription(mBackupContentDesc);
+ }
+ }
+ }
+
+ private void cancelLongHover() {
+ if (mLongHoverRunnable != null) {
+ removeCallbacks(mLongHoverRunnable);
+ }
+ setLongHovered(false);
+ }
+
+ public interface OnPressedListener {
+
+ void onPressed(View view, boolean pressed);
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/DialpadTextView.java b/java/com/android/dialer/dialpadview/DialpadTextView.java
new file mode 100644
index 000000000..5b1b7bb5d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadTextView.java
@@ -0,0 +1,71 @@
+/*
+ * 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.dialpadview;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.widget.TextView;
+
+/**
+ * This is a custom text view intended only for rendering the numerals (and star and pound) on the
+ * dialpad. TextView has built in top/bottom padding to help account for ascenders/descenders.
+ *
+ * <p>Since vertical space is at a premium on the dialpad, particularly if the font size is scaled
+ * to a larger default, for the dialpad we use this class to more precisely render characters
+ * according to the precise amount of space they need.
+ */
+public class DialpadTextView extends TextView {
+
+ private Rect mTextBounds = new Rect();
+ private String mTextStr;
+
+ public DialpadTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ /** Draw the text to fit within the height/width which have been specified during measurement. */
+ @Override
+ public void draw(Canvas canvas) {
+ Paint paint = getPaint();
+
+ // Without this, the draw does not respect the style's specified text color.
+ paint.setColor(getCurrentTextColor());
+
+ // The text bounds values are relative and can be negative,, so rather than specifying a
+ // standard origin such as 0, 0, we need to use negative of the left/top bounds.
+ // For example, the bounds may be: Left: 11, Right: 37, Top: -77, Bottom: 0
+ canvas.drawText(mTextStr, -mTextBounds.left, -mTextBounds.top, paint);
+ }
+
+ /**
+ * Calculate the pixel-accurate bounds of the text when rendered, and use that to specify the
+ * height and width.
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mTextStr = getText().toString();
+ getPaint().getTextBounds(mTextStr, 0, mTextStr.length(), mTextBounds);
+
+ int width = resolveSize(mTextBounds.width(), widthMeasureSpec);
+ int height = resolveSize(mTextBounds.height(), heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/DialpadView.java b/java/com/android/dialer/dialpadview/DialpadView.java
new file mode 100644
index 000000000..4a9b500b7
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DialpadView.java
@@ -0,0 +1,464 @@
+/*
+ * 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.dialpadview;
+
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.style.TtsSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.animation.AnimUtils;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/** View that displays a twelve-key phone dialpad. */
+public class DialpadView extends LinearLayout {
+
+ private static final String TAG = DialpadView.class.getSimpleName();
+
+ private static final double DELAY_MULTIPLIER = 0.66;
+ private static final double DURATION_MULTIPLIER = 0.8;
+ // For animation.
+ private static final int KEY_FRAME_DURATION = 33;
+ /** {@code True} if the dialpad is in landscape orientation. */
+ private final boolean mIsLandscape;
+ /** {@code True} if the dialpad is showing in a right-to-left locale. */
+ private final boolean mIsRtl;
+
+ private final int[] mButtonIds =
+ new int[] {
+ R.id.zero,
+ 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.pound
+ };
+ private EditText mDigits;
+ private ImageButton mDelete;
+ private View mOverflowMenuButton;
+ private ColorStateList mRippleColor;
+ private ViewGroup mRateContainer;
+ private TextView mIldCountry;
+ private TextView mIldRate;
+ private boolean mCanDigitsBeEdited;
+ private int mTranslateDistance;
+
+ public DialpadView(Context context) {
+ this(context, null);
+ }
+
+ public DialpadView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DialpadView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Dialpad);
+ mRippleColor = a.getColorStateList(R.styleable.Dialpad_dialpad_key_button_touch_tint);
+ a.recycle();
+
+ mTranslateDistance =
+ getResources().getDimensionPixelSize(R.dimen.dialpad_key_button_translate_y);
+
+ mIsLandscape =
+ getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ mIsRtl =
+ TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ setupKeypad();
+ mDigits = (EditText) findViewById(R.id.digits);
+ mDelete = (ImageButton) findViewById(R.id.deleteButton);
+ mOverflowMenuButton = findViewById(R.id.dialpad_overflow);
+ mRateContainer = (ViewGroup) findViewById(R.id.rate_container);
+ mIldCountry = (TextView) mRateContainer.findViewById(R.id.ild_country);
+ mIldRate = (TextView) mRateContainer.findViewById(R.id.ild_rate);
+
+ AccessibilityManager accessibilityManager =
+ (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (accessibilityManager.isEnabled()) {
+ // The text view must be selected to send accessibility events.
+ mDigits.setSelected(true);
+ }
+ }
+
+ private void setupKeypad() {
+ final int[] letterIds =
+ new int[] {
+ R.string.dialpad_0_letters,
+ R.string.dialpad_1_letters,
+ R.string.dialpad_2_letters,
+ R.string.dialpad_3_letters,
+ R.string.dialpad_4_letters,
+ R.string.dialpad_5_letters,
+ R.string.dialpad_6_letters,
+ R.string.dialpad_7_letters,
+ R.string.dialpad_8_letters,
+ R.string.dialpad_9_letters,
+ R.string.dialpad_star_letters,
+ R.string.dialpad_pound_letters
+ };
+
+ final Resources resources = getContext().getResources();
+
+ DialpadKeyButton dialpadKey;
+ TextView numberView;
+ TextView lettersView;
+
+ final Locale currentLocale = resources.getConfiguration().locale;
+ final NumberFormat nf;
+ // We translate dialpad numbers only for "fa" and not any other locale
+ // ("ar" anybody ?).
+ if ("fa".equals(currentLocale.getLanguage())) {
+ nf = DecimalFormat.getInstance(resources.getConfiguration().locale);
+ } else {
+ nf = DecimalFormat.getInstance(Locale.ENGLISH);
+ }
+
+ for (int i = 0; i < mButtonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
+ numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number);
+ lettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters);
+
+ final String numberString;
+ final CharSequence numberContentDescription;
+ if (mButtonIds[i] == R.id.pound) {
+ numberString = resources.getString(R.string.dialpad_pound_number);
+ numberContentDescription = numberString;
+ } else if (mButtonIds[i] == R.id.star) {
+ numberString = resources.getString(R.string.dialpad_star_number);
+ numberContentDescription = numberString;
+ } else {
+ numberString = nf.format(i);
+ // The content description is used for Talkback key presses. The number is
+ // separated by a "," to introduce a slight delay. Convert letters into a verbatim
+ // span so that they are read as letters instead of as one word.
+ String letters = resources.getString(letterIds[i]);
+ Spannable spannable =
+ Spannable.Factory.getInstance().newSpannable(numberString + "," + letters);
+ spannable.setSpan(
+ (new TtsSpan.VerbatimBuilder(letters)).build(),
+ numberString.length() + 1,
+ numberString.length() + 1 + letters.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ numberContentDescription = spannable;
+ }
+
+ final RippleDrawable rippleBackground =
+ (RippleDrawable) getDrawableCompat(getContext(), R.drawable.btn_dialpad_key);
+ if (mRippleColor != null) {
+ rippleBackground.setColor(mRippleColor);
+ }
+
+ numberView.setText(numberString);
+ numberView.setElegantTextHeight(false);
+ dialpadKey.setContentDescription(numberContentDescription);
+ dialpadKey.setBackground(rippleBackground);
+
+ if (lettersView != null) {
+ lettersView.setText(resources.getString(letterIds[i]));
+ }
+ }
+
+ final DialpadKeyButton one = (DialpadKeyButton) findViewById(R.id.one);
+ one.setLongHoverContentDescription(resources.getText(R.string.description_voicemail_button));
+
+ final DialpadKeyButton zero = (DialpadKeyButton) findViewById(R.id.zero);
+ zero.setLongHoverContentDescription(resources.getText(R.string.description_image_button_plus));
+ }
+
+ private Drawable getDrawableCompat(Context context, int id) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return context.getDrawable(id);
+ } else {
+ return context.getResources().getDrawable(id);
+ }
+ }
+
+ public void setShowVoicemailButton(boolean show) {
+ View view = findViewById(R.id.dialpad_key_voicemail);
+ if (view != null) {
+ view.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ /**
+ * Whether or not the digits above the dialer can be edited.
+ *
+ * @param canBeEdited If true, the backspace button will be shown and the digits EditText will be
+ * configured to allow text manipulation.
+ */
+ public void setCanDigitsBeEdited(boolean canBeEdited) {
+ View deleteButton = findViewById(R.id.deleteButton);
+ deleteButton.setVisibility(canBeEdited ? View.VISIBLE : View.INVISIBLE);
+ View overflowMenuButton = findViewById(R.id.dialpad_overflow);
+ overflowMenuButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE);
+
+ EditText digits = (EditText) findViewById(R.id.digits);
+ digits.setClickable(canBeEdited);
+ digits.setLongClickable(canBeEdited);
+ digits.setFocusableInTouchMode(canBeEdited);
+ digits.setCursorVisible(false);
+
+ mCanDigitsBeEdited = canBeEdited;
+ }
+
+ public void setCallRateInformation(String countryName, String displayRate) {
+ if (TextUtils.isEmpty(countryName) && TextUtils.isEmpty(displayRate)) {
+ mRateContainer.setVisibility(View.GONE);
+ return;
+ }
+ mRateContainer.setVisibility(View.VISIBLE);
+ mIldCountry.setText(countryName);
+ mIldRate.setText(displayRate);
+ }
+
+ public boolean canDigitsBeEdited() {
+ return mCanDigitsBeEdited;
+ }
+
+ /**
+ * Always returns true for onHoverEvent callbacks, to fix problems with accessibility due to the
+ * dialpad overlaying other fragments.
+ */
+ @Override
+ public boolean onHoverEvent(MotionEvent event) {
+ return true;
+ }
+
+ public void animateShow() {
+ // This is a hack; without this, the setTranslationY is delayed in being applied, and the
+ // numbers appear at their original position (0) momentarily before animating.
+ final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {};
+
+ for (int i = 0; i < mButtonIds.length; i++) {
+ int delay = (int) (getKeyButtonAnimationDelay(mButtonIds[i]) * DELAY_MULTIPLIER);
+ int duration = (int) (getKeyButtonAnimationDuration(mButtonIds[i]) * DURATION_MULTIPLIER);
+ final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
+
+ ViewPropertyAnimator animator = dialpadKey.animate();
+ if (mIsLandscape) {
+ // Landscape orientation requires translation along the X axis.
+ // For RTL locales, ensure we translate negative on the X axis.
+ dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance);
+ animator.translationX(0);
+ } else {
+ // Portrait orientation requires translation along the Y axis.
+ dialpadKey.setTranslationY(mTranslateDistance);
+ animator.translationY(0);
+ }
+ animator
+ .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
+ .setStartDelay(delay)
+ .setDuration(duration)
+ .setListener(showListener)
+ .start();
+ }
+ }
+
+ public EditText getDigits() {
+ return mDigits;
+ }
+
+ public ImageButton getDeleteButton() {
+ return mDelete;
+ }
+
+ public View getOverflowMenuButton() {
+ return mOverflowMenuButton;
+ }
+
+ /**
+ * Get the animation delay for the buttons, taking into account whether the dialpad is in
+ * landscape left-to-right, landscape right-to-left, or portrait.
+ *
+ * @param buttonId The button ID.
+ * @return The animation delay.
+ */
+ private int getKeyButtonAnimationDelay(int buttonId) {
+ if (mIsLandscape) {
+ if (mIsRtl) {
+ if (buttonId == R.id.three) {
+ return KEY_FRAME_DURATION * 1;
+ } else if (buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 2;
+ } else if (buttonId == R.id.nine) {
+ return KEY_FRAME_DURATION * 3;
+ } else if (buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 4;
+ } else if (buttonId == R.id.two) {
+ return KEY_FRAME_DURATION * 5;
+ } else if (buttonId == R.id.five) {
+ return KEY_FRAME_DURATION * 6;
+ } else if (buttonId == R.id.eight) {
+ return KEY_FRAME_DURATION * 7;
+ } else if (buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.one) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.four) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.seven || buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 11;
+ }
+ } else {
+ if (buttonId == R.id.one) {
+ return KEY_FRAME_DURATION * 1;
+ } else if (buttonId == R.id.four) {
+ return KEY_FRAME_DURATION * 2;
+ } else if (buttonId == R.id.seven) {
+ return KEY_FRAME_DURATION * 3;
+ } else if (buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 4;
+ } else if (buttonId == R.id.two) {
+ return KEY_FRAME_DURATION * 5;
+ } else if (buttonId == R.id.five) {
+ return KEY_FRAME_DURATION * 6;
+ } else if (buttonId == R.id.eight) {
+ return KEY_FRAME_DURATION * 7;
+ } else if (buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.three) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.nine || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 11;
+ }
+ }
+ } else {
+ if (buttonId == R.id.one) {
+ return KEY_FRAME_DURATION * 1;
+ } else if (buttonId == R.id.two) {
+ return KEY_FRAME_DURATION * 2;
+ } else if (buttonId == R.id.three) {
+ return KEY_FRAME_DURATION * 3;
+ } else if (buttonId == R.id.four) {
+ return KEY_FRAME_DURATION * 4;
+ } else if (buttonId == R.id.five) {
+ return KEY_FRAME_DURATION * 5;
+ } else if (buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 6;
+ } else if (buttonId == R.id.seven) {
+ return KEY_FRAME_DURATION * 7;
+ } else if (buttonId == R.id.eight) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.nine) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.zero || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 11;
+ }
+ }
+
+ Log.wtf(TAG, "Attempted to get animation delay for invalid key button id.");
+ return 0;
+ }
+
+ /**
+ * Get the button animation duration, taking into account whether the dialpad is in landscape
+ * left-to-right, landscape right-to-left, or portrait.
+ *
+ * @param buttonId The button ID.
+ * @return The animation duration.
+ */
+ private int getKeyButtonAnimationDuration(int buttonId) {
+ if (mIsLandscape) {
+ if (mIsRtl) {
+ if (buttonId == R.id.one
+ || buttonId == R.id.four
+ || buttonId == R.id.seven
+ || buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 8;
+ } else if (buttonId == R.id.two
+ || buttonId == R.id.five
+ || buttonId == R.id.eight
+ || buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.three
+ || buttonId == R.id.six
+ || buttonId == R.id.nine
+ || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 10;
+ }
+ } else {
+ if (buttonId == R.id.one
+ || buttonId == R.id.four
+ || buttonId == R.id.seven
+ || buttonId == R.id.star) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.two
+ || buttonId == R.id.five
+ || buttonId == R.id.eight
+ || buttonId == R.id.zero) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.three
+ || buttonId == R.id.six
+ || buttonId == R.id.nine
+ || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 8;
+ }
+ }
+ } else {
+ if (buttonId == R.id.one
+ || buttonId == R.id.two
+ || buttonId == R.id.three
+ || buttonId == R.id.four
+ || buttonId == R.id.five
+ || buttonId == R.id.six) {
+ return KEY_FRAME_DURATION * 10;
+ } else if (buttonId == R.id.seven || buttonId == R.id.eight || buttonId == R.id.nine) {
+ return KEY_FRAME_DURATION * 9;
+ } else if (buttonId == R.id.star || buttonId == R.id.zero || buttonId == R.id.pound) {
+ return KEY_FRAME_DURATION * 8;
+ }
+ }
+
+ Log.wtf(TAG, "Attempted to get animation duration for invalid key button id.");
+ return 0;
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/DigitsEditText.java b/java/com/android/dialer/dialpadview/DigitsEditText.java
new file mode 100644
index 000000000..4a4b9b4e2
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/DigitsEditText.java
@@ -0,0 +1,57 @@
+/*
+ * 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.dialpadview;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.InputType;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.inputmethod.InputMethodManager;
+import com.android.dialer.widget.ResizingTextEditText;
+
+/** EditText which suppresses IME show up. */
+public class DigitsEditText extends ResizingTextEditText {
+
+ public DigitsEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+ setShowSoftInputOnFocus(false);
+ }
+
+ @Override
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+ super.onFocusChanged(focused, direction, previouslyFocusedRect);
+ final InputMethodManager imm =
+ ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE));
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ final boolean ret = super.onTouchEvent(event);
+ // Must be done after super.onTouchEvent()
+ final InputMethodManager imm =
+ ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE));
+ if (imm != null && imm.isActive(this)) {
+ imm.hideSoftInputFromWindow(getApplicationWindowToken(), 0);
+ }
+ return ret;
+ }
+}
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml
new file mode 100644
index 000000000..4efa80d86
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_in_duration"
+ android:fromYDelta="100%p"
+ android:toYDelta="0"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml
new file mode 100644
index 000000000..4e5a2053c
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_in_duration"
+ android:fromXDelta="-100%p"
+ android:toXDelta="0"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml
new file mode 100644
index 000000000..5a6dfaa79
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2014, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_in_duration"
+ android:fromXDelta="100%p"
+ android:toXDelta="0"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml
new file mode 100644
index 000000000..01ac48247
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_out_duration"
+ android:fromYDelta="0"
+ android:toYDelta="100%p"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml
new file mode 100644
index 000000000..5ac1d290f
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_out_duration"
+ android:fromXDelta="0"
+ android:toXDelta="-100%"/>
diff --git a/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml
new file mode 100644
index 000000000..5f5690232
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (c) 2014, The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<translate xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@integer/dialpad_slide_out_duration"
+ android:fromXDelta="0"
+ android:toXDelta="100%"/>
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png
new file mode 100644
index 000000000..3380a899d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png
new file mode 100644
index 000000000..ff9753c18
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png
new file mode 100644
index 000000000..7bf83fa6a
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png
new file mode 100644
index 000000000..1a9cd75a0
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png
new file mode 100644
index 000000000..e588d90e9
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png
new file mode 100644
index 000000000..4706112d6
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png
new file mode 100644
index 000000000..262e9df91
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png
new file mode 100644
index 000000000..46630d430
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png
new file mode 100644
index 000000000..947aac142
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png
new file mode 100644
index 000000000..790f93590
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png
new file mode 100644
index 000000000..40a1a84e3
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png
new file mode 100644
index 000000000..64a52d030
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png
new file mode 100644
index 000000000..e84d8f4a8
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png
new file mode 100644
index 000000000..0e720ddbd
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png
new file mode 100644
index 000000000..5dafee092
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png
new file mode 100644
index 000000000..e8bab3fec
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png
new file mode 100644
index 000000000..6bd53f5c5
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png
new file mode 100644
index 000000000..6bc437298
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png
new file mode 100644
index 000000000..87bc11364
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png
new file mode 100644
index 000000000..0b4e18389
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png
new file mode 100644
index 000000000..915607633
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png
new file mode 100644
index 000000000..2b0dba7bc
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png
new file mode 100644
index 000000000..7e4fd3e49
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png
new file mode 100644
index 000000000..6866fa430
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png
new file mode 100644
index 000000000..51b4401ca
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png
new file mode 100644
index 000000000..186508a9f
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png
new file mode 100644
index 000000000..a0a7c9d28
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png
new file mode 100644
index 000000000..92526f5a6
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png
new file mode 100644
index 000000000..59d9b9506
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png
new file mode 100644
index 000000000..aa8849e86
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png
new file mode 100644
index 000000000..7af3396b4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png
new file mode 100644
index 000000000..df42feecb
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png
new file mode 100644
index 000000000..c974a8005
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png
new file mode 100644
index 000000000..c6e8be023
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png
new file mode 100644
index 000000000..9028bd437
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png
Binary files differ
diff --git a/java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml b/java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml
new file mode 100644
index 000000000..53cd7a85d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight"/> \ No newline at end of file
diff --git a/java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml b/java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml
new file mode 100644
index 000000000..ee0f40ab5
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:endColor="@android:color/darker_gray"
+ android:startColor="@android:color/transparent"/>
+</shape>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml
new file mode 100644
index 000000000..941fdb2ec
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- A layout representing a single key in the dialpad -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="right|center_vertical"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"
+ android:layout_marginBottom="0dp"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"
+ android:layout_gravity="right"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"
+ android:layout_gravity="right|center"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml
new file mode 100644
index 000000000..65a2308fc
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/one"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="right|center_vertical"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"
+ android:layout_marginBottom="0dp"
+ android:layout_marginRight="@dimen/dialpad_key_one_margin_right"
+ android:layout_gravity="right"/>
+ <FrameLayout
+ android:layout_width="@dimen/dialpad_key_text_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="left|center">
+ <ImageView
+ android:id="@+id/dialpad_key_voicemail"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_dialpad_voicemail"
+ android:tint="@color/dialpad_voicemail_tint"/>
+ </FrameLayout>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml
new file mode 100644
index 000000000..98c353128
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pound"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="center_vertical|right"
+ android:orientation="horizontal">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@id/dialpad_key_number"
+ style="@style/DialpadKeyPoundStyle"
+ android:layout_width="@dimen/dialpad_key_number_width"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"/>
+ <View
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml
new file mode 100644
index 000000000..b91c71680
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/star"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="center_vertical|right"
+ android:orientation="horizontal">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@id/dialpad_key_number"
+ style="@style/DialpadKeyStarStyle"
+ android:layout_width="@dimen/dialpad_key_number_width"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"/>
+ <View
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml
new file mode 100644
index 000000000..d885ddf05
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- A layout representing the zero key in the dialpad, with the plus sign shifted up because it is
+ smaller than a regular letter -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/zero"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle"
+ android:layout_gravity="right|center_vertical"
+ android:baselineAligned="false"
+ android:orientation="horizontal">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadBottomKeyNumberStyle"
+ android:layout_marginBottom="0dp"
+ android:layout_marginRight="@dimen/dialpad_key_margin_right"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"
+ android:layout_width="@dimen/dialpad_key_text_width"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad.xml
new file mode 100644
index 000000000..5a14d14ea
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Dialpad in the Phone app. -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dialpad"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:orientation="vertical">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include layout="@layout/dialpad_key_one"/>
+ <include
+ android:id="@+id/two"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/three"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include
+ android:id="@+id/four"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/five"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/six"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include
+ android:id="@+id/seven"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/eight"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <include
+ android:id="@+id/nine"
+ style="@style/DialpadKeyButtonStyle"
+ layout="@layout/dialpad_key"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="horizontal">
+ <Space style="@style/DialpadSpaceStyle"/>
+ <include layout="@layout/dialpad_key_star"/>
+ <include layout="@layout/dialpad_key_zero"/>
+ <include layout="@layout/dialpad_key_pound"/>
+ <Space style="@style/DialpadSpaceStyle"/>
+ </LinearLayout>
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="?attr/dialpad_end_key_spacing"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml
new file mode 100644
index 000000000..77e4fc53a
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- A layout representing a single key in the dialpad -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout style="@style/DialpadKeyInternalLayoutStyle">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml
new file mode 100644
index 000000000..36f62c85d
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/one"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyNumberStyle"/>
+ <RelativeLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <ImageView
+ android:id="@+id/dialpad_key_voicemail"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:paddingTop="@dimen/dialpad_voicemail_icon_padding_top"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_dialpad_voicemail"
+ android:tint="?attr/dialpad_voicemail_tint"/>
+ <!-- Place empty text view so vertical height is same as other dialpad keys. -->
+ <TextView style="@style/DialpadKeyLettersStyle"/>
+ </RelativeLayout>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml
new file mode 100644
index 000000000..d37a6aa78
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/pound"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@id/dialpad_key_number"
+ style="@style/DialpadKeyPoundStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml
new file mode 100644
index 000000000..d288475d0
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/star"
+ style="@style/DialpadKeyButtonStyle">
+ <LinearLayout
+ style="@style/DialpadKeyInternalLayoutStyle">
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadKeyStarStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml
new file mode 100644
index 000000000..943ae48dd
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- A layout representing the zero key in the dialpad, with the plus sign shifted up because it is
+ smaller than a regular letter -->
+<com.android.dialer.dialpadview.DialpadKeyButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/zero"
+ style="@style/DialpadKeyButtonStyle">
+
+ <LinearLayout style="@style/DialpadKeyInternalLayoutStyle">
+
+ <!-- Note in the referenced styles that we assign hard widths to these components
+ because we want them to line up vertically when we arrange them in an MxN grid -->
+
+ <com.android.dialer.dialpadview.DialpadTextView
+ android:id="@+id/dialpad_key_number"
+ style="@style/DialpadBottomKeyNumberStyle"/>
+
+ <TextView
+ android:id="@+id/dialpad_key_letters"
+ style="@style/DialpadKeyLettersStyle"/>
+ </LinearLayout>
+</com.android.dialer.dialpadview.DialpadKeyButton>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml
new file mode 100644
index 000000000..47112fbb1
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:theme="?attr/dialpad_style">
+ <include layout="@layout/dialpad_view_unthemed"/>
+</FrameLayout>
diff --git a/java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml b/java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml
new file mode 100644
index 000000000..4b08c6de7
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml
@@ -0,0 +1,153 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dialpad_view"
+ class="com.android.dialer.dialpadview.DialpadView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="bottom"
+ android:background="?attr/dialpad_background"
+ android:clickable="true"
+ android:layoutDirection="ltr"
+ android:orientation="vertical">
+
+ <!-- Text field where call rate is displayed for ILD calls. -->
+ <LinearLayout
+ android:id="@+id/rate_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:visibility="gone">
+
+ <LinearLayout
+ android:id="@+id/ild_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/ild_margin_height"
+ android:layout_marginBottom="@dimen/ild_margin_height"
+ android:layout_gravity="center_horizontal"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/ild_country"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <TextView
+ android:id="@+id/ild_rate"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="4dp"/>
+
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#e3e3e3"/>
+
+ </LinearLayout>
+
+ <!-- Text field and possibly soft menu button above the keypad where
+ the digits are displayed. -->
+ <LinearLayout
+ android:id="@+id/digits_container"
+ android:layout_width="match_parent"
+ android:layout_height="?attr/dialpad_digits_adjustable_height"
+ android:orientation="horizontal">
+
+ <ImageButton
+ android:id="@+id/dialpad_back"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/dialpad_overflow_margin"
+ android:paddingLeft="@dimen/dialpad_digits_menu_left_padding"
+ android:paddingRight="@dimen/dialpad_digits_menu_right_padding"
+ android:background="@drawable/btn_dialpad_key"
+ android:contentDescription="@string/description_dialpad_back"
+ android:gravity="center"
+ android:src="@drawable/ic_close_black_24dp"
+ android:tint="?attr/dialpad_icon_tint"
+ android:tintMode="src_in"
+ android:visibility="gone"/>
+
+ <ImageButton
+ android:id="@+id/dialpad_overflow"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_margin="@dimen/dialpad_overflow_margin"
+ android:paddingLeft="@dimen/dialpad_digits_menu_left_padding"
+ android:paddingRight="@dimen/dialpad_digits_menu_right_padding"
+ android:background="@drawable/btn_dialpad_key"
+ android:contentDescription="@string/description_dialpad_overflow"
+ android:gravity="center"
+ android:src="@drawable/ic_overflow_menu"
+ android:tint="?attr/dialpad_icon_tint"
+ android:tintMode="src_in"
+ android:visibility="gone"/>
+
+ <view xmlns:ex="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/digits"
+ class="com.android.dialer.dialpadview.DigitsEditText"
+ android:textStyle="normal"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:background="@android:color/transparent"
+ android:cursorVisible="false"
+ android:focusableInTouchMode="true"
+ android:fontFamily="sans-serif"
+ android:freezesText="true"
+ android:gravity="center"
+ android:maxLines="1"
+ android:scrollHorizontally="true"
+ android:singleLine="true"
+ android:textColor="?attr/dialpad_text_color"
+ android:textCursorDrawable="@null"
+ android:textSize="?attr/dialpad_digits_adjustable_text_size"
+ ex:resizing_text_min_size="@dimen/dialpad_digits_text_min_size"/>
+
+ <ImageButton
+ android:id="@+id/deleteButton"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:paddingLeft="@dimen/dialpad_digits_padding"
+ android:paddingRight="@dimen/dialpad_digits_padding"
+ android:background="@drawable/btn_dialpad_key"
+ android:contentDescription="@string/description_delete_button"
+ android:src="@drawable/ic_dialpad_delete"
+ android:state_enabled="false"
+ android:tint="?attr/dialpad_icon_tint"
+ android:tintMode="src_in"/>
+ </LinearLayout>
+
+ <View
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="#e3e3e3"/>
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialpad_space_above_keys"/>
+
+ <include layout="@layout/dialpad"/>
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/dialpad_space_below_keys"/>
+
+</view>
diff --git a/java/com/android/dialer/dialpadview/res/values-land/dimens.xml b/java/com/android/dialer/dialpadview/res/values-land/dimens.xml
new file mode 100644
index 000000000..617134ad4
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values-land/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<resources>
+ <dimen name="dialpad_key_margin_right">5dp</dimen>
+ <!-- Right margins for specific keys to align them correctly -->
+ <dimen name="dialpad_key_one_margin_right">3dp</dimen>
+ <dimen name="dialpad_key_text_width">35dp</dimen>
+ <dimen name="dialpad_key_number_width">20sp</dimen>
+ <dimen name="dialpad_symbol_margin_bottom">0dp</dimen>
+
+ <!-- The bottom space of the dialpad to account for the dial button -->
+ <dimen name="dialpad_bottom_space_height">65dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values-land/styles.xml b/java/com/android/dialer/dialpadview/res/values-land/styles.xml
new file mode 100644
index 000000000..f98372509
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values-land/styles.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <style name="DialpadKeyNumberStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_primary</item>
+ <item name="android:textSize">?attr/dialpad_key_numbers_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:layout_width">@dimen/dialpad_key_number_width</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">?attr/dialpad_key_number_margin_bottom</item>
+ </style>
+
+ <style name="DialpadKeyLettersStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_letters_size</item>
+ <item name="android:fontFamily">sans-serif-regular</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">left</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/animation_constants.xml b/java/com/android/dialer/dialpadview/res/values/animation_constants.xml
new file mode 100644
index 000000000..edd19d755
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/animation_constants.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="dialpad_slide_in_duration">400</integer>
+ <integer name="dialpad_slide_out_duration">400</integer>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/attrs.xml b/java/com/android/dialer/dialpadview/res/values/attrs.xml
new file mode 100644
index 000000000..273879f3e
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/attrs.xml
@@ -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.
+ -->
+
+<resources>
+
+ <attr format="reference" name="dialpad_style"/>
+ <attr format="dimension" name="dialpad_end_key_spacing"/>
+
+ <declare-styleable name="Dialpad">
+ <attr format="color" name="dialpad_key_button_touch_tint"/>
+ <attr format="dimension" name="dialpad_digits_adjustable_text_size"/>
+ <attr format="dimension" name="dialpad_digits_adjustable_height"/>
+ <attr format="dimension" name="dialpad_key_numbers_size"/>
+ <attr format="dimension" name="dialpad_key_number_margin_bottom"/>
+ <attr format="dimension" name="dialpad_zero_key_number_margin_bottom"/>
+ </declare-styleable>
+
+ <declare-styleable name="Theme.Dialpad">
+ <attr format="color" name="dialpad_text_color"/>
+ <attr format="color" name="dialpad_text_color_primary"/>
+ <attr format="color" name="dialpad_text_color_secondary"/>
+ <attr format="color" name="dialpad_icon_tint"/>
+ <attr format="color" name="dialpad_voicemail_tint"/>
+ <attr format="color" name="dialpad_background"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/colors.xml b/java/com/android/dialer/dialpadview/res/values/colors.xml
new file mode 100644
index 000000000..d27468db7
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/colors.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <!-- Colors for the dialpad -->
+ <color name="background_dialpad">#fcfcfc</color>
+ <color name="background_dialpad_pressed">#ececec</color>
+ <color name="dialpad_primary_text_color">@color/dialer_theme_color</color>
+ <color name="dialpad_secondary_text_color">#737373</color>
+ <color name="dialpad_digits_text_color">#333</color>
+ <color name="dialpad_separator_line_color">#dadada</color>
+ <color name="dialpad_icon_tint">#89000000</color>
+ <color name="dialpad_voicemail_tint">#919191</color>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/dimens.xml b/java/com/android/dialer/dialpadview/res/values/dimens.xml
new file mode 100644
index 000000000..210c81697
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/dimens.xml
@@ -0,0 +1,48 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <!-- Text dimensions for dialpad keys -->
+ <dimen name="dialpad_key_numbers_default_size">36sp</dimen>
+ <dimen name="dialpad_key_letters_size">12sp</dimen>
+ <dimen name="dialpad_key_pound_size">23sp</dimen>
+ <dimen name="dialpad_key_star_size">36sp</dimen>
+ <dimen name="dialpad_key_height">64dp</dimen>
+ <dimen name="dialpad_key_number_default_margin_bottom">3dp</dimen>
+ <!-- Zero key should have less space between self and text because "+" is smaller -->
+ <dimen name="dialpad_zero_key_number_default_margin_bottom">1dp</dimen>
+ <dimen name="dialpad_symbol_margin_bottom">13dp</dimen>
+ <dimen name="dialpad_key_plus_size">18sp</dimen>
+ <dimen name="dialpad_horizontal_padding">5dp</dimen>
+ <dimen name="dialpad_digits_text_size">34sp</dimen>
+ <dimen name="dialpad_digits_text_min_size">24sp</dimen>
+ <dimen name="dialpad_digits_height">60dp</dimen>
+ <dimen name="dialpad_digits_padding">16dp</dimen>
+ <dimen name="dialpad_digits_menu_left_padding">8dp</dimen>
+ <dimen name="dialpad_digits_menu_right_padding">10dp</dimen>
+ <dimen name="dialpad_center_margin">3dp</dimen>
+ <dimen name="dialpad_button_margin">2dp</dimen>
+ <dimen name="dialpad_voicemail_icon_padding_top">2dp</dimen>
+ <dimen name="dialpad_key_button_translate_y">100dp</dimen>
+ <dimen name="dialpad_overflow_margin">8dp</dimen>
+ <dimen name="dialpad_space_above_keys">14dp</dimen>
+ <dimen name="dialpad_space_below_keys">8dp</dimen>
+ <!-- The bottom space of the dialpad to account for the dial button -->
+ <dimen name="dialpad_bottom_space_height">80dp</dimen>
+
+ <!-- Top/Bottom padding around the ILD rate display box. -->
+ <dimen name="ild_margin_height">10dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/strings.xml b/java/com/android/dialer/dialpadview/res/values/strings.xml
new file mode 100644
index 000000000..920e6e25c
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/strings.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <string name="dialpad_star_number" translatable="false">*</string>
+ <string name="dialpad_pound_number" translatable="false">#</string>
+
+ <string name="dialpad_0_letters" translatable="false">+</string>
+ <string name="dialpad_1_letters" translatable="false"></string>
+ <string name="dialpad_2_letters" translatable="false">ABC</string>
+ <string name="dialpad_3_letters" translatable="false">DEF</string>
+ <string name="dialpad_4_letters" translatable="false">GHI</string>
+ <string name="dialpad_5_letters" translatable="false">JKL</string>
+ <string name="dialpad_6_letters" translatable="false">MNO</string>
+ <string name="dialpad_7_letters" translatable="false">PQRS</string>
+ <string name="dialpad_8_letters" translatable="false">TUV</string>
+ <string name="dialpad_9_letters" translatable="false">WXYZ</string>
+ <string name="dialpad_star_letters" translatable="false"></string>
+ <string name="dialpad_pound_letters" translatable="false"></string>
+
+ <!-- String describing the back button in the dialpad. -->
+ <string name="description_dialpad_back">Navigate back</string>
+
+ <!-- String describing the overflow menu button in the dialpad. -->
+ <string name="description_dialpad_overflow">More options</string>
+
+ <!-- String describing the Delete/Backspace ImageButton.
+ Used by AccessibilityService to announce the purpose of the button.
+ -->
+ <string name="description_delete_button">backspace</string>
+
+ <!-- String describing the button used to add a plus (+) symbol to the dialpad -->
+ <string name="description_image_button_plus">plus</string>
+
+ <!-- String describing the Voicemail ImageButton.
+ Used by AccessibilityService to announce the purpose of the button.
+ -->
+ <string name="description_voicemail_button">voicemail</string>
+
+</resources>
diff --git a/java/com/android/dialer/dialpadview/res/values/styles.xml b/java/com/android/dialer/dialpadview/res/values/styles.xml
new file mode 100644
index 000000000..2fa2c3f2e
--- /dev/null
+++ b/java/com/android/dialer/dialpadview/res/values/styles.xml
@@ -0,0 +1,118 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <style name="DialpadSpaceStyle">
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_weight">3</item>
+ </style>
+
+ <style name="DialpadKeyNumberStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_primary</item>
+ <item name="android:textSize">?attr/dialpad_key_numbers_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">?attr/dialpad_key_number_margin_bottom</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="DialpadBottomKeyNumberStyle" parent="DialpadKeyNumberStyle">
+ <item name="android:layout_marginBottom">?attr/dialpad_zero_key_number_margin_bottom</item>
+ </style>
+
+ <style name="DialpadKeyStarStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_star_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:alpha">0.8</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="DialpadKeyPoundStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_pound_size</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ <item name="android:alpha">0.8</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
+ <item name="android:gravity">center</item>
+ </style>
+
+ <style name="DialpadKeyLettersStyle">
+ <item name="android:textColor">?attr/dialpad_text_color_secondary</item>
+ <item name="android:textSize">@dimen/dialpad_key_letters_size</item>
+ <item name="android:fontFamily">sans-serif-regular</item>
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:gravity">center_horizontal</item>
+ </style>
+
+ <style name="DialpadKeyButtonStyle">
+ <item name="android:soundEffectsEnabled">false</item>
+ <item name="android:clickable">true</item>
+ <item name="android:layout_width">0dp</item>
+ <item name="android:layout_height">match_parent</item>
+ <item name="android:layout_weight">13</item>
+ <item name="android:minHeight">@dimen/dialpad_key_height</item>
+ <item name="android:background">@drawable/btn_dialpad_key</item>
+ <item name="android:focusable">true</item>
+ </style>
+
+ <style name="DialpadKeyInternalLayoutStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_gravity">center</item>
+ <item name="android:gravity">center</item>
+ <item name="android:orientation">vertical</item>
+ </style>
+
+ <style name="Dialpad">
+ <item name="dialpad_digits_adjustable_height">@dimen/dialpad_digits_height</item>
+ <item name="dialpad_digits_adjustable_text_size">@dimen/dialpad_digits_text_size</item>
+ <item name="dialpad_key_numbers_size">@dimen/dialpad_key_numbers_default_size</item>
+ <item name="dialpad_key_number_margin_bottom">@dimen/dialpad_key_number_default_margin_bottom
+ </item>
+ <item name="dialpad_zero_key_number_margin_bottom">
+ @dimen/dialpad_zero_key_number_default_margin_bottom
+ </item>
+ <item name="dialpad_end_key_spacing">@dimen/dialpad_bottom_space_height</item>
+ </style>
+
+ <style name="Dialpad.Light">
+ <item name="dialpad_text_color">@color/dialpad_digits_text_color</item>
+ <item name="dialpad_text_color_primary">@color/dialpad_primary_text_color</item>
+ <item name="dialpad_text_color_secondary">@color/dialpad_secondary_text_color</item>
+ <item name="dialpad_icon_tint">@color/dialpad_icon_tint</item>
+ <item name="dialpad_voicemail_tint">@color/dialpad_voicemail_tint</item>
+ <item name="dialpad_background">@color/background_dialpad</item>
+ </style>
+
+ <style name="Dialpad.Dark">
+ <item name="dialpad_text_color">@android:color/white</item>
+ <item name="dialpad_text_color_primary">@android:color/white</item>
+ <item name="dialpad_text_color_secondary">#ffd4d6d7</item>
+ <item name="dialpad_icon_tint">@android:color/white</item>
+ <item name="dialpad_voicemail_tint">?attr/dialpad_text_color_secondary</item>
+ <item name="dialpad_background">#00000000</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/disabled_lint_checks.txt b/java/com/android/dialer/disabled_lint_checks.txt
new file mode 100644
index 000000000..13a3d05cf
--- /dev/null
+++ b/java/com/android/dialer/disabled_lint_checks.txt
@@ -0,0 +1 @@
+InlinedApi
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
new file mode 100644
index 000000000..14299f92c
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java
@@ -0,0 +1,76 @@
+/*
+ * 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.enrichedcall;
+
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_EnrichedCallCapabilities extends EnrichedCallCapabilities {
+
+ private final boolean supportsCallComposer;
+ private final boolean supportsPostCall;
+
+ AutoValue_EnrichedCallCapabilities(
+ boolean supportsCallComposer,
+ boolean supportsPostCall) {
+ this.supportsCallComposer = supportsCallComposer;
+ this.supportsPostCall = supportsPostCall;
+ }
+
+ @Override
+ public boolean supportsCallComposer() {
+ return supportsCallComposer;
+ }
+
+ @Override
+ public boolean supportsPostCall() {
+ return supportsPostCall;
+ }
+
+ @Override
+ public String toString() {
+ return "EnrichedCallCapabilities{"
+ + "supportsCallComposer=" + supportsCallComposer + ", "
+ + "supportsPostCall=" + supportsPostCall + ", "
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof EnrichedCallCapabilities) {
+ EnrichedCallCapabilities that = (EnrichedCallCapabilities) o;
+ return (this.supportsCallComposer == that.supportsCallComposer())
+ && (this.supportsPostCall == that.supportsPostCall());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.supportsCallComposer ? 1231 : 1237;
+ h *= 1000003;
+ h ^= this.supportsPostCall ? 1231 : 1237;
+ return h;
+ }
+
+}
+
diff --git a/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
new file mode 100644
index 000000000..edfefc479
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java
@@ -0,0 +1,127 @@
+/*
+ * 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.enrichedcall;
+
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_OutgoingCallComposerData extends OutgoingCallComposerData {
+
+ private final String subject;
+ private final Uri imageUri;
+ private final String imageContentType;
+
+ private AutoValue_OutgoingCallComposerData(
+ @Nullable String subject,
+ @Nullable Uri imageUri,
+ @Nullable String imageContentType) {
+ this.subject = subject;
+ this.imageUri = imageUri;
+ this.imageContentType = imageContentType;
+ }
+
+ @Nullable
+ @Override
+ public String getSubject() {
+ return subject;
+ }
+
+ @Nullable
+ @Override
+ public Uri getImageUri() {
+ return imageUri;
+ }
+
+ @Nullable
+ @Override
+ public String getImageContentType() {
+ return imageContentType;
+ }
+
+ @Override
+ public String toString() {
+ return "OutgoingCallComposerData{"
+ + "subject=" + subject + ", "
+ + "imageUri=" + imageUri + ", "
+ + "imageContentType=" + imageContentType
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof OutgoingCallComposerData) {
+ OutgoingCallComposerData that = (OutgoingCallComposerData) o;
+ return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
+ && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
+ && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (subject == null) ? 0 : this.subject.hashCode();
+ h *= 1000003;
+ h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
+ h *= 1000003;
+ h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
+ return h;
+ }
+
+ static final class Builder extends OutgoingCallComposerData.Builder {
+ private String subject;
+ private Uri imageUri;
+ private String imageContentType;
+ Builder() {
+ }
+ private Builder(OutgoingCallComposerData source) {
+ this.subject = source.getSubject();
+ this.imageUri = source.getImageUri();
+ this.imageContentType = source.getImageContentType();
+ }
+ @Override
+ public OutgoingCallComposerData.Builder setSubject(@Nullable String subject) {
+ this.subject = subject;
+ return this;
+ }
+ @Override
+ OutgoingCallComposerData.Builder setImageUri(@Nullable Uri imageUri) {
+ this.imageUri = imageUri;
+ return this;
+ }
+ @Override
+ OutgoingCallComposerData.Builder setImageContentType(@Nullable String imageContentType) {
+ this.imageContentType = imageContentType;
+ return this;
+ }
+ @Override
+ OutgoingCallComposerData autoBuild() {
+ return new AutoValue_OutgoingCallComposerData(
+ this.subject,
+ this.imageUri,
+ this.imageContentType);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
new file mode 100644
index 000000000..b7d780950
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java
@@ -0,0 +1,36 @@
+/*
+ * 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.enrichedcall;
+
+
+
+/** Value type holding enriched call capabilities. */
+
+public abstract class EnrichedCallCapabilities {
+
+ public static final EnrichedCallCapabilities NO_CAPABILITIES =
+ EnrichedCallCapabilities.create(false, false);
+
+ public static EnrichedCallCapabilities create(
+ boolean supportsCallComposer, boolean supportsPostCall) {
+ return new AutoValue_EnrichedCallCapabilities(supportsCallComposer, supportsPostCall);
+ }
+
+ public abstract boolean supportsCallComposer();
+
+ public abstract boolean supportsPostCall();
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManager.java b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
new file mode 100644
index 000000000..6af8c409a
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManager.java
@@ -0,0 +1,225 @@
+/*
+ * 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.enrichedcall;
+
+import android.app.Application;
+import android.support.annotation.IntDef;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.multimedia.MultimediaData;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Performs all enriched calling logic. */
+public interface EnrichedCallManager {
+
+ /** Factory for {@link EnrichedCallManager}. */
+ interface Factory {
+ EnrichedCallManager getEnrichedCallManager();
+ }
+
+ /** Accessor for {@link EnrichedCallManager}. */
+ class Accessor {
+
+ /**
+ * @throws IllegalArgumentException if application does not implement {@link
+ * EnrichedCallManager.Factory}
+ */
+ @NonNull
+ public static EnrichedCallManager getInstance(@NonNull Application application) {
+ Assert.isNotNull(application);
+
+ return ((EnrichedCallManager.Factory) application).getEnrichedCallManager();
+ }
+ }
+
+ /** Receives updates when enriched call capabilities are ready. */
+ interface CapabilitiesListener {
+
+ /** Callback fired when the capabilities are updated. */
+ @MainThread
+ void onCapabilitiesUpdated();
+ }
+
+ /**
+ * Registers the given {@link CapabilitiesListener}.
+ *
+ * <p>As a result of this method, the listener will receive a call to {@link
+ * CapabilitiesListener#onCapabilitiesUpdated()} after a call to {@link
+ * #requestCapabilities(String)}.
+ */
+ @MainThread
+ void registerCapabilitiesListener(@NonNull CapabilitiesListener listener);
+
+ /**
+ * Starts an asynchronous process to get enriched call capabilities of the given number.
+ *
+ * <p>Registered listeners will receive a call to {@link
+ * CapabilitiesListener#onCapabilitiesUpdated()} on completion.
+ *
+ * @param number the remote number in any format
+ */
+ @MainThread
+ void requestCapabilities(@NonNull String number);
+
+ /**
+ * Unregisters the given {@link CapabilitiesListener}.
+ *
+ * <p>As a result of this method, the listener will not receive capabilities of the given number.
+ */
+ @MainThread
+ void unregisterCapabilitiesListener(@NonNull CapabilitiesListener listener);
+
+ /** Gets the cached capabilities for the given number, else null */
+ @MainThread
+ @Nullable
+ EnrichedCallCapabilities getCapabilities(@NonNull String number);
+
+ /** Clears any cached data, such as capabilities. */
+ @MainThread
+ void clearCachedData();
+
+ /** Possible states for call composer sessions. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ STATE_NONE,
+ STATE_STARTING,
+ STATE_STARTED,
+ STATE_START_FAILED,
+ STATE_MESSAGE_SENT,
+ STATE_MESSAGE_FAILED,
+ STATE_CLOSED,
+ })
+ @interface State {}
+
+ int STATE_NONE = 0;
+ int STATE_STARTING = STATE_NONE + 1;
+ int STATE_STARTED = STATE_STARTING + 1;
+ int STATE_START_FAILED = STATE_STARTED + 1;
+ int STATE_MESSAGE_SENT = STATE_START_FAILED + 1;
+ int STATE_MESSAGE_FAILED = STATE_MESSAGE_SENT + 1;
+ int STATE_CLOSED = STATE_MESSAGE_FAILED + 1;
+
+ /**
+ * Starts a call composer session with the given remote number.
+ *
+ * @param number the remote number in any format
+ * @return the id for the started session, or {@link Session#NO_SESSION_ID} if the session fails
+ */
+ @MainThread
+ long startCallComposerSession(@NonNull String number);
+
+ /**
+ * Sends the given information through an open enriched call session. As per the enriched calling
+ * spec, up to two messages are sent: the first is an enriched call data message that optionally
+ * includes the subject and the second is the optional image data message.
+ *
+ * @param sessionId the id for the session. See {@link #startCallComposerSession(String)}
+ * @param data the {@link MultimediaData}
+ * @throws IllegalArgumentException if there's no open session with the given number
+ * @throws IllegalStateException if the session isn't in the {@link #STATE_STARTED} state
+ */
+ @MainThread
+ void sendCallComposerData(long sessionId, @NonNull MultimediaData data);
+
+ /**
+ * Ends the given call composer session. Ending a session means that the call composer session
+ * will be closed.
+ *
+ * @param sessionId the id of the session to end
+ */
+ @MainThread
+ void endCallComposerSession(long sessionId);
+
+ /**
+ * Called once the capabilities are available for a corresponding call to {@link
+ * #requestCapabilities(String)}.
+ *
+ * @param number the remote number in any format
+ * @param capabilities the supported capabilities
+ */
+ @MainThread
+ void onCapabilitiesReceived(
+ @NonNull String number, @NonNull EnrichedCallCapabilities capabilities);
+
+ /** Receives updates when the state of an enriched call changes. */
+ interface StateChangedListener {
+
+ /**
+ * Callback fired when state changes. Listeners should call {@link #getSession(String)} to
+ * retrieve the new state.
+ */
+ void onEnrichedCallStateChanged();
+ }
+
+ /**
+ * Registers the given {@link StateChangedListener}.
+ *
+ * <p>As a result of this method, the listener will receive updates when the state of any enriched
+ * call changes.
+ */
+ @MainThread
+ void registerStateChangedListener(@NonNull StateChangedListener listener);
+
+ /** Returns the {@link Session} for the given number, or {@code null} if no session exists. */
+ @MainThread
+ @Nullable
+ Session getSession(@NonNull String number);
+
+ /** Returns the {@link Session} for the given sessionId, or {@code null} if no session exists. */
+ @MainThread
+ @Nullable
+ Session getSession(long sessionId);
+
+ /**
+ * Unregisters the given {@link StateChangedListener}.
+ *
+ * <p>As a result of this method, the listener will not receive updates when the state of enriched
+ * calls changes.
+ */
+ @MainThread
+ void unregisterStateChangedListener(@NonNull StateChangedListener listener);
+
+ /**
+ * Called when the status of an enriched call session changes.
+ *
+ *
+ * @throws IllegalArgumentException if the state is invalid
+ */
+ @MainThread
+ void onSessionStatusUpdate(long sessionId, @NonNull String number, int state);
+
+ /**
+ * Called when the status of an enriched call message updates.
+ *
+ *
+ * @throws IllegalArgumentException if the state is invalid
+ * @throws IllegalStateException if there's no session for the given id
+ */
+ @MainThread
+ void onMessageUpdate(long sessionId, @NonNull String messageId, int state);
+
+ /**
+ * Called when call composer data arrives for the given session.
+ *
+ * @throws IllegalStateException if there's no session for the given id
+ */
+ @MainThread
+ void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData);
+}
diff --git a/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java b/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
new file mode 100644
index 000000000..db9a799d3
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java
@@ -0,0 +1,84 @@
+/*
+ * 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.enrichedcall;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.multimedia.MultimediaData;
+
+/** Stub implementation of {@link EnrichedCallManager}. */
+public final class EnrichedCallManagerStub implements EnrichedCallManager {
+
+ @Override
+ public void registerCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
+
+ @Override
+ public void requestCapabilities(@NonNull String number) {}
+
+ @Override
+ public void unregisterCapabilitiesListener(@NonNull CapabilitiesListener listener) {}
+
+ @Override
+ public EnrichedCallCapabilities getCapabilities(@NonNull String number) {
+ return null;
+ }
+
+ @Override
+ public void clearCachedData() {}
+
+ @Override
+ public long startCallComposerSession(@NonNull String number) {
+ return Session.NO_SESSION_ID;
+ }
+
+ @Override
+ public void sendCallComposerData(long sessionId, @NonNull MultimediaData data) {}
+
+ @Override
+ public void endCallComposerSession(long sessionId) {}
+
+ @Override
+ public void onCapabilitiesReceived(
+ @NonNull String number, @NonNull EnrichedCallCapabilities capabilities) {}
+
+ @Override
+ public void registerStateChangedListener(@NonNull StateChangedListener listener) {}
+
+ @Nullable
+ @Override
+ public Session getSession(@NonNull String number) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public Session getSession(long sessionId) {
+ return null;
+ }
+
+ @Override
+ public void unregisterStateChangedListener(@NonNull StateChangedListener listener) {}
+
+ @Override
+ public void onSessionStatusUpdate(long sessionId, @NonNull String number, int state) {}
+
+ @Override
+ public void onMessageUpdate(long sessionId, @NonNull String messageId, int state) {}
+
+ @Override
+ public void onIncomingCallComposerData(long sessionId, @NonNull MultimediaData multimediaData) {}
+}
diff --git a/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
new file mode 100644
index 000000000..a8ee49d4e
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java
@@ -0,0 +1,94 @@
+/*
+ * 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.enrichedcall;
+
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+
+
+/**
+ * Value type holding references to all data that could be provided for the call composer.
+ *
+ * <p>Note: Either the subject, the image data, or both must be specified, e.g.
+ *
+ * <pre>
+ * OutgoingCallComposerData.builder.build(); // throws exception, no data set
+ * OutgoingCallComposerData
+ * .setSubject(subject)
+ * .build(); // Success
+ * OutgoingCallComposerData
+ * .setImageData(uri, contentType)
+ * .build(); // Success
+ * OutgoingCallComposerData
+ * .setSubject(subject)
+ * .setImageData(uri, contentType)
+ * .build(); // Success
+ * </pre>
+ */
+
+public abstract class OutgoingCallComposerData {
+
+ public static Builder builder() {
+ return new AutoValue_OutgoingCallComposerData.Builder();
+ }
+
+ public boolean hasImageData() {
+ return getImageUri() != null && getImageContentType() != null;
+ }
+
+ @Nullable
+ public abstract String getSubject();
+
+ @Nullable
+ public abstract Uri getImageUri();
+
+ @Nullable
+ public abstract String getImageContentType();
+
+ /** Builds instances of {@link OutgoingCallComposerData}. */
+
+ public abstract static class Builder {
+ public abstract Builder setSubject(String subject);
+
+ public Builder setImageData(@NonNull Uri imageUri, @NonNull String imageContentType) {
+ setImageUri(Assert.isNotNull(imageUri));
+ setImageContentType(Assert.isNotNull(imageContentType));
+ return this;
+ }
+
+ abstract Builder setImageUri(Uri imageUri);
+
+ abstract Builder setImageContentType(String imageContentType);
+
+ abstract OutgoingCallComposerData autoBuild();
+
+ /**
+ * Returns the OutgoingCallComposerData from this builder.
+ *
+ * @return the OutgoingCallComposerData.
+ * @throws IllegalStateException if neither {@link #setSubject(String)} nor {@link
+ * #setImageData(Uri, String)} were called.
+ */
+ public OutgoingCallComposerData build() {
+ OutgoingCallComposerData data = autoBuild();
+ Assert.checkState(data.getSubject() != null || data.hasImageData());
+ return data;
+ }
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/Session.java b/java/com/android/dialer/enrichedcall/Session.java
new file mode 100644
index 000000000..b0439fae9
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/Session.java
@@ -0,0 +1,63 @@
+/*
+ * 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.enrichedcall;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.enrichedcall.EnrichedCallManager.State;
+import com.android.dialer.multimedia.MultimediaData;
+
+/** Holds state information and data about enriched calling sessions. */
+public interface Session {
+
+ /** Id used for sessions that fail to start. */
+ long NO_SESSION_ID = -1;
+
+ /**
+ * An id for the specific case when sending a message fails so early that a message id isn't
+ * created.
+ */
+ String MESSAGE_ID_COULD_NOT_CREATE_ID = "messageIdCouldNotCreateId";
+
+ /**
+ * Returns the id associated with this session, or {@link #NO_SESSION_ID} if this represents a
+ * session that failed to start.
+ */
+ long getSessionId();
+
+ /** Returns the number associated with the remote end of this session. */
+ @NonNull
+ String getRemoteNumber();
+
+ /** Returns the {@link State} for this session. */
+ @State
+ int getState();
+
+ /** Returns the {@link MultimediaData} associated with this session. */
+ @NonNull
+ MultimediaData getMultimediaData();
+
+ /** Returns type of this session, based on some arbitrarily defined type. */
+ int getType();
+
+ /**
+ * Sets the {@link MultimediaData} for this session.
+ *
+ *
+ * @throws IllegalArgumentException if the type is invalid
+ */
+ void setSessionData(@NonNull MultimediaData multimediaData, int type);
+}
diff --git a/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java b/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
new file mode 100644
index 000000000..39c55d040
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java
@@ -0,0 +1,32 @@
+/*
+ * 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.enrichedcall;
+
+import dagger.Module;
+import dagger.Provides;
+import javax.inject.Singleton;
+
+/** Module which binds {@link EnrichedCallManagerStub}. */
+@Module
+public class StubEnrichedCallModule {
+
+ @Provides
+ @Singleton
+ static EnrichedCallManager provideEnrichedCallManager() {
+ return new EnrichedCallManagerStub();
+ }
+}
diff --git a/java/com/android/dialer/enrichedcall/extensions/StateExtension.java b/java/com/android/dialer/enrichedcall/extensions/StateExtension.java
new file mode 100644
index 000000000..8a4f6409d
--- /dev/null
+++ b/java/com/android/dialer/enrichedcall/extensions/StateExtension.java
@@ -0,0 +1,54 @@
+/*
+ * 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.enrichedcall.extensions;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.EnrichedCallManager.State;
+
+/** Extends the {@link State} to include a toString method. */
+public class StateExtension {
+
+ /** Returns the string representation for the given {@link State}. */
+ @NonNull
+ public static String toString(@State int callComposerState) {
+ if (callComposerState == EnrichedCallManager.STATE_NONE) {
+ return "STATE_NONE";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_STARTING) {
+ return "STATE_STARTING";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_STARTED) {
+ return "STATE_STARTED";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_START_FAILED) {
+ return "STATE_START_FAILED";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_MESSAGE_SENT) {
+ return "STATE_MESSAGE_SENT";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_MESSAGE_FAILED) {
+ return "STATE_MESSAGE_FAILED";
+ }
+ if (callComposerState == EnrichedCallManager.STATE_CLOSED) {
+ return "STATE_CLOSED";
+ }
+ Assert.checkArgument(false, "Unexpected callComposerState: %d", callComposerState);
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/inject/ApplicationModule.java b/java/com/android/dialer/inject/ApplicationModule.java
new file mode 100644
index 000000000..99e5296ea
--- /dev/null
+++ b/java/com/android/dialer/inject/ApplicationModule.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.inject;
+
+import android.app.Application;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import dagger.Module;
+import dagger.Provides;
+
+/** Provides the singleton application object. */
+@Module
+public final class ApplicationModule {
+
+ @NonNull private final Application application;
+
+ public ApplicationModule(@NonNull Application application) {
+ this.application = Assert.isNotNull(application);
+ }
+
+ @Provides
+ Application provideApplication() {
+ return application;
+ }
+}
diff --git a/java/com/android/dialer/inject/DialerAppComponent.java b/java/com/android/dialer/inject/DialerAppComponent.java
new file mode 100644
index 000000000..9832ce804
--- /dev/null
+++ b/java/com/android/dialer/inject/DialerAppComponent.java
@@ -0,0 +1,29 @@
+/*
+ * 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.inject;
+
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.StubEnrichedCallModule;
+import dagger.Component;
+import javax.inject.Singleton;
+
+/** Core application-wide {@link Component} for the open source dialer app. */
+@Singleton
+@Component(modules = {ApplicationModule.class, StubEnrichedCallModule.class})
+public interface DialerAppComponent {
+ EnrichedCallManager enrichedCallManager();
+}
diff --git a/java/com/android/dialer/interactions/AndroidManifest.xml b/java/com/android/dialer/interactions/AndroidManifest.xml
new file mode 100644
index 000000000..4571a6965
--- /dev/null
+++ b/java/com/android/dialer/interactions/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.interactions">
+
+ <application>
+
+ <!-- Service to update a contact -->
+ <service
+ android:exported="false"
+ android:name="com.android.dialer.interactions.ContactUpdateService"/>
+
+ <receiver android:name="com.android.dialer.interactions.UndemoteOutgoingCallReceiver">
+ <intent-filter>
+ <action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
+ </intent-filter>
+ </receiver>
+
+ </application>
+
+</manifest>
+
diff --git a/java/com/android/dialer/interactions/ContactUpdateService.java b/java/com/android/dialer/interactions/ContactUpdateService.java
new file mode 100644
index 000000000..9b2d701d2
--- /dev/null
+++ b/java/com/android/dialer/interactions/ContactUpdateService.java
@@ -0,0 +1,48 @@
+/*
+ * 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.interactions;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import com.android.contacts.common.database.ContactUpdateUtils;
+
+/** Service for updating primary number on a contact. */
+public class ContactUpdateService extends IntentService {
+
+ public static final String EXTRA_PHONE_NUMBER_DATA_ID = "phone_number_data_id";
+
+ public ContactUpdateService() {
+ super(ContactUpdateService.class.getSimpleName());
+ setIntentRedelivery(true);
+ }
+
+ /** Creates an intent that sets the selected data item as super primary (default) */
+ public static Intent createSetSuperPrimaryIntent(Context context, long dataId) {
+ Intent serviceIntent = new Intent(context, ContactUpdateService.class);
+ serviceIntent.putExtra(EXTRA_PHONE_NUMBER_DATA_ID, dataId);
+ return serviceIntent;
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ // Currently this service only handles one type of update.
+ long dataId = intent.getLongExtra(EXTRA_PHONE_NUMBER_DATA_ID, -1);
+
+ ContactUpdateUtils.setSuperPrimary(this, dataId);
+ }
+}
diff --git a/java/com/android/dialer/interactions/PhoneNumberInteraction.java b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
new file mode 100644
index 000000000..f36e5319c
--- /dev/null
+++ b/java/com/android/dialer/interactions/PhoneNumberInteraction.java
@@ -0,0 +1,557 @@
+/*
+ * 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.interactions;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.IntDef;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.ListAdapter;
+import android.widget.TextView;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.Collapser.Collapsible;
+import com.android.contacts.common.MoreContactUtils;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callintent.CallIntentParser;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.TransactionSafeActivity;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Initiates phone calls or a text message. If there are multiple candidates, this class shows a
+ * dialog to pick one. Creating one of these interactions should be done through the static factory
+ * methods.
+ *
+ * <p>Note that this class initiates not only usual *phone* calls but also *SIP* calls.
+ *
+ * <p>TODO: clean up code and documents since it is quite confusing to use "phone numbers" or "phone
+ * calls" here while they can be SIP addresses or SIP calls (See also issue 5039627).
+ */
+public class PhoneNumberInteraction implements OnLoadCompleteListener<Cursor> {
+
+ private static final String TAG = PhoneNumberInteraction.class.getSimpleName();
+ /** The identifier for a permissions request if one is generated. */
+ public static final int REQUEST_READ_CONTACTS = 1;
+
+ private static final String[] PHONE_NUMBER_PROJECTION =
+ new String[] {
+ Phone._ID,
+ Phone.NUMBER,
+ Phone.IS_SUPER_PRIMARY,
+ RawContacts.ACCOUNT_TYPE,
+ RawContacts.DATA_SET,
+ Phone.TYPE,
+ Phone.LABEL,
+ Phone.MIMETYPE,
+ Phone.CONTACT_ID,
+ };
+
+ private static final String PHONE_NUMBER_SELECTION =
+ Data.MIMETYPE
+ + " IN ('"
+ + Phone.CONTENT_ITEM_TYPE
+ + "', "
+ + "'"
+ + SipAddress.CONTENT_ITEM_TYPE
+ + "') AND "
+ + Data.DATA1
+ + " NOT NULL";
+ private static final int UNKNOWN_CONTACT_ID = -1;
+ private final Context mContext;
+ private final int mInteractionType;
+ private final CallSpecificAppData mCallSpecificAppData;
+ private long mContactId = UNKNOWN_CONTACT_ID;
+ private CursorLoader mLoader;
+ private boolean mIsVideoCall;
+
+ /** Error codes for interactions. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ InteractionErrorCode.CONTACT_NOT_FOUND,
+ InteractionErrorCode.CONTACT_HAS_NO_NUMBER,
+ InteractionErrorCode.USER_LEAVING_ACTIVITY,
+ InteractionErrorCode.OTHER_ERROR
+ }
+ )
+ public @interface InteractionErrorCode {
+
+ int CONTACT_NOT_FOUND = 1;
+ int CONTACT_HAS_NO_NUMBER = 2;
+ int OTHER_ERROR = 3;
+ int USER_LEAVING_ACTIVITY = 4;
+ }
+
+ /**
+ * Activities which use this class must implement this. They will be notified if there was an
+ * error performing the interaction. For example, this callback will be invoked on the activity if
+ * the contact URI provided points to a deleted contact, or to a contact without a phone number.
+ */
+ public interface InteractionErrorListener {
+
+ void interactionError(@InteractionErrorCode int interactionErrorCode);
+ }
+
+ /**
+ * Activities which use this class must implement this. They will be notified if the phone number
+ * disambiguation dialog is dismissed.
+ */
+ public interface DisambigDialogDismissedListener {
+ void onDisambigDialogDismissed();
+ }
+
+ private PhoneNumberInteraction(
+ Context context,
+ int interactionType,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ mContext = context;
+ mInteractionType = interactionType;
+ mCallSpecificAppData = callSpecificAppData;
+ mIsVideoCall = isVideoCall;
+
+ Assert.checkArgument(context instanceof InteractionErrorListener);
+ Assert.checkArgument(context instanceof DisambigDialogDismissedListener);
+ Assert.checkArgument(context instanceof ActivityCompat.OnRequestPermissionsResultCallback);
+ }
+
+ private static void performAction(
+ Context context,
+ String phoneNumber,
+ int interactionType,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ Intent intent;
+ switch (interactionType) {
+ case ContactDisplayUtils.INTERACTION_SMS:
+ intent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("sms", phoneNumber, null));
+ break;
+ default:
+ intent =
+ new CallIntentBuilder(phoneNumber, callSpecificAppData)
+ .setIsVideoCall(isVideoCall)
+ .build();
+ break;
+ }
+ DialerUtils.startActivityWithErrorToast(context, intent);
+ }
+
+ /**
+ * @param activity that is calling this interaction. This must be of type {@link
+ * TransactionSafeActivity} because we need to check on the activity state after the phone
+ * numbers have been queried for. The activity must implement {@link InteractionErrorListener}
+ * and {@link DisambigDialogDismissedListener}.
+ * @param isVideoCall {@code true} if the call is a video call, {@code false} otherwise.
+ */
+ public static void startInteractionForPhoneCall(
+ TransactionSafeActivity activity,
+ Uri uri,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ new PhoneNumberInteraction(
+ activity, ContactDisplayUtils.INTERACTION_CALL, isVideoCall, callSpecificAppData)
+ .startInteraction(uri);
+ }
+
+ private void performAction(String phoneNumber) {
+ PhoneNumberInteraction.performAction(
+ mContext, phoneNumber, mInteractionType, mIsVideoCall, mCallSpecificAppData);
+ }
+
+ /**
+ * Initiates the interaction to result in either a phone call or sms message for a contact.
+ *
+ * @param uri Contact Uri
+ */
+ private void startInteraction(Uri uri) {
+ // It's possible for a shortcut to have been created, and then Contacts permissions revoked. To
+ // avoid a crash when the user tries to use such a shortcut, check for this condition and ask
+ // the user for the permission.
+ if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("PhoneNumberInteraction.startInteraction", "No contact permissions");
+ ActivityCompat.requestPermissions(
+ (Activity) mContext,
+ new String[] {Manifest.permission.READ_CONTACTS},
+ REQUEST_READ_CONTACTS);
+ return;
+ }
+
+ if (mLoader != null) {
+ mLoader.reset();
+ }
+ final Uri queryUri;
+ final String inputUriAsString = uri.toString();
+ if (inputUriAsString.startsWith(Contacts.CONTENT_URI.toString())) {
+ if (!inputUriAsString.endsWith(Contacts.Data.CONTENT_DIRECTORY)) {
+ queryUri = Uri.withAppendedPath(uri, Contacts.Data.CONTENT_DIRECTORY);
+ } else {
+ queryUri = uri;
+ }
+ } else if (inputUriAsString.startsWith(Data.CONTENT_URI.toString())) {
+ queryUri = uri;
+ } else {
+ throw new UnsupportedOperationException(
+ "Input Uri must be contact Uri or data Uri (input: \"" + uri + "\")");
+ }
+
+ mLoader =
+ new CursorLoader(
+ mContext, queryUri, PHONE_NUMBER_PROJECTION, PHONE_NUMBER_SELECTION, null, null);
+ mLoader.registerListener(0, this);
+ mLoader.startLoading();
+ }
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
+ if (cursor == null) {
+ LogUtil.i("PhoneNumberInteraction.onLoadComplete", "null cursor");
+ interactionError(InteractionErrorCode.OTHER_ERROR);
+ return;
+ }
+ try {
+ ArrayList<PhoneItem> phoneList = new ArrayList<>();
+ String primaryPhone = null;
+ if (!isSafeToCommitTransactions()) {
+ LogUtil.i("PhoneNumberInteraction.onLoadComplete", "not safe to commit transaction");
+ interactionError(InteractionErrorCode.USER_LEAVING_ACTIVITY);
+ return;
+ }
+ if (cursor.moveToFirst()) {
+ int contactIdColumn = cursor.getColumnIndexOrThrow(Phone.CONTACT_ID);
+ int isSuperPrimaryColumn = cursor.getColumnIndexOrThrow(Phone.IS_SUPER_PRIMARY);
+ int phoneNumberColumn = cursor.getColumnIndexOrThrow(Phone.NUMBER);
+ int phoneIdColumn = cursor.getColumnIndexOrThrow(Phone._ID);
+ int accountTypeColumn = cursor.getColumnIndexOrThrow(RawContacts.ACCOUNT_TYPE);
+ int dataSetColumn = cursor.getColumnIndexOrThrow(RawContacts.DATA_SET);
+ int phoneTypeColumn = cursor.getColumnIndexOrThrow(Phone.TYPE);
+ int phoneLabelColumn = cursor.getColumnIndexOrThrow(Phone.LABEL);
+ int phoneMimeTpeColumn = cursor.getColumnIndexOrThrow(Phone.MIMETYPE);
+ do {
+ if (mContactId == UNKNOWN_CONTACT_ID) {
+ mContactId = cursor.getLong(contactIdColumn);
+ }
+
+ if (cursor.getInt(isSuperPrimaryColumn) != 0) {
+ // Found super primary, call it.
+ primaryPhone = cursor.getString(phoneNumberColumn);
+ }
+
+ PhoneItem item = new PhoneItem();
+ item.id = cursor.getLong(phoneIdColumn);
+ item.phoneNumber = cursor.getString(phoneNumberColumn);
+ item.accountType = cursor.getString(accountTypeColumn);
+ item.dataSet = cursor.getString(dataSetColumn);
+ item.type = cursor.getInt(phoneTypeColumn);
+ item.label = cursor.getString(phoneLabelColumn);
+ item.mimeType = cursor.getString(phoneMimeTpeColumn);
+
+ phoneList.add(item);
+ } while (cursor.moveToNext());
+ } else {
+ interactionError(InteractionErrorCode.CONTACT_NOT_FOUND);
+ return;
+ }
+
+ if (primaryPhone != null) {
+ performAction(primaryPhone);
+ return;
+ }
+
+ Collapser.collapseList(phoneList, mContext);
+ if (phoneList.size() == 0) {
+ interactionError(InteractionErrorCode.CONTACT_HAS_NO_NUMBER);
+ } else if (phoneList.size() == 1) {
+ PhoneItem item = phoneList.get(0);
+ performAction(item.phoneNumber);
+ } else {
+ // There are multiple candidates. Let the user choose one.
+ showDisambiguationDialog(phoneList);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ private void interactionError(@InteractionErrorCode int interactionErrorCode) {
+ // mContext is really the activity -- see ctor docs.
+ ((InteractionErrorListener) mContext).interactionError(interactionErrorCode);
+ }
+
+ private boolean isSafeToCommitTransactions() {
+ return !(mContext instanceof TransactionSafeActivity)
+ || ((TransactionSafeActivity) mContext).isSafeToCommitTransactions();
+ }
+
+ @VisibleForTesting
+ /* package */ CursorLoader getLoader() {
+ return mLoader;
+ }
+
+ private void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
+ final Activity activity = (Activity) mContext;
+ if (activity.isDestroyed()) {
+ // Check whether the activity is still running
+ return;
+ }
+ try {
+ PhoneDisambiguationDialogFragment.show(
+ activity.getFragmentManager(),
+ phoneList,
+ mInteractionType,
+ mIsVideoCall,
+ mCallSpecificAppData);
+ } catch (IllegalStateException e) {
+ // ignore to be safe. Shouldn't happen because we checked the
+ // activity wasn't destroyed, but to be safe.
+ }
+ }
+
+ /** A model object for capturing a phone number for a given contact. */
+ @VisibleForTesting
+ /* package */ static class PhoneItem implements Parcelable, Collapsible<PhoneItem> {
+
+ public static final Parcelable.Creator<PhoneItem> CREATOR =
+ new Parcelable.Creator<PhoneItem>() {
+ @Override
+ public PhoneItem createFromParcel(Parcel in) {
+ return new PhoneItem(in);
+ }
+
+ @Override
+ public PhoneItem[] newArray(int size) {
+ return new PhoneItem[size];
+ }
+ };
+ long id;
+ String phoneNumber;
+ String accountType;
+ String dataSet;
+ long type;
+ String label;
+ /** {@link Phone#CONTENT_ITEM_TYPE} or {@link SipAddress#CONTENT_ITEM_TYPE}. */
+ String mimeType;
+
+ private PhoneItem() {}
+
+ private PhoneItem(Parcel in) {
+ this.id = in.readLong();
+ this.phoneNumber = in.readString();
+ this.accountType = in.readString();
+ this.dataSet = in.readString();
+ this.type = in.readLong();
+ this.label = in.readString();
+ this.mimeType = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeString(phoneNumber);
+ dest.writeString(accountType);
+ dest.writeString(dataSet);
+ dest.writeLong(type);
+ dest.writeString(label);
+ dest.writeString(mimeType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void collapseWith(PhoneItem phoneItem) {
+ // Just keep the number and id we already have.
+ }
+
+ @Override
+ public boolean shouldCollapseWith(PhoneItem phoneItem, Context context) {
+ return MoreContactUtils.shouldCollapse(
+ Phone.CONTENT_ITEM_TYPE, phoneNumber, Phone.CONTENT_ITEM_TYPE, phoneItem.phoneNumber);
+ }
+
+ @Override
+ public String toString() {
+ return phoneNumber;
+ }
+ }
+
+ /** A list adapter that populates the list of contact's phone numbers. */
+ private static class PhoneItemAdapter extends ArrayAdapter<PhoneItem> {
+
+ private final int mInteractionType;
+
+ PhoneItemAdapter(Context context, List<PhoneItem> list, int interactionType) {
+ super(context, R.layout.phone_disambig_item, android.R.id.text2, list);
+ mInteractionType = interactionType;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ final View view = super.getView(position, convertView, parent);
+
+ final PhoneItem item = getItem(position);
+ Assert.isNotNull(item, "Null item at position: %d", position);
+ final TextView typeView = (TextView) view.findViewById(android.R.id.text1);
+ CharSequence value =
+ ContactDisplayUtils.getLabelForCallOrSms(
+ (int) item.type, item.label, mInteractionType, getContext());
+
+ typeView.setText(value);
+ return view;
+ }
+ }
+
+ /**
+ * {@link DialogFragment} used for displaying a dialog with a list of phone numbers of which one
+ * will be chosen to make a call or initiate an sms message.
+ *
+ * <p>It is recommended to use {@link #startInteractionForPhoneCall(TransactionSafeActivity, Uri,
+ * boolean, int)} instead of directly using this class, as those methods handle one or multiple
+ * data cases appropriately.
+ *
+ * <p>This fragment may only be attached to activities which implement {@link
+ * DisambigDialogDismissedListener}.
+ */
+ @SuppressWarnings("WeakerAccess") // Made public to let the system reach this class
+ public static class PhoneDisambiguationDialogFragment extends DialogFragment
+ implements DialogInterface.OnClickListener, DialogInterface.OnDismissListener {
+
+ private static final String ARG_PHONE_LIST = "phoneList";
+ private static final String ARG_INTERACTION_TYPE = "interactionType";
+ private static final String ARG_IS_VIDEO_CALL = "is_video_call";
+
+ private int mInteractionType;
+ private ListAdapter mPhonesAdapter;
+ private List<PhoneItem> mPhoneList;
+ private CallSpecificAppData mCallSpecificAppData;
+ private boolean mIsVideoCall;
+
+ public PhoneDisambiguationDialogFragment() {
+ super();
+ }
+
+ public static void show(
+ FragmentManager fragmentManager,
+ ArrayList<PhoneItem> phoneList,
+ int interactionType,
+ boolean isVideoCall,
+ CallSpecificAppData callSpecificAppData) {
+ PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
+ bundle.putInt(ARG_INTERACTION_TYPE, interactionType);
+ bundle.putBoolean(ARG_IS_VIDEO_CALL, isVideoCall);
+ CallIntentParser.putCallSpecificAppData(bundle, callSpecificAppData);
+ fragment.setArguments(bundle);
+ fragment.show(fragmentManager, TAG);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Activity activity = getActivity();
+ Assert.checkState(activity instanceof DisambigDialogDismissedListener);
+
+ mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
+ mInteractionType = getArguments().getInt(ARG_INTERACTION_TYPE);
+ mIsVideoCall = getArguments().getBoolean(ARG_IS_VIDEO_CALL);
+ mCallSpecificAppData = CallIntentParser.getCallSpecificAppData(getArguments());
+
+ mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType);
+ final LayoutInflater inflater = activity.getLayoutInflater();
+ @SuppressLint("InflateParams") // Allowed since dialog view is not available yet
+ final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null);
+ return new AlertDialog.Builder(activity)
+ .setAdapter(mPhonesAdapter, this)
+ .setTitle(
+ mInteractionType == ContactDisplayUtils.INTERACTION_SMS
+ ? R.string.sms_disambig_title
+ : R.string.call_disambig_title)
+ .setView(setPrimaryView)
+ .create();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ final Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+ final AlertDialog alertDialog = (AlertDialog) dialog;
+ if (mPhoneList.size() > which && which >= 0) {
+ final PhoneItem phoneItem = mPhoneList.get(which);
+ final CheckBox checkBox = (CheckBox) alertDialog.findViewById(R.id.setPrimary);
+ if (checkBox.isChecked()) {
+ // Request to mark the data as primary in the background.
+ final Intent serviceIntent =
+ ContactUpdateService.createSetSuperPrimaryIntent(activity, phoneItem.id);
+ activity.startService(serviceIntent);
+ }
+
+ PhoneNumberInteraction.performAction(
+ activity, phoneItem.phoneNumber, mInteractionType, mIsVideoCall, mCallSpecificAppData);
+ } else {
+ dialog.dismiss();
+ }
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ super.onDismiss(dialogInterface);
+ Activity activity = getActivity();
+ if (activity != null) {
+ ((DisambigDialogDismissedListener) activity).onDisambigDialogDismissed();
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java b/java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
new file mode 100644
index 000000000..68b011a04
--- /dev/null
+++ b/java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java
@@ -0,0 +1,107 @@
+/*
+ * 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.interactions;
+
+import static android.Manifest.permission.READ_CONTACTS;
+import static android.Manifest.permission.WRITE_CONTACTS;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.PinnedPositions;
+import android.text.TextUtils;
+import com.android.dialer.util.PermissionsUtil;
+
+/**
+ * This broadcast receiver is used to listen to outgoing calls and undemote formerly demoted
+ * contacts if a phone call is made to a phone number belonging to that contact.
+ *
+ * <p>NOTE This doesn't work for corp contacts.
+ */
+public class UndemoteOutgoingCallReceiver extends BroadcastReceiver {
+
+ private static final long NO_CONTACT_FOUND = -1;
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (!PermissionsUtil.hasPermission(context, READ_CONTACTS)
+ || !PermissionsUtil.hasPermission(context, WRITE_CONTACTS)) {
+ return;
+ }
+ if (intent != null && Intent.ACTION_NEW_OUTGOING_CALL.equals(intent.getAction())) {
+ final String number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+ new Thread() {
+ @Override
+ public void run() {
+ final long id = getContactIdFromPhoneNumber(context, number);
+ if (id != NO_CONTACT_FOUND) {
+ undemoteContactWithId(context, id);
+ }
+ }
+ }.start();
+ }
+ }
+
+ private void undemoteContactWithId(Context context, long id) {
+ // If the contact is not demoted, this will not do anything. Otherwise, it will
+ // restore it to an unpinned position. If it was a frequently called contact, it will
+ // show up once again show up on the favorites screen.
+ if (PermissionsUtil.hasPermission(context, WRITE_CONTACTS)) {
+ try {
+ PinnedPositions.undemote(context.getContentResolver(), id);
+ } catch (SecurityException e) {
+ // Just in case
+ }
+ }
+ }
+
+ private long getContactIdFromPhoneNumber(Context context, String number) {
+ if (!PermissionsUtil.hasPermission(context, READ_CONTACTS)) {
+ return NO_CONTACT_FOUND;
+ }
+ final Uri contactUri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number));
+ final Cursor cursor;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(contactUri, new String[] {PhoneLookup._ID}, null, null, null);
+ } catch (SecurityException e) {
+ // Just in case
+ return NO_CONTACT_FOUND;
+ }
+ if (cursor == null) {
+ return NO_CONTACT_FOUND;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ final long id = cursor.getLong(0);
+ return id;
+ } else {
+ return NO_CONTACT_FOUND;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+}
diff --git a/java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml b/java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml
new file mode 100644
index 000000000..879ea0e96
--- /dev/null
+++ b/java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ class="com.android.contacts.common.widget.ActivityTouchLinearLayout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="30dip"
+ android:paddingEnd="30dip"
+ android:gravity="center_vertical"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@android:id/text1"
+ android:textStyle="bold"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+ <!-- Phone number should be displayed ltr -->
+ <TextView
+ android:id="@android:id/text2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="-4dip"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textDirection="ltr"/>
+
+</view>
diff --git a/java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml b/java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml
new file mode 100644
index 000000000..62ef4b76f
--- /dev/null
+++ b/java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="14dip"
+ android:paddingEnd="15dip"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/setPrimary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clickable="true"
+ android:focusable="true"
+ android:text="@string/make_primary"/>
+</LinearLayout>
diff --git a/java/com/android/dialer/interactions/res/values/strings.xml b/java/com/android/dialer/interactions/res/values/strings.xml
new file mode 100644
index 000000000..eea8795b5
--- /dev/null
+++ b/java/com/android/dialer/interactions/res/values/strings.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <!-- Title for the sms disambiguation dialog -->
+ <string name="sms_disambig_title">Choose number</string>
+
+ <!-- Title for the call disambiguation dialog -->
+ <string name="call_disambig_title">Choose number</string>
+
+ <!-- Message next to disambiguation dialog check box -->
+ <string name="make_primary">Remember this choice</string>
+
+</resources>
diff --git a/java/com/android/dialer/logging/Logger.java b/java/com/android/dialer/logging/Logger.java
new file mode 100644
index 000000000..207c35d9c
--- /dev/null
+++ b/java/com/android/dialer/logging/Logger.java
@@ -0,0 +1,49 @@
+/*
+ * 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.logging;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Single entry point for all logging/analytics-related work for all user interactions. */
+public class Logger {
+
+ private static LoggingBindings loggingBindings;
+
+ private Logger() {}
+
+ public static LoggingBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (loggingBindings != null) {
+ return loggingBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof LoggingBindingsFactory) {
+ loggingBindings = ((LoggingBindingsFactory) application).newLoggingBindings();
+ }
+
+ if (loggingBindings == null) {
+ loggingBindings = new LoggingBindingsStub();
+ }
+ return loggingBindings;
+ }
+
+ public static void setForTesting(LoggingBindings loggingBindings) {
+ Logger.loggingBindings = loggingBindings;
+ }
+}
diff --git a/java/com/android/dialer/logging/LoggingBindings.java b/java/com/android/dialer/logging/LoggingBindings.java
new file mode 100644
index 000000000..cf921c3fa
--- /dev/null
+++ b/java/com/android/dialer/logging/LoggingBindings.java
@@ -0,0 +1,59 @@
+/*
+ * 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.logging;
+
+import android.app.Activity;
+
+/** Allows the container application to gather analytics. */
+public interface LoggingBindings {
+
+ /**
+ * Logs an impression for a general dialer event that's not associated with a specific call.
+ *
+ * @param dialerImpression an integer representing what event occurred.
+ * @see com.android.dialer.logging.nano.DialerImpression
+ */
+ void logImpression(int dialerImpression);
+
+ /**
+ * Logs an impression for a general dialer event that's associated with a specific call.
+ *
+ * @param dialerImpression an integer representing what event occurred.
+ * @param callId unique ID of the call.
+ * @param callStartTimeMillis the absolute time when the call started.
+ * @see com.android.dialer.logging.nano.DialerImpression
+ */
+ void logCallImpression(int dialerImpression, String callId, long callStartTimeMillis);
+
+ /**
+ * Logs an interaction that occurred.
+ *
+ * @param interaction an integer representing what interaction occurred.
+ * @see com.android.dialer.logging.nano.InteractionEvent
+ */
+ void logInteraction(int interaction);
+
+ /**
+ * Logs an event indicating that a screen was displayed.
+ *
+ * @param screenEvent an integer representing the displayed screen.
+ * @param activity Parent activity of the displayed screen.
+ * @see com.android.dialer.logging.nano.ScreenEvent
+ */
+ void logScreenView(int screenEvent, Activity activity);
+
+ /** Logs a hit event to the analytics server. */
+ void sendHitEventAnalytics(String category, String action, String label, long value);
+}
diff --git a/java/com/android/dialer/logging/LoggingBindingsFactory.java b/java/com/android/dialer/logging/LoggingBindingsFactory.java
new file mode 100644
index 000000000..0722cf453
--- /dev/null
+++ b/java/com/android/dialer/logging/LoggingBindingsFactory.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.dialer.logging;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the LoggingBindings.
+ */
+public interface LoggingBindingsFactory {
+
+ LoggingBindings newLoggingBindings();
+}
diff --git a/java/com/android/dialer/logging/LoggingBindingsStub.java b/java/com/android/dialer/logging/LoggingBindingsStub.java
new file mode 100644
index 000000000..89c56eb91
--- /dev/null
+++ b/java/com/android/dialer/logging/LoggingBindingsStub.java
@@ -0,0 +1,36 @@
+/*
+ * 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.logging;
+
+import android.app.Activity;
+
+/** Default implementation for logging bindings. */
+public class LoggingBindingsStub implements LoggingBindings {
+
+ @Override
+ public void logImpression(int dialerImpression) {}
+
+ @Override
+ public void logCallImpression(int dialerImpression, String callId, long callStartTimeMillis) {}
+
+ @Override
+ public void logInteraction(int interaction) {}
+
+ @Override
+ public void logScreenView(int screenEvent, Activity activity) {}
+
+ @Override
+ public void sendHitEventAnalytics(String category, String action, String label, long value) {}
+}
diff --git a/java/com/android/dialer/logging/nano/ContactLookupResult.java b/java/com/android/dialer/logging/nano/ContactLookupResult.java
new file mode 100644
index 000000000..8960560fb
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ContactLookupResult.java
@@ -0,0 +1,91 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class ContactLookupResult extends
+ com.google.protobuf.nano.ExtendableMessageNano<ContactLookupResult> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_LOOKUP_RESULT_TYPE = 0;
+ public static final int NOT_FOUND = 1;
+ public static final int LOCAL_CONTACT = 2;
+ public static final int LOCAL_CACHE = 3;
+ public static final int REMOTE = 4;
+ public static final int EMERGENCY = 5;
+ public static final int VOICEMAIL = 6;
+ }
+
+ private static volatile ContactLookupResult[] _emptyArray;
+ public static ContactLookupResult[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ContactLookupResult[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ContactLookupResult)
+
+ public ContactLookupResult() {
+ clear();
+ }
+
+ public ContactLookupResult clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ContactLookupResult mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ContactLookupResult parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactLookupResult(), data);
+ }
+
+ public static ContactLookupResult parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ContactLookupResult().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/ContactSource.java b/java/com/android/dialer/logging/nano/ContactSource.java
new file mode 100644
index 000000000..35d8b8ca1
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ContactSource.java
@@ -0,0 +1,90 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class ContactSource extends
+ com.google.protobuf.nano.ExtendableMessageNano<ContactSource> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_SOURCE_TYPE = 0;
+ public static final int SOURCE_TYPE_DIRECTORY = 1;
+ public static final int SOURCE_TYPE_EXTENDED = 2;
+ public static final int SOURCE_TYPE_PLACES = 3;
+ public static final int SOURCE_TYPE_PROFILE = 4;
+ public static final int SOURCE_TYPE_CNAP = 5;
+ }
+
+ private static volatile ContactSource[] _emptyArray;
+ public static ContactSource[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ContactSource[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ContactSource)
+
+ public ContactSource() {
+ clear();
+ }
+
+ public ContactSource clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ContactSource mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ContactSource parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ContactSource(), data);
+ }
+
+ public static ContactSource parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ContactSource().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/DialerImpression.java b/java/com/android/dialer/logging/nano/DialerImpression.java
new file mode 100644
index 000000000..6bb56751f
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/DialerImpression.java
@@ -0,0 +1,178 @@
+/*
+ * 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.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class DialerImpression extends
+ com.google.protobuf.nano.ExtendableMessageNano<DialerImpression> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_AOSP_EVENT_TYPE = 1000;
+ public static final int APP_LAUNCHED = 1001;
+ public static final int IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE = 1002;
+ public static final int IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE = 1003;
+ public static final int CALL_LOG_BLOCK_REPORT_SPAM = 1004;
+ public static final int CALL_LOG_BLOCK_NUMBER = 1005;
+ public static final int CALL_LOG_UNBLOCK_NUMBER = 1006;
+ public static final int CALL_LOG_REPORT_AS_NOT_SPAM = 1007;
+ public static final int DIALOG_ACTION_CONFIRM_NUMBER_NOT_SPAM = 1008;
+ public static final int REPORT_AS_NOT_SPAM_VIA_UNBLOCK_NUMBER = 1009;
+ public static final int DIALOG_ACTION_CONFIRM_NUMBER_SPAM_INDIRECTLY_VIA_BLOCK_NUMBER = 1010;
+ public static final int REPORT_CALL_AS_SPAM_VIA_CALL_LOG_BLOCK_REPORT_SPAM_SENT_VIA_BLOCK_NUMBER_DIALOG = 1011;
+ public static final int USER_ACTION_BLOCKED_NUMBER = 1012;
+ public static final int USER_ACTION_UNBLOCKED_NUMBER = 1013;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER = 1014;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG = 1015;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG = 1016;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS = 1019;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM = 1020;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_NOT_SPAM_AND_BLOCKED = 1021;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM = 1022;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG = 1024;
+ public static final int SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG = 1025;
+ public static final int SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_SPAM = 1026;
+ public static final int SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_NOT_SPAM = 1027;
+ public static final int USER_PARTICIPATED_IN_A_CALL = 1028;
+ public static final int INCOMING_SPAM_CALL = 1029;
+ public static final int INCOMING_NON_SPAM_CALL = 1030;
+ public static final int SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE = 1041;
+ public static final int SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE = 1042;
+ public static final int NON_SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE = 1043;
+ public static final int NON_SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE = 1044;
+ public static final int VOICEMAIL_ALERT_SET_PIN_SHOWN = 1045;
+ public static final int VOICEMAIL_ALERT_SET_PIN_CLICKED = 1046;
+ public static final int USER_DID_NOT_PARTICIPATE_IN_CALL = 1047;
+ public static final int USER_DELETED_CALL_LOG_ITEM = 1048;
+ public static final int CALL_LOG_SEND_MESSAGE = 1049;
+ public static final int CALL_LOG_ADD_TO_CONTACT = 1050;
+ public static final int CALL_LOG_CREATE_NEW_CONTACT = 1051;
+ public static final int VOICEMAIL_DELETE_ENTRY = 1052;
+ public static final int VOICEMAIL_EXPAND_ENTRY = 1053;
+ public static final int VOICEMAIL_PLAY_AUDIO_DIRECTLY = 1054;
+ public static final int VOICEMAIL_PLAY_AUDIO_AFTER_EXPANDING_ENTRY = 1055;
+ public static final int REJECT_INCOMING_CALL_FROM_NOTIFICATION = 1056;
+ public static final int REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN = 1057;
+ public static final int CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM = 1058;
+ public static final int CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER = 1059;
+ public static final int CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER = 1060;
+ public static final int CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM = 1061;
+ public static final int NEW_CONTACT_OVERFLOW = 1062;
+ public static final int NEW_CONTACT_FAB = 1063;
+ public static final int VOICEMAIL_VVM3_TOS_SHOWN = 1064;
+ public static final int VOICEMAIL_VVM3_TOS_ACCEPTED = 1065;
+ public static final int VOICEMAIL_VVM3_TOS_DECLINED = 1066;
+ public static final int VOICEMAIL_VVM3_TOS_DECLINE_CLICKED = 1067;
+ public static final int VOICEMAIL_VVM3_TOS_DECLINE_CHANGE_PIN_SHOWN = 1068;
+ public static final int STORAGE_PERMISSION_DISPLAYED = 1069;
+ public static final int CAMERA_PERMISSION_DISPLAYED = 1074;
+ public static final int STORAGE_PERMISSION_REQUESTED = 1070;
+ public static final int CAMERA_PERMISSION_REQUESTED = 1075;
+ public static final int STORAGE_PERMISSION_SETTINGS = 1071;
+ public static final int CAMERA_PERMISSION_SETTINGS = 1076;
+ public static final int STORAGE_PERMISSION_GRANTED = 1072;
+ public static final int CAMERA_PERMISSION_GRANTED = 1077;
+ public static final int STORAGE_PERMISSION_DENIED = 1073;
+ public static final int CAMERA_PERMISSION_DENIED = 1078;
+ public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_ACTIVITY = 1079;
+ public static final int VOICEMAIL_CONFIGURATION_STATE_CORRUPTION_DETECTED_FROM_NOTIFICATION = 1080;
+ public static final int BACKUP_ON_BACKUP = 1081;
+ public static final int BACKUP_ON_FULL_BACKUP = 1082;
+ public static final int BACKUP_ON_BACKUP_DISABLED = 1083;
+ public static final int BACKUP_VOICEMAIL_BACKED_UP = 1084;
+ public static final int BACKUP_FULL_BACKED_UP = 1085;
+ public static final int BACKUP_ON_BACKUP_JSON_EXCEPTION = 1086;
+ public static final int BACKUP_ON_QUOTA_EXCEEDED = 1087;
+ public static final int BACKUP_ON_RESTORE = 1088;
+ public static final int BACKUP_RESTORED_FILE = 1089;
+ public static final int BACKUP_RESTORED_VOICEMAIL = 1090;
+ public static final int BACKUP_ON_RESTORE_FINISHED = 1091;
+ public static final int BACKUP_ON_RESTORE_DISABLED = 1092;
+ public static final int BACKUP_ON_RESTORE_JSON_EXCEPTION = 1093;
+ public static final int BACKUP_ON_RESTORE_IO_EXCEPTION = 1094;
+ public static final int BACKUP_MAX_VM_BACKUP_REACHED = 1095;
+ public static final int EVENT_ANSWER_HINT_ACTIVATED = 1096;
+ public static final int EVENT_ANSWER_HINT_DEACTIVATED = 1097;
+ public static final int VVM_TAB_VISIBLE = 1098;
+ public static final int VVM_SHARE_VISIBLE = 1099;
+ public static final int VVM_SHARE_PRESSED = 1100;
+ public static final int OUTGOING_VIDEO_CALL = 1101;
+ public static final int INCOMING_VIDEO_CALL = 1102;
+ public static final int USER_PARTICIPATED_IN_A_VIDEO_CALL = 1103;
+ public static final int BACKUP_ON_RESTORE_VM_DUPLICATE_NOT_RESTORING = 1104;
+ public static final int CALL_LOG_SHARE_AND_CALL = 1105;
+ public static final int CALL_COMPOSER_ACTIVITY_PLACE_RCS_CALL = 1106;
+ public static final int CALL_COMPOSER_ACTIVITY_SEND_AND_CALL_PRESSED_WHEN_SESSION_NOT_READY = 1107;
+ }
+
+ private static volatile DialerImpression[] _emptyArray;
+ public static DialerImpression[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new DialerImpression[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.DialerImpression)
+
+ public DialerImpression() {
+ clear();
+ }
+
+ public DialerImpression clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public DialerImpression mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static DialerImpression parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new DialerImpression(), data);
+ }
+
+ public static DialerImpression parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new DialerImpression().mergeFrom(input);
+ }
+}
+
diff --git a/java/com/android/dialer/logging/nano/InteractionEvent.java b/java/com/android/dialer/logging/nano/InteractionEvent.java
new file mode 100644
index 000000000..8d9430be9
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/InteractionEvent.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class InteractionEvent
+ extends com.google.protobuf.nano.ExtendableMessageNano<InteractionEvent> {
+
+ // enum Type
+ /** This file is autogenerated, but javadoc required. */
+ public interface Type {
+ public static final int UNKNOWN = 0;
+ public static final int CALL_BLOCKED = 15;
+ public static final int BLOCK_NUMBER_CALL_LOG = 16;
+ public static final int BLOCK_NUMBER_CALL_DETAIL = 17;
+ public static final int BLOCK_NUMBER_MANAGEMENT_SCREEN = 18;
+ public static final int UNBLOCK_NUMBER_CALL_LOG = 19;
+ public static final int UNBLOCK_NUMBER_CALL_DETAIL = 20;
+ public static final int UNBLOCK_NUMBER_MANAGEMENT_SCREEN = 21;
+ public static final int IMPORT_SEND_TO_VOICEMAIL = 22;
+ public static final int UNDO_BLOCK_NUMBER = 23;
+ public static final int UNDO_UNBLOCK_NUMBER = 24;
+ }
+
+ private static volatile InteractionEvent[] _emptyArray;
+ public static InteractionEvent[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new InteractionEvent[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.InteractionEvent)
+
+ public InteractionEvent() {
+ clear();
+ }
+
+ public InteractionEvent clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public InteractionEvent mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static InteractionEvent parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new InteractionEvent(), data);
+ }
+
+ public static InteractionEvent parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new InteractionEvent().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/ReportingLocation.java b/java/com/android/dialer/logging/nano/ReportingLocation.java
new file mode 100644
index 000000000..1f05ce414
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ReportingLocation.java
@@ -0,0 +1,87 @@
+/*
+ * 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
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+@SuppressWarnings("hiding")
+public final class ReportingLocation extends
+ com.google.protobuf.nano.ExtendableMessageNano<ReportingLocation> {
+
+ // enum Type
+ public interface Type {
+ public static final int UNKNOWN_REPORTING_LOCATION = 0;
+ public static final int CALL_LOG_HISTORY = 1;
+ public static final int FEEDBACK_PROMPT = 2;
+ }
+
+ private static volatile ReportingLocation[] _emptyArray;
+ public static ReportingLocation[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (
+ com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ReportingLocation[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ReportingLocation)
+
+ public ReportingLocation() {
+ clear();
+ }
+
+ public ReportingLocation clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ReportingLocation mergeFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default: {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ReportingLocation parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ReportingLocation(), data);
+ }
+
+ public static ReportingLocation parseFrom(
+ com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ReportingLocation().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/logging/nano/ScreenEvent.java b/java/com/android/dialer/logging/nano/ScreenEvent.java
new file mode 100644
index 000000000..be4e5eb9e
--- /dev/null
+++ b/java/com/android/dialer/logging/nano/ScreenEvent.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.
+ */
+
+// Generated by the protocol buffer compiler. DO NOT EDIT!
+
+package com.android.dialer.logging.nano;
+
+/** This file is autogenerated, but javadoc required. */
+@SuppressWarnings("hiding")
+public final class ScreenEvent extends com.google.protobuf.nano.ExtendableMessageNano<ScreenEvent> {
+
+ // enum Type
+ /** This file is autogenerated, but javadoc required. */
+ public interface Type {
+ public static final int UNKNOWN = 0;
+ public static final int DIALPAD = 1;
+ public static final int SPEED_DIAL = 2;
+ public static final int CALL_LOG = 3;
+ public static final int VOICEMAIL_LOG = 4;
+ public static final int ALL_CONTACTS = 5;
+ public static final int REGULAR_SEARCH = 6;
+ public static final int SMART_DIAL_SEARCH = 7;
+ public static final int CALL_LOG_FILTER = 8;
+ public static final int SETTINGS = 9;
+ public static final int IMPORT_EXPORT_CONTACTS = 10;
+ public static final int CLEAR_FREQUENTS = 11;
+ public static final int SEND_FEEDBACK = 12;
+ public static final int INCALL = 13;
+ public static final int INCOMING_CALL = 14;
+ public static final int CONFERENCE_MANAGEMENT = 15;
+ public static final int INCALL_DIALPAD = 16;
+ public static final int CALL_LOG_CONTEXT_MENU = 17;
+ public static final int BLOCKED_NUMBER_MANAGEMENT = 18;
+ public static final int BLOCKED_NUMBER_ADD_NUMBER = 19;
+ public static final int CALL_DETAILS = 20;
+ }
+
+ private static volatile ScreenEvent[] _emptyArray;
+ public static ScreenEvent[] emptyArray() {
+ // Lazily initializes the empty array
+ if (_emptyArray == null) {
+ synchronized (com.google.protobuf.nano.InternalNano.LAZY_INIT_LOCK) {
+ if (_emptyArray == null) {
+ _emptyArray = new ScreenEvent[0];
+ }
+ }
+ }
+ return _emptyArray;
+ }
+
+ // @@protoc_insertion_point(class_scope:com.android.dialer.logging.ScreenEvent)
+
+ public ScreenEvent() {
+ clear();
+ }
+
+ public ScreenEvent clear() {
+ unknownFieldData = null;
+ cachedSize = -1;
+ return this;
+ }
+
+ @Override
+ public ScreenEvent mergeFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ while (true) {
+ int tag = input.readTag();
+ switch (tag) {
+ case 0:
+ return this;
+ default:
+ {
+ if (!super.storeUnknownField(input, tag)) {
+ return this;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ public static ScreenEvent parseFrom(byte[] data)
+ throws com.google.protobuf.nano.InvalidProtocolBufferNanoException {
+ return com.google.protobuf.nano.MessageNano.mergeFrom(new ScreenEvent(), data);
+ }
+
+ public static ScreenEvent parseFrom(com.google.protobuf.nano.CodedInputByteBufferNano input)
+ throws java.io.IOException {
+ return new ScreenEvent().mergeFrom(input);
+ }
+}
diff --git a/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
new file mode 100644
index 000000000..cc6815094
--- /dev/null
+++ b/java/com/android/dialer/multimedia/AutoValue_MultimediaData.java
@@ -0,0 +1,165 @@
+/*
+ * 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.multimedia;
+
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_MultimediaData extends MultimediaData {
+
+ private final String subject;
+ private final Location location;
+ private final Uri imageUri;
+ private final String imageContentType;
+ private final boolean important;
+
+ private AutoValue_MultimediaData(
+ @Nullable String subject,
+ @Nullable Location location,
+ @Nullable Uri imageUri,
+ @Nullable String imageContentType,
+ boolean important) {
+ this.subject = subject;
+ this.location = location;
+ this.imageUri = imageUri;
+ this.imageContentType = imageContentType;
+ this.important = important;
+ }
+
+ @Nullable
+ @Override
+ public String getSubject() {
+ return subject;
+ }
+
+ @Nullable
+ @Override
+ public Location getLocation() {
+ return location;
+ }
+
+ @Nullable
+ @Override
+ public Uri getImageUri() {
+ return imageUri;
+ }
+
+ @Nullable
+ @Override
+ public String getImageContentType() {
+ return imageContentType;
+ }
+
+ @Override
+ public boolean isImportant() {
+ return important;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof MultimediaData) {
+ MultimediaData that = (MultimediaData) o;
+ return ((this.subject == null) ? (that.getSubject() == null) : this.subject.equals(that.getSubject()))
+ && ((this.location == null) ? (that.getLocation() == null) : this.location.equals(that.getLocation()))
+ && ((this.imageUri == null) ? (that.getImageUri() == null) : this.imageUri.equals(that.getImageUri()))
+ && ((this.imageContentType == null) ? (that.getImageContentType() == null) : this.imageContentType.equals(that.getImageContentType()))
+ && (this.important == that.isImportant());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (subject == null) ? 0 : this.subject.hashCode();
+ h *= 1000003;
+ h ^= (location == null) ? 0 : this.location.hashCode();
+ h *= 1000003;
+ h ^= (imageUri == null) ? 0 : this.imageUri.hashCode();
+ h *= 1000003;
+ h ^= (imageContentType == null) ? 0 : this.imageContentType.hashCode();
+ h *= 1000003;
+ h ^= this.important ? 1231 : 1237;
+ return h;
+ }
+
+ static final class Builder extends MultimediaData.Builder {
+ private String subject;
+ private Location location;
+ private Uri imageUri;
+ private String imageContentType;
+ private Boolean important;
+ Builder() {
+ }
+ private Builder(MultimediaData source) {
+ this.subject = source.getSubject();
+ this.location = source.getLocation();
+ this.imageUri = source.getImageUri();
+ this.imageContentType = source.getImageContentType();
+ this.important = source.isImportant();
+ }
+ @Override
+ public MultimediaData.Builder setSubject(@Nullable String subject) {
+ this.subject = subject;
+ return this;
+ }
+ @Override
+ public MultimediaData.Builder setLocation(@Nullable Location location) {
+ this.location = location;
+ return this;
+ }
+ @Override
+ MultimediaData.Builder setImageUri(@Nullable Uri imageUri) {
+ this.imageUri = imageUri;
+ return this;
+ }
+ @Override
+ MultimediaData.Builder setImageContentType(@Nullable String imageContentType) {
+ this.imageContentType = imageContentType;
+ return this;
+ }
+ @Override
+ public MultimediaData.Builder setImportant(boolean important) {
+ this.important = important;
+ return this;
+ }
+ @Override
+ public MultimediaData build() {
+ String missing = "";
+ if (this.important == null) {
+ missing += " important";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_MultimediaData(
+ this.subject,
+ this.location,
+ this.imageUri,
+ this.imageContentType,
+ this.important);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/multimedia/MultimediaData.java b/java/com/android/dialer/multimedia/MultimediaData.java
new file mode 100644
index 000000000..ebd41a918
--- /dev/null
+++ b/java/com/android/dialer/multimedia/MultimediaData.java
@@ -0,0 +1,100 @@
+/*
+ * 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.multimedia;
+
+import android.location.Location;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+
+
+/** Holds the data associated with an enriched call session. */
+
+public abstract class MultimediaData {
+
+ public static final MultimediaData EMPTY = builder().build();
+
+ @NonNull
+ public static Builder builder() {
+ return new AutoValue_MultimediaData.Builder().setImportant(false);
+ }
+
+ /** Returns the call composer subject if set, or null if this isn't a call composer session. */
+ @Nullable
+ public abstract String getSubject();
+
+ /** Returns the call composer location if set, or null if this isn't a call composer session. */
+ @Nullable
+ public abstract Location getLocation();
+
+ /** Returns {@code true} if this session contains image data. */
+ public boolean hasImageData() {
+ // imageUri and content are always either both null or nonnull
+ return getImageUri() != null && getImageContentType() != null;
+ }
+
+ /** Returns the call composer photo if set, or null if this isn't a call composer session. */
+ @Nullable
+ public abstract Uri getImageUri();
+
+ /**
+ * Returns the content type of the image, either image/png or image/jpeg, if set, or null if this
+ * isn't a call composer session.
+ */
+ @Nullable
+ public abstract String getImageContentType();
+
+ /** Returns {@code true} if this is a call composer session that's marked as important. */
+ public abstract boolean isImportant();
+
+ /** Returns the string form of this MultimediaData with no PII. */
+ @Override
+ public String toString() {
+ return String.format(
+ "MultimediaData{subject: %s, location: %s, imageUrl: %s, imageContentType: %s, "
+ + "important: %b}",
+ LogUtil.sanitizePii(getSubject()),
+ LogUtil.sanitizePii(getLocation()),
+ LogUtil.sanitizePii(getImageUri()),
+ getImageContentType(),
+ isImportant());
+ }
+
+ /** Creates instances of {@link MultimediaData}. */
+
+ public abstract static class Builder {
+
+ public abstract Builder setSubject(@NonNull String subject);
+
+ public abstract Builder setLocation(@NonNull Location location);
+
+ public Builder setImage(@NonNull Uri image, @NonNull String imageContentType) {
+ setImageUri(image);
+ setImageContentType(imageContentType);
+ return this;
+ }
+
+ abstract Builder setImageUri(@NonNull Uri image);
+
+ abstract Builder setImageContentType(@NonNull String imageContentType);
+
+ public abstract Builder setImportant(boolean isImportant);
+
+ public abstract MultimediaData build();
+ }
+}
diff --git a/java/com/android/dialer/p13n/inference/P13nRanking.java b/java/com/android/dialer/p13n/inference/P13nRanking.java
new file mode 100644
index 000000000..6bfc0352a
--- /dev/null
+++ b/java/com/android/dialer/p13n/inference/P13nRanking.java
@@ -0,0 +1,75 @@
+/*
+ * 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.p13n.inference;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.p13n.inference.protocol.P13nRanker;
+import com.android.dialer.p13n.inference.protocol.P13nRankerFactory;
+import java.util.List;
+
+/** Single entry point for all personalized ranking. */
+public final class P13nRanking {
+
+ private static P13nRanker ranker;
+
+ private P13nRanking() {}
+
+ @MainThread
+ @NonNull
+ public static P13nRanker get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ Assert.isMainThread();
+ if (ranker != null) {
+ return ranker;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof P13nRankerFactory) {
+ ranker = ((P13nRankerFactory) application).newP13nRanker();
+ }
+
+ if (ranker == null) {
+ ranker =
+ new P13nRanker() {
+ @Override
+ public void refresh(@Nullable P13nRefreshCompleteListener listener) {}
+
+ @Override
+ public List<String> rankList(List<String> phoneNumbers) {
+ return phoneNumbers;
+ }
+
+ @NonNull
+ @Override
+ public Cursor rankCursor(
+ @NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex) {
+ return phoneQueryResults;
+ }
+ };
+ }
+ return ranker;
+ }
+
+ public static void setForTesting(@NonNull P13nRanker ranker) {
+ P13nRanking.ranker = ranker;
+ }
+}
diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
new file mode 100644
index 000000000..9a859a6db
--- /dev/null
+++ b/java/com/android/dialer/p13n/inference/protocol/P13nRanker.java
@@ -0,0 +1,75 @@
+/*
+ * 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.p13n.inference.protocol;
+
+import android.database.Cursor;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.List;
+
+/** Provides personalized ranking of outgoing call targets. */
+public interface P13nRanker {
+
+ /**
+ * Re-orders a list of phone numbers according to likelihood they will be the next outgoing call.
+ *
+ * @param phoneNumbers the list of candidate numbers to call (may be in contacts list or not)
+ */
+ @NonNull
+ @MainThread
+ List<String> rankList(@NonNull List<String> phoneNumbers);
+
+ /**
+ * Re-orders a retrieved contact list according to likelihood they will be the next outgoing call.
+ *
+ * <p>A new cursor with reordered data is returned; the input cursor is unmodified except for its
+ * position. If the order is unchanged, this method may return a reference to the unmodified input
+ * cursor directly. The order would be unchanged if the ranking cache is not yet ready, or if the
+ * input cursor is closed or invalid, or if any other error occurs in the ranking process.
+ *
+ * @param phoneQueryResults cursor of results of a Dialer search query
+ * @param phoneNumberColumnIndex column index of the phone number in the cursor data
+ * @return new cursor of data reordered by ranking (or reference to input cursor if order
+ * unchanged)
+ */
+ @NonNull
+ @MainThread
+ Cursor rankCursor(@NonNull Cursor phoneQueryResults, int phoneNumberColumnIndex);
+
+ /**
+ * Refreshes ranking cache (pulls fresh contextual features, pre-caches inference results, etc.).
+ *
+ * <p>Asynchronously runs in background as the process might take a few seconds, notifying a
+ * listener upon completion; meanwhile, any calls to {@link #rankList} will simply return the
+ * input in same order.
+ *
+ * @param listener callback for when ranking refresh has completed; null value skips notification.
+ */
+ @MainThread
+ void refresh(@Nullable P13nRefreshCompleteListener listener);
+
+ /**
+ * Callback class for when ranking refresh has completed.
+ *
+ * <p>Primary use is to notify {@link com.android.dialer.app.DialtactsActivity} that the ranking
+ * functions {@link #rankList} and {@link #rankCursor(Cursor, int)} will now give useful results.
+ */
+ interface P13nRefreshCompleteListener {
+
+ /** Callback for when ranking refresh has completed. */
+ void onP13nRefreshComplete();
+ }
+}
diff --git a/java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java b/java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java
new file mode 100644
index 000000000..7038cf456
--- /dev/null
+++ b/java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.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.p13n.inference.protocol;
+
+import android.support.annotation.Nullable;
+
+/**
+ * This interface should be implemented by the Application subclass. It allows this module to get
+ * references to the {@link P13nRanker}.
+ */
+public interface P13nRankerFactory {
+ @Nullable
+ P13nRanker newP13nRanker();
+}
diff --git a/java/com/android/dialer/p13n/logging/P13nLogger.java b/java/com/android/dialer/p13n/logging/P13nLogger.java
new file mode 100644
index 000000000..069a29328
--- /dev/null
+++ b/java/com/android/dialer/p13n/logging/P13nLogger.java
@@ -0,0 +1,35 @@
+/*
+ * 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.p13n.logging;
+
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+
+/** Allows logging of data for personalization. */
+public interface P13nLogger {
+
+ /**
+ * Logs a search query (text or digits) entered by user.
+ *
+ * @param query search text (or digits) entered by user
+ * @param adapter list adapter providing access to contacts matching search query
+ */
+ void onSearchQuery(String query, PhoneNumberListAdapter adapter);
+
+ /**
+ * Resets logging session (clears searches, re-initializes app entry timestamp, etc.) Should be
+ * called when Dialer app is resumed.
+ */
+ void reset();
+}
diff --git a/java/com/android/dialer/p13n/logging/P13nLoggerFactory.java b/java/com/android/dialer/p13n/logging/P13nLoggerFactory.java
new file mode 100644
index 000000000..7350e99e1
--- /dev/null
+++ b/java/com/android/dialer/p13n/logging/P13nLoggerFactory.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.dialer.p13n.logging;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/**
+ * This interface should be implemented by the Application subclass. It allows this module to get
+ * references to the P13nLogger.
+ */
+public interface P13nLoggerFactory {
+
+ @Nullable
+ P13nLogger newP13nLogger(@NonNull Context context);
+}
diff --git a/java/com/android/dialer/p13n/logging/P13nLogging.java b/java/com/android/dialer/p13n/logging/P13nLogging.java
new file mode 100644
index 000000000..21b97257b
--- /dev/null
+++ b/java/com/android/dialer/p13n/logging/P13nLogging.java
@@ -0,0 +1,60 @@
+/*
+ * 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.p13n.logging;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.contacts.common.list.PhoneNumberListAdapter;
+import com.android.dialer.common.Assert;
+
+/** Single entry point for all logging for personalization. */
+public final class P13nLogging {
+
+ private static P13nLogger logger;
+
+ private P13nLogging() {}
+
+ @NonNull
+ public static P13nLogger get(@NonNull Context context) {
+ Assert.isNotNull(context);
+ Assert.isMainThread();
+ if (logger != null) {
+ return logger;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof P13nLoggerFactory) {
+ logger = ((P13nLoggerFactory) application).newP13nLogger(context);
+ }
+
+ if (logger == null) {
+ logger =
+ new P13nLogger() {
+ @Override
+ public void onSearchQuery(String query, PhoneNumberListAdapter adapter) {}
+
+ @Override
+ public void reset() {}
+ };
+ }
+ return logger;
+ }
+
+ public static void setForTesting(@NonNull P13nLogger logger) {
+ P13nLogging.logger = logger;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java
new file mode 100644
index 000000000..03b77b91c
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/CachedNumberLookupService.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.phonenumbercache;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import java.io.InputStream;
+
+public interface CachedNumberLookupService {
+
+ CachedContactInfo buildCachedContactInfo(ContactInfo info);
+
+ /**
+ * Perform a lookup using the cached number lookup service to return contact information stored in
+ * the cache that corresponds to the given number.
+ *
+ * @param context Valid context
+ * @param number Phone number to lookup the cache for
+ * @return A {@link CachedContactInfo} containing the contact information if the phone number is
+ * found in the cache, {@link ContactInfo#EMPTY} if the phone number was not found in the
+ * cache, and null if there was an error when querying the cache.
+ */
+ CachedContactInfo lookupCachedContactFromNumber(Context context, String number);
+
+ void addContact(Context context, CachedContactInfo info);
+
+ boolean isCacheUri(String uri);
+
+ boolean isBusiness(int sourceType);
+
+ boolean canReportAsInvalid(int sourceType, String objectId);
+
+ /** @return return {@link Uri} to the photo or return {@code null} when failing to add photo */
+ @Nullable
+ Uri addPhoto(Context context, String number, InputStream in);
+
+ /**
+ * Remove all cached phone number entries from the cache, regardless of how old they are.
+ *
+ * @param context Valid context
+ */
+ void clearAllCacheEntries(Context context);
+
+ interface CachedContactInfo {
+
+ int SOURCE_TYPE_DIRECTORY = 1;
+ int SOURCE_TYPE_EXTENDED = 2;
+ int SOURCE_TYPE_PLACES = 3;
+ int SOURCE_TYPE_PROFILE = 4;
+ int SOURCE_TYPE_CNAP = 5;
+
+ ContactInfo getContactInfo();
+
+ void setSource(int sourceType, String name, long directoryId);
+
+ void setDirectorySource(String name, long directoryId);
+
+ void setExtendedSource(String name, long directoryId);
+
+ void setLookupKey(String lookupKey);
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/CallLogQuery.java b/java/com/android/dialer/phonenumbercache/CallLogQuery.java
new file mode 100644
index 000000000..6d4756927
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/CallLogQuery.java
@@ -0,0 +1,107 @@
+/*
+ * 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.phonenumbercache;
+
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** The query for the call log table. */
+public final class CallLogQuery {
+
+ public static final int ID = 0;
+ public static final int NUMBER = 1;
+ public static final int DATE = 2;
+ public static final int DURATION = 3;
+ public static final int CALL_TYPE = 4;
+ public static final int COUNTRY_ISO = 5;
+ public static final int VOICEMAIL_URI = 6;
+ public static final int GEOCODED_LOCATION = 7;
+ public static final int CACHED_NAME = 8;
+ public static final int CACHED_NUMBER_TYPE = 9;
+ public static final int CACHED_NUMBER_LABEL = 10;
+ public static final int CACHED_LOOKUP_URI = 11;
+ public static final int CACHED_MATCHED_NUMBER = 12;
+ public static final int CACHED_NORMALIZED_NUMBER = 13;
+ public static final int CACHED_PHOTO_ID = 14;
+ public static final int CACHED_FORMATTED_NUMBER = 15;
+ public static final int IS_READ = 16;
+ public static final int NUMBER_PRESENTATION = 17;
+ public static final int ACCOUNT_COMPONENT_NAME = 18;
+ public static final int ACCOUNT_ID = 19;
+ public static final int FEATURES = 20;
+ public static final int DATA_USAGE = 21;
+ public static final int TRANSCRIPTION = 22;
+ public static final int CACHED_PHOTO_URI = 23;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int POST_DIAL_DIGITS = 24;
+
+ @RequiresApi(VERSION_CODES.N)
+ public static final int VIA_NUMBER = 25;
+
+ private static final String[] PROJECTION_M =
+ new String[] {
+ Calls._ID, // 0
+ Calls.NUMBER, // 1
+ Calls.DATE, // 2
+ Calls.DURATION, // 3
+ Calls.TYPE, // 4
+ Calls.COUNTRY_ISO, // 5
+ Calls.VOICEMAIL_URI, // 6
+ Calls.GEOCODED_LOCATION, // 7
+ Calls.CACHED_NAME, // 8
+ Calls.CACHED_NUMBER_TYPE, // 9
+ Calls.CACHED_NUMBER_LABEL, // 10
+ Calls.CACHED_LOOKUP_URI, // 11
+ Calls.CACHED_MATCHED_NUMBER, // 12
+ Calls.CACHED_NORMALIZED_NUMBER, // 13
+ Calls.CACHED_PHOTO_ID, // 14
+ Calls.CACHED_FORMATTED_NUMBER, // 15
+ Calls.IS_READ, // 16
+ Calls.NUMBER_PRESENTATION, // 17
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME, // 18
+ Calls.PHONE_ACCOUNT_ID, // 19
+ Calls.FEATURES, // 20
+ Calls.DATA_USAGE, // 21
+ Calls.TRANSCRIPTION, // 22
+ Calls.CACHED_PHOTO_URI, // 23
+ };
+
+ private static final String[] PROJECTION_N;
+
+ static {
+ List<String> projectionList = new ArrayList<>(Arrays.asList(PROJECTION_M));
+ projectionList.add(CallLog.Calls.POST_DIAL_DIGITS);
+ projectionList.add(CallLog.Calls.VIA_NUMBER);
+ PROJECTION_N = projectionList.toArray(new String[projectionList.size()]);
+ }
+
+ @NonNull
+ public static String[] getProjection() {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PROJECTION_N;
+ }
+ return PROJECTION_M;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfo.java b/java/com/android/dialer/phonenumbercache/ContactInfo.java
new file mode 100644
index 000000000..d7a75c34f
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/ContactInfo.java
@@ -0,0 +1,165 @@
+/*
+ * 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.phonenumbercache;
+
+import android.net.Uri;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.util.UriUtils;
+
+/** Information for a contact as needed by the Call Log. */
+public class ContactInfo {
+
+ public static final ContactInfo EMPTY = new ContactInfo();
+ public Uri lookupUri;
+ /**
+ * Contact lookup key. Note this may be a lookup key for a corp contact, in which case "lookup by
+ * lookup key" doesn't work on the personal profile.
+ */
+ public String lookupKey;
+
+ public String name;
+ public String nameAlternative;
+ public int type;
+ public String label;
+ public String number;
+ public String formattedNumber;
+ /*
+ * ContactInfo.normalizedNumber is a column value returned by PhoneLookup query. By definition,
+ * it's E164 representation.
+ * http://developer.android.com/reference/android/provider/ContactsContract.PhoneLookupColumns.
+ * html#NORMALIZED_NUMBER.
+ *
+ * The fallback value, when PhoneLookup fails or else, should be either null or
+ * PhoneNumberUtils.formatNumberToE164.
+ */
+ public String normalizedNumber;
+ /** The photo for the contact, if available. */
+ public long photoId;
+ /** The high-res photo for the contact, if available. */
+ public Uri photoUri;
+
+ public boolean isBadData;
+ public String objectId;
+ public @UserType long userType;
+ public int sourceType = 0;
+
+ /** @see android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE */
+ public int carrierPresence;
+
+ @Override
+ public int hashCode() {
+ // Uses only name and contactUri to determine hashcode.
+ // This should be sufficient to have a reasonable distribution of hash codes.
+ // Moreover, there should be no two people with the same lookupUri.
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((lookupUri == null) ? 0 : lookupUri.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.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;
+ }
+ ContactInfo other = (ContactInfo) obj;
+ if (!UriUtils.areEqual(lookupUri, other.lookupUri)) {
+ return false;
+ }
+ if (!TextUtils.equals(name, other.name)) {
+ return false;
+ }
+ if (!TextUtils.equals(nameAlternative, other.nameAlternative)) {
+ return false;
+ }
+ if (type != other.type) {
+ return false;
+ }
+ if (!TextUtils.equals(label, other.label)) {
+ return false;
+ }
+ if (!TextUtils.equals(number, other.number)) {
+ return false;
+ }
+ if (!TextUtils.equals(formattedNumber, other.formattedNumber)) {
+ return false;
+ }
+ if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) {
+ return false;
+ }
+ if (photoId != other.photoId) {
+ return false;
+ }
+ if (!UriUtils.areEqual(photoUri, other.photoUri)) {
+ return false;
+ }
+ if (!TextUtils.equals(objectId, other.objectId)) {
+ return false;
+ }
+ if (userType != other.userType) {
+ return false;
+ }
+ return carrierPresence == other.carrierPresence;
+ }
+
+ @Override
+ public String toString() {
+ return "ContactInfo{"
+ + "lookupUri="
+ + lookupUri
+ + ", name='"
+ + name
+ + '\''
+ + ", nameAlternative='"
+ + nameAlternative
+ + '\''
+ + ", type="
+ + type
+ + ", label='"
+ + label
+ + '\''
+ + ", number='"
+ + number
+ + '\''
+ + ", formattedNumber='"
+ + formattedNumber
+ + '\''
+ + ", normalizedNumber='"
+ + normalizedNumber
+ + '\''
+ + ", photoId="
+ + photoId
+ + ", photoUri="
+ + photoUri
+ + ", objectId='"
+ + objectId
+ + '\''
+ + ", userType="
+ + userType
+ + ", carrierPresence="
+ + carrierPresence
+ + '}';
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
new file mode 100644
index 000000000..6a5e2e6b4
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/ContactInfoHelper.java
@@ -0,0 +1,586 @@
+/*
+ * 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.phonenumbercache;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.contacts.common.util.Constants;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+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.List;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/** Utility class to look up the contact information for a given number. */
+// This class uses Java 7 language features, so it must target M+
+@TargetApi(VERSION_CODES.M)
+public class ContactInfoHelper {
+
+ private static final String TAG = ContactInfoHelper.class.getSimpleName();
+
+ private final Context mContext;
+ private final String mCurrentCountryIso;
+ private final CachedNumberLookupService mCachedNumberLookupService;
+
+ public ContactInfoHelper(Context context, String currentCountryIso) {
+ mContext = context;
+ mCurrentCountryIso = currentCountryIso;
+ mCachedNumberLookupService = PhoneNumberCache.get(mContext).getCachedNumberLookupService();
+ }
+
+ /**
+ * Creates a JSON-encoded lookup uri for a unknown number without an associated contact
+ *
+ * @param number - Unknown phone number
+ * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick
+ * contact card.
+ */
+ private static Uri createTemporaryContactUri(String number) {
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject().put(Phone.NUMBER, number).put(Phone.TYPE, Phone.TYPE_CUSTOM));
+
+ final String jsonString =
+ new JSONObject()
+ .put(Contacts.DISPLAY_NAME, number)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.PHONE)
+ .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
+ .toString();
+
+ return Contacts.CONTENT_LOOKUP_URI
+ .buildUpon()
+ .appendPath(Constants.LOOKUP_URI_ENCODED)
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Long.MAX_VALUE))
+ .encodedFragment(jsonString)
+ .build();
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ public static String lookUpDisplayNameAlternative(
+ Context context, String lookupKey, @UserType long userType, @Nullable Long directoryId) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ if (lookupKey == null || userType == ContactsUtils.USER_TYPE_WORK) {
+ return null;
+ }
+
+ if (directoryId != null) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} with work lookup key is not allowed.
+ if (DirectoryCompat.isEnterpriseDirectoryId(directoryId)) {
+ return null;
+ }
+
+ // Skip this to avoid an extra remote network call for alternative name
+ if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ return null;
+ }
+ }
+
+ final Uri uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+ Cursor cursor = null;
+ try {
+ cursor =
+ context
+ .getContentResolver()
+ .query(uri, PhoneQuery.DISPLAY_NAME_ALTERNATIVE_PROJECTION, null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getString(PhoneQuery.NAME_ALTERNATIVE);
+ }
+ } catch (IllegalArgumentException e) {
+ // Avoid dialer crash when lookup key is not valid
+ Log.e(TAG, "IllegalArgumentException in lookUpDisplayNameAlternative", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return null;
+ }
+
+ public static Uri getContactInfoLookupUri(String number) {
+ return getContactInfoLookupUri(number, -1);
+ }
+
+ public static Uri getContactInfoLookupUri(String number, long directoryId) {
+ // Get URI for the number in the PhoneLookup table, with a parameter to indicate whether
+ // the number is a SIP number.
+ Uri uri = PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
+ if (VERSION.SDK_INT < VERSION_CODES.N) {
+ if (directoryId != -1) {
+ // ENTERPRISE_CONTENT_FILTER_URI in M doesn't support directory lookup
+ uri = PhoneLookup.CONTENT_FILTER_URI;
+ } else {
+ // b/25900607 in M. PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, encodes twice.
+ number = Uri.encode(number);
+ }
+ }
+ Uri.Builder builder =
+ uri.buildUpon()
+ .appendPath(number)
+ .appendQueryParameter(
+ PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS,
+ String.valueOf(PhoneNumberHelper.isUriNumber(number)));
+ if (directoryId != -1) {
+ builder.appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Returns the contact information stored in an entry of the call log.
+ *
+ * @param c A cursor pointing to an entry in the call log.
+ */
+ public static ContactInfo getContactInfo(Cursor c) {
+ ContactInfo info = new ContactInfo();
+ info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
+ info.name = c.getString(CallLogQuery.CACHED_NAME);
+ info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+ info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+ String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
+ String postDialDigits =
+ (VERSION.SDK_INT >= VERSION_CODES.N) ? c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
+ info.number =
+ (matchedNumber == null) ? c.getString(CallLogQuery.NUMBER) + postDialDigits : matchedNumber;
+
+ info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
+ info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
+ info.photoUri =
+ UriUtils.nullForNonContactsUri(
+ UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_PHOTO_URI)));
+ info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
+
+ return info;
+ }
+
+ public ContactInfo lookupNumber(String number, String countryIso) {
+ return lookupNumber(number, countryIso, -1);
+ }
+
+ /**
+ * Returns the contact information for the given number.
+ *
+ * <p>If the number does not match any contact, returns a contact info containing only the number
+ * and the formatted number.
+ *
+ * <p>If an error occurs during the lookup, it returns null.
+ *
+ * @param number the number to look up
+ * @param countryIso the country associated with this number
+ * @param directoryId the id of the directory to lookup
+ */
+ @Nullable
+ @SuppressWarnings("ReferenceEquality")
+ public ContactInfo lookupNumber(String number, String countryIso, long directoryId) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ ContactInfo info;
+
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ // The number is a SIP address..
+ info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
+ if (info == null || info == ContactInfo.EMPTY) {
+ // If lookup failed, check if the "username" of the SIP address is a phone number.
+ String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ info = queryContactInfoForPhoneNumber(username, countryIso, directoryId);
+ }
+ }
+ } else {
+ // Look for a contact that has the given phone number.
+ info = queryContactInfoForPhoneNumber(number, countryIso, directoryId);
+ }
+
+ final ContactInfo updatedInfo;
+ if (info == null) {
+ // The lookup failed.
+ updatedInfo = null;
+ } else {
+ // If we did not find a matching contact, generate an empty contact info for the number.
+ if (info == ContactInfo.EMPTY) {
+ // Did not find a matching contact.
+ updatedInfo = createEmptyContactInfoForNumber(number, countryIso);
+ } else {
+ updatedInfo = info;
+ }
+ }
+ return updatedInfo;
+ }
+
+ private ContactInfo createEmptyContactInfoForNumber(String number, String countryIso) {
+ ContactInfo contactInfo = new ContactInfo();
+ contactInfo.number = number;
+ contactInfo.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ contactInfo.lookupUri = createTemporaryContactUri(contactInfo.formattedNumber);
+ return contactInfo;
+ }
+
+ /**
+ * Return the contact info object if the remote directory lookup succeeds, otherwise return an
+ * empty contact info for the number.
+ */
+ public ContactInfo lookupNumberInRemoteDirectory(String number, String countryIso) {
+ if (mCachedNumberLookupService != null) {
+ List<Long> remoteDirectories = getRemoteDirectories(mContext);
+ for (long directoryId : remoteDirectories) {
+ ContactInfo contactInfo = lookupNumber(number, countryIso, directoryId);
+ if (hasName(contactInfo)) {
+ return contactInfo;
+ }
+ }
+ }
+ return createEmptyContactInfoForNumber(number, countryIso);
+ }
+
+ public boolean hasName(ContactInfo contactInfo) {
+ return contactInfo != null && !TextUtils.isEmpty(contactInfo.name);
+ }
+
+ private List<Long> getRemoteDirectories(Context context) {
+ List<Long> remoteDirectories = new ArrayList<>();
+ Uri uri =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ ? Directory.ENTERPRISE_CONTENT_URI
+ : Directory.CONTENT_URI;
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(uri, new String[] {Directory._ID}, null, null, null);
+ int idIndex = cursor.getColumnIndex(Directory._ID);
+ if (cursor == null) {
+ return remoteDirectories;
+ }
+ try {
+ while (cursor.moveToNext()) {
+ long directoryId = cursor.getLong(idIndex);
+ if (DirectoryCompat.isRemoteDirectoryId(directoryId)) {
+ remoteDirectories.add(directoryId);
+ }
+ }
+ } finally {
+ cursor.close();
+ }
+ return remoteDirectories;
+ }
+
+ /**
+ * Looks up a contact using the given URI.
+ *
+ * <p>It returns null if an error occurs, {@link ContactInfo#EMPTY} if no matching contact is
+ * found, or the {@link ContactInfo} for the given contact.
+ *
+ * <p>The {@link ContactInfo#formattedNumber} field is always set to {@code null} in the returned
+ * value.
+ */
+ ContactInfo lookupContactFromUri(Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+ if (!PermissionsUtil.hasContactsPermissions(mContext)) {
+ return ContactInfo.EMPTY;
+ }
+
+ Cursor phoneLookupCursor = null;
+ try {
+ String[] projection = PhoneQuery.getPhoneLookupProjection(uri);
+ phoneLookupCursor = mContext.getContentResolver().query(uri, projection, null, null, null);
+ } catch (NullPointerException e) {
+ // Trap NPE from pre-N CP2
+ return null;
+ }
+ if (phoneLookupCursor == null) {
+ return null;
+ }
+
+ try {
+ if (!phoneLookupCursor.moveToFirst()) {
+ return ContactInfo.EMPTY;
+ }
+ String lookupKey = phoneLookupCursor.getString(PhoneQuery.LOOKUP_KEY);
+ ContactInfo contactInfo = createPhoneLookupContactInfo(phoneLookupCursor, lookupKey);
+ fillAdditionalContactInfo(mContext, contactInfo);
+ return contactInfo;
+ } finally {
+ phoneLookupCursor.close();
+ }
+ }
+
+ private ContactInfo createPhoneLookupContactInfo(Cursor phoneLookupCursor, String lookupKey) {
+ ContactInfo info = new ContactInfo();
+ info.lookupKey = lookupKey;
+ info.lookupUri =
+ Contacts.getLookupUri(phoneLookupCursor.getLong(PhoneQuery.PERSON_ID), lookupKey);
+ info.name = phoneLookupCursor.getString(PhoneQuery.NAME);
+ info.type = phoneLookupCursor.getInt(PhoneQuery.PHONE_TYPE);
+ info.label = phoneLookupCursor.getString(PhoneQuery.LABEL);
+ info.number = phoneLookupCursor.getString(PhoneQuery.MATCHED_NUMBER);
+ info.normalizedNumber = phoneLookupCursor.getString(PhoneQuery.NORMALIZED_NUMBER);
+ info.photoId = phoneLookupCursor.getLong(PhoneQuery.PHOTO_ID);
+ info.photoUri = UriUtils.parseUriOrNull(phoneLookupCursor.getString(PhoneQuery.PHOTO_URI));
+ info.formattedNumber = null;
+ info.userType =
+ ContactsUtils.determineUserType(null, phoneLookupCursor.getLong(PhoneQuery.PERSON_ID));
+
+ return info;
+ }
+
+ private void fillAdditionalContactInfo(Context context, ContactInfo contactInfo) {
+ if (contactInfo.number == null) {
+ return;
+ }
+ Uri uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(contactInfo.number));
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(uri, PhoneQuery.ADDITIONAL_CONTACT_INFO_PROJECTION, null, null, null)) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ return;
+ }
+ contactInfo.nameAlternative =
+ cursor.getString(PhoneQuery.ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE);
+ contactInfo.carrierPresence =
+ cursor.getInt(PhoneQuery.ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE);
+ }
+ }
+
+ /**
+ * Determines the contact information for the given phone number.
+ *
+ * <p>It returns the contact info if found.
+ *
+ * <p>If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+ *
+ * <p>If the lookup fails for some other reason, it returns null.
+ */
+ @SuppressWarnings("ReferenceEquality")
+ private ContactInfo queryContactInfoForPhoneNumber(
+ String number, String countryIso, long directoryId) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ ContactInfo info = lookupContactFromUri(getContactInfoLookupUri(number, directoryId));
+ if (info != null && info != ContactInfo.EMPTY) {
+ info.formattedNumber = formatPhoneNumber(number, null, countryIso);
+ } else if (mCachedNumberLookupService != null) {
+ CachedContactInfo cacheInfo =
+ mCachedNumberLookupService.lookupCachedContactFromNumber(mContext, number);
+ if (cacheInfo != null) {
+ info = cacheInfo.getContactInfo().isBadData ? null : cacheInfo.getContactInfo();
+ } else {
+ info = null;
+ }
+ }
+ return info;
+ }
+
+ /**
+ * Format the given phone number
+ *
+ * @param number the number to be formatted.
+ * @param normalizedNumber the normalized number of the given number.
+ * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be
+ * used to format the number if the normalized phone is null.
+ * @return the formatted number, or the given number if it was formatted.
+ */
+ private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
+ if (TextUtils.isEmpty(number)) {
+ return "";
+ }
+ // If "number" is really a SIP address, don't try to do any formatting at all.
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ return number;
+ }
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = mCurrentCountryIso;
+ }
+ return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+ }
+
+ /**
+ * Stores differences between the updated contact info and the current call log contact info.
+ *
+ * @param number The number of the contact.
+ * @param countryIso The country associated with this number.
+ * @param updatedInfo The updated contact info.
+ * @param callLogInfo The call log entry's current contact info.
+ */
+ public void updateCallLogContactInfo(
+ String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) {
+ if (!PermissionsUtil.hasPermission(mContext, android.Manifest.permission.WRITE_CALL_LOG)) {
+ return;
+ }
+
+ final ContentValues values = new ContentValues();
+ boolean needsUpdate = false;
+
+ if (callLogInfo != null) {
+ if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.type != callLogInfo.type) {
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ needsUpdate = true;
+ }
+
+ if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ needsUpdate = true;
+ }
+
+ // Only replace the normalized number if the new updated normalized number isn't empty.
+ if (!TextUtils.isEmpty(updatedInfo.normalizedNumber)
+ && !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ needsUpdate = true;
+ }
+
+ if (updatedInfo.photoId != callLogInfo.photoId) {
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ needsUpdate = true;
+ }
+
+ final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(updatedInfo.photoUri);
+ if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, callLogInfo.photoUri)) {
+ values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly));
+ needsUpdate = true;
+ }
+
+ if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+ } else {
+ // No previous values, store all of them.
+ values.put(Calls.CACHED_NAME, updatedInfo.name);
+ values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
+ values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
+ values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
+ values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
+ values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
+ values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
+ values.put(
+ Calls.CACHED_PHOTO_URI,
+ UriUtils.uriToString(UriUtils.nullForNonContactsUri(updatedInfo.photoUri)));
+ values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
+ needsUpdate = true;
+ }
+
+ if (!needsUpdate) {
+ return;
+ }
+
+ try {
+ if (countryIso == null) {
+ mContext
+ .getContentResolver()
+ .update(
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
+ new String[] {number});
+ } else {
+ mContext
+ .getContentResolver()
+ .update(
+ TelecomUtil.getCallLogUri(mContext),
+ values,
+ Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
+ new String[] {number, countryIso});
+ }
+ } catch (SQLiteFullException e) {
+ Log.e(TAG, "Unable to update contact info in call log db", e);
+ }
+ }
+
+ public void updateCachedNumberLookupService(ContactInfo updatedInfo) {
+ if (mCachedNumberLookupService != null) {
+ if (hasName(updatedInfo)) {
+ CachedContactInfo cachedContactInfo =
+ mCachedNumberLookupService.buildCachedContactInfo(updatedInfo);
+ mCachedNumberLookupService.addContact(mContext, cachedContactInfo);
+ }
+ }
+ }
+
+ /**
+ * Given a contact's sourceType, return true if the contact is a business
+ *
+ * @param sourceType sourceType of the contact. This is usually populated by {@link
+ * #mCachedNumberLookupService}.
+ */
+ public boolean isBusiness(int sourceType) {
+ return mCachedNumberLookupService != null && mCachedNumberLookupService.isBusiness(sourceType);
+ }
+
+ /**
+ * This function looks at a contact's source and determines if the user can mark caller ids from
+ * this source as invalid.
+ *
+ * @param sourceType The source type to be checked
+ * @param objectId The ID of the Contact object.
+ * @return true if contacts from this source can be marked with an invalid caller id
+ */
+ public boolean canReportAsInvalid(int sourceType, String objectId) {
+ return mCachedNumberLookupService != null
+ && mCachedNumberLookupService.canReportAsInvalid(sourceType, objectId);
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java b/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java
new file mode 100644
index 000000000..74175e8ba
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java
@@ -0,0 +1,40 @@
+/*
+ * 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.phonenumbercache;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.PhoneLookup;
+
+public final class PhoneLookupUtil {
+
+ private PhoneLookupUtil() {}
+
+ /** @return the column name that stores contact id for phone lookup query. */
+ public static String getContactIdColumnNameForUri(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PhoneLookup.CONTACT_ID;
+ }
+ // In pre-N, contact id is stored in {@link PhoneLookup#_ID} in non-sip query.
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PhoneLookup.CONTACT_ID : ContactsContract.PhoneLookup._ID;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCache.java
new file mode 100644
index 000000000..aefa544cb
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCache.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.phonenumbercache;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the phone number cache bindings. */
+public class PhoneNumberCache {
+
+ private static PhoneNumberCacheBindings phoneNumberCacheBindings;
+
+ private PhoneNumberCache() {}
+
+ public static PhoneNumberCacheBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (phoneNumberCacheBindings != null) {
+ return phoneNumberCacheBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof PhoneNumberCacheBindingsFactory) {
+ phoneNumberCacheBindings =
+ ((PhoneNumberCacheBindingsFactory) application).newPhoneNumberCacheBindings();
+ }
+
+ if (phoneNumberCacheBindings == null) {
+ phoneNumberCacheBindings = new PhoneNumberCacheBindingsStub();
+ }
+ return phoneNumberCacheBindings;
+ }
+
+ public static void setForTesting(PhoneNumberCacheBindings phoneNumberCacheBindings) {
+ PhoneNumberCache.phoneNumberCacheBindings = phoneNumberCacheBindings;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java
new file mode 100644
index 000000000..6e3ed9d06
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.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.phonenumbercache;
+
+import android.support.annotation.Nullable;
+
+/** Allows the container application provide a number look up service. */
+public interface PhoneNumberCacheBindings {
+
+ @Nullable
+ CachedNumberLookupService getCachedNumberLookupService();
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java
new file mode 100644
index 000000000..3552529ba
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.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.phonenumbercache;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the PhoneNumberCacheBindings.
+ */
+public interface PhoneNumberCacheBindingsFactory {
+
+ PhoneNumberCacheBindings newPhoneNumberCacheBindings();
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java
new file mode 100644
index 000000000..c7fb97807
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.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.dialer.phonenumbercache;
+
+import android.support.annotation.Nullable;
+
+/** Default implementation of PhoneNumberCacheBindings. */
+public class PhoneNumberCacheBindingsStub implements PhoneNumberCacheBindings {
+
+ @Override
+ @Nullable
+ public CachedNumberLookupService getCachedNumberLookupService() {
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/phonenumbercache/PhoneQuery.java b/java/com/android/dialer/phonenumbercache/PhoneQuery.java
new file mode 100644
index 000000000..5ddd5f846
--- /dev/null
+++ b/java/com/android/dialer/phonenumbercache/PhoneQuery.java
@@ -0,0 +1,96 @@
+/*
+ * 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.phonenumbercache;
+
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+
+/** The queries to look up the {@link ContactInfo} for a given number in the Call Log. */
+final class PhoneQuery {
+
+ public static final int PERSON_ID = 0;
+ public static final int NAME = 1;
+ public static final int PHONE_TYPE = 2;
+ public static final int LABEL = 3;
+ public static final int MATCHED_NUMBER = 4;
+ public static final int NORMALIZED_NUMBER = 5;
+ public static final int PHOTO_ID = 6;
+ public static final int LOOKUP_KEY = 7;
+ public static final int PHOTO_URI = 8;
+ /** Projection to look up a contact's DISPLAY_NAME_ALTERNATIVE */
+ public static final String[] DISPLAY_NAME_ALTERNATIVE_PROJECTION =
+ new String[] {
+ Contacts.DISPLAY_NAME_ALTERNATIVE,
+ };
+
+ public static final int NAME_ALTERNATIVE = 0;
+
+ public static final String[] ADDITIONAL_CONTACT_INFO_PROJECTION =
+ new String[] {Phone.DISPLAY_NAME_ALTERNATIVE, Phone.CARRIER_PRESENCE};
+ public static final int ADDITIONAL_CONTACT_INFO_DISPLAY_NAME_ALTERNATIVE = 0;
+ public static final int ADDITIONAL_CONTACT_INFO_CARRIER_PRESENCE = 1;
+
+ /**
+ * Projection to look up the ContactInfo. Does not include DISPLAY_NAME_ALTERNATIVE as that column
+ * isn't available in ContactsCommon.PhoneLookup. We should always use this projection starting
+ * from NYC onward.
+ */
+ private static final String[] PHONE_LOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup.CONTACT_ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_ID,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI
+ };
+ /**
+ * Similar to {@link PHONE_LOOKUP_PROJECTION}. In pre-N, contact id is stored in {@link
+ * PhoneLookup#_ID} in non-sip query.
+ */
+ private static final String[] BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.TYPE,
+ PhoneLookup.LABEL,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.PHOTO_ID,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.PHOTO_URI
+ };
+
+ public static String[] getPhoneLookupProjection(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return PHONE_LOOKUP_PROJECTION;
+ }
+ // Pre-N
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip) ? PHONE_LOOKUP_PROJECTION : BACKWARD_COMPATIBLE_NON_SIP_PHONE_LOOKUP_PROJECTION;
+ }
+}
diff --git a/java/com/android/dialer/phonenumberutil/AndroidManifest.xml b/java/com/android/dialer/phonenumberutil/AndroidManifest.xml
new file mode 100644
index 000000000..f7ee4001c
--- /dev/null
+++ b/java/com/android/dialer/phonenumberutil/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.phonenumberutil">
+</manifest>
diff --git a/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
new file mode 100644
index 000000000..ea4396f02
--- /dev/null
+++ b/java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java
@@ -0,0 +1,276 @@
+/*
+ * 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.phonenumberutil;
+
+import android.content.Context;
+import android.provider.CallLog;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+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.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+public class PhoneNumberHelper {
+
+ private static final String TAG = "PhoneNumberUtil";
+ private static final Set<String> LEGACY_UNKNOWN_NUMBERS =
+ new HashSet<>(Arrays.asList("-1", "-2", "-3"));
+
+ /** Returns true if it is possible to place a call to the given number. */
+ public static boolean canPlaceCallsTo(CharSequence number, int presentation) {
+ return presentation == CallLog.Calls.PRESENTATION_ALLOWED
+ && !TextUtils.isEmpty(number)
+ && !isLegacyUnknownNumbers(number);
+ }
+
+ /**
+ * 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 static boolean isVoicemailNumber(
+ Context context, PhoneAccountHandle accountHandle, CharSequence number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+ return TelecomUtil.isVoicemailNumber(context, accountHandle, number.toString());
+ }
+
+ /**
+ * Returns true if the given number is a SIP address. To be able to mock-out this, it is not a
+ * static method.
+ */
+ public static boolean isSipNumber(CharSequence number) {
+ return number != null && isUriNumber(number.toString());
+ }
+
+ public static boolean isUnknownNumberThatCanBeLookedUp(
+ Context context, PhoneAccountHandle accountHandle, CharSequence number, int presentation) {
+ if (presentation == CallLog.Calls.PRESENTATION_UNKNOWN) {
+ return false;
+ }
+ if (presentation == CallLog.Calls.PRESENTATION_RESTRICTED) {
+ return false;
+ }
+ if (presentation == CallLog.Calls.PRESENTATION_PAYPHONE) {
+ return false;
+ }
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+ if (isVoicemailNumber(context, accountHandle, number)) {
+ return false;
+ }
+ if (isLegacyUnknownNumbers(number)) {
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean isLegacyUnknownNumbers(CharSequence number) {
+ return number != null && LEGACY_UNKNOWN_NUMBERS.contains(number.toString());
+ }
+
+ /**
+ * @return a geographical description string for the specified number.
+ * @see com.android.i18n.phonenumbers.PhoneNumberOfflineGeocoder
+ */
+ public static String getGeoDescription(Context context, String number) {
+ LogUtil.v("PhoneNumberHelper.getGeoDescription", "" + LogUtil.sanitizePii(number));
+
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ PhoneNumberUtil util = PhoneNumberUtil.getInstance();
+ PhoneNumberOfflineGeocoder geocoder = PhoneNumberOfflineGeocoder.getInstance();
+
+ Locale locale = context.getResources().getConfiguration().locale;
+ String countryIso = getCurrentCountryIso(context, locale);
+ Phonenumber.PhoneNumber pn = null;
+ try {
+ LogUtil.v(
+ "PhoneNumberHelper.getGeoDescription",
+ "parsing '" + LogUtil.sanitizePii(number) + "' for countryIso '" + countryIso + "'...");
+ pn = util.parse(number, countryIso);
+ LogUtil.v(
+ "PhoneNumberHelper.getGeoDescription", "- parsed number: " + LogUtil.sanitizePii(pn));
+ } catch (NumberParseException e) {
+ LogUtil.e(
+ "PhoneNumberHelper.getGeoDescription",
+ "getGeoDescription: NumberParseException for incoming number '"
+ + LogUtil.sanitizePii(number)
+ + "'");
+ }
+
+ if (pn != null) {
+ String description = geocoder.getDescriptionForNumber(pn, locale);
+ LogUtil.v("PhoneNumberHelper.getGeoDescription", "- got description: '" + description + "'");
+ return description;
+ }
+
+ return null;
+ }
+
+ /**
+ * @return The ISO 3166-1 two letters country code of the country the user is in based on the
+ * network location. If the network location does not exist, fall back to the locale setting.
+ */
+ public static String getCurrentCountryIso(Context context, Locale locale) {
+ // Without framework function calls, this seems to be the most accurate location service
+ // we can rely on.
+ final TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+
+ String countryIso = telephonyManager.getNetworkCountryIso();
+ if (TextUtils.isEmpty(countryIso)) {
+ countryIso = locale.getCountry();
+ LogUtil.i(
+ "PhoneNumberHelper.getCurrentCountryIso",
+ "No CountryDetector; falling back to countryIso based on locale: " + countryIso);
+ }
+ countryIso = countryIso.toUpperCase();
+
+ return countryIso;
+ }
+
+ /**
+ * @return Formatted phone number. e.g. 1-123-456-7890. Returns the original number if formatting
+ * failed.
+ */
+ public static String formatNumber(@Nullable String number, Context context) {
+ // The number can be null e.g. schema is voicemail and uri content is empty.
+ if (number == null) {
+ return null;
+ }
+ String formattedNumber =
+ PhoneNumberUtils.formatNumber(
+ number, PhoneNumberHelper.getCurrentCountryIso(context, Locale.getDefault()));
+ return formattedNumber != null ? formattedNumber : number;
+ }
+
+ /**
+ * Determines if the specified number is actually a URI (i.e. a SIP address) rather than a regular
+ * PSTN phone number, based on whether or not the number contains an "@" character.
+ *
+ * @param number Phone number
+ * @return true if number contains @
+ * <p>TODO: Remove if PhoneNumberUtils.isUriNumber(String number) is made public.
+ */
+ public static boolean isUriNumber(String number) {
+ // Note we allow either "@" or "%40" to indicate a URI, in case
+ // the passed-in string is URI-escaped. (Neither "@" nor "%40"
+ // will ever be found in a legal PSTN number.)
+ return number != null && (number.contains("@") || number.contains("%40"));
+ }
+
+ /**
+ * @param number SIP address of the form "username@domainname" (or the URI-escaped equivalent
+ * "username%40domainname")
+ * <p>TODO: Remove if PhoneNumberUtils.getUsernameFromUriNumber(String number) is made public.
+ * @return the "username" part of the specified SIP address, i.e. the part before the "@"
+ * character (or "%40").
+ */
+ public static String getUsernameFromUriNumber(String number) {
+ // The delimiter between username and domain name can be
+ // either "@" or "%40" (the URI-escaped equivalent.)
+ int delimiterIndex = number.indexOf('@');
+ if (delimiterIndex < 0) {
+ delimiterIndex = number.indexOf("%40");
+ }
+ if (delimiterIndex < 0) {
+ LogUtil.i(
+ "PhoneNumberHelper.getUsernameFromUriNumber",
+ "getUsernameFromUriNumber: no delimiter found in SIP address: "
+ + LogUtil.sanitizePii(number));
+ return number;
+ }
+ return number.substring(0, delimiterIndex);
+ }
+
+
+ private static boolean isVerizon(Context context) {
+ // Verizon MCC/MNC codes copied from com/android/voicemailomtp/res/xml/vvm_config.xml.
+ // TODO: Need a better way to do per carrier and per OEM configurations.
+ switch (context.getSystemService(TelephonyManager.class).getSimOperator()) {
+ case "310004":
+ case "310010":
+ case "310012":
+ case "310013":
+ case "310590":
+ case "310890":
+ case "310910":
+ case "311110":
+ case "311270":
+ case "311271":
+ case "311272":
+ case "311273":
+ case "311274":
+ case "311275":
+ case "311276":
+ case "311277":
+ case "311278":
+ case "311279":
+ case "311280":
+ case "311281":
+ case "311282":
+ case "311283":
+ case "311284":
+ case "311285":
+ case "311286":
+ case "311287":
+ case "311288":
+ case "311289":
+ case "311390":
+ case "311480":
+ case "311481":
+ case "311482":
+ case "311483":
+ case "311484":
+ case "311485":
+ case "311486":
+ case "311487":
+ case "311488":
+ case "311489":
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Gets the label to display for a phone call where the presentation is set as
+ * PRESENTATION_RESTRICTED. For Verizon we want this to be displayed as "Restricted". For all
+ * other carriers we want this to be be displayed as "Private number".
+ */
+ public static CharSequence getDisplayNameForRestrictedNumber(Context context) {
+ if (isVerizon(context)) {
+ return context.getString(R.string.private_num_verizon);
+ } else {
+ return context.getString(R.string.private_num_non_verizon);
+ }
+ }
+}
diff --git a/java/com/android/dialer/phonenumberutil/res/values/strings.xml b/java/com/android/dialer/phonenumberutil/res/values/strings.xml
new file mode 100644
index 000000000..f31883ef6
--- /dev/null
+++ b/java/com/android/dialer/phonenumberutil/res/values/strings.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <!-- String used to display calls from private numbers in the call log and in call UI. For
+ example, if the user gets an incoming phone call from an unknown number, the caller ID will
+ read "Restricted". We only show this string if the user is on the Verizon network. -->
+ <string name="private_num_verizon">Restricted</string>
+
+ <!-- String used to display calls from private numbers in the call log. For example, if the user
+ gets an incoming phone call from an unknown number, the caller ID will read "Private number".
+ We only show this string if the user is not on the Verizon network. -->
+ <string name="private_num_non_verizon">Private number</string>
+</resources>
diff --git a/java/com/android/dialer/proguard/UsedByReflection.java b/java/com/android/dialer/proguard/UsedByReflection.java
new file mode 100644
index 000000000..200c33ed8
--- /dev/null
+++ b/java/com/android/dialer/proguard/UsedByReflection.java
@@ -0,0 +1,34 @@
+/*
+ * 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.proguard;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Denotes that the class, constructor, method or field is used for reflection and therefore cannot
+ * be removed by tools like ProGuard. Use the value parameter to mention a file that uses the
+ * component marked as UsedByReflection.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.FIELD})
+public @interface UsedByReflection {
+
+ String value();
+}
diff --git a/java/com/android/dialer/protos/ProtoParsers.java b/java/com/android/dialer/protos/ProtoParsers.java
new file mode 100644
index 000000000..7dfbc7c5e
--- /dev/null
+++ b/java/com/android/dialer/protos/ProtoParsers.java
@@ -0,0 +1,167 @@
+/*
+ * 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.protos;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.dialer.common.Assert;
+import com.google.protobuf.nano.CodedOutputByteBufferNano;
+import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
+import com.google.protobuf.nano.MessageNano;
+import java.io.IOException;
+
+/** Useful methods for using Protocol Buffers with Android. */
+public final class ProtoParsers {
+
+ private ProtoParsers() {}
+
+ /** Retrieve a proto from a Bundle */
+ @SuppressWarnings("unchecked") // We want to eventually optimize away parser classes, so cast
+ public static <T extends MessageNano> T get(Bundle bundle, String key, T defaultInstance)
+ throws InvalidProtocolBufferNanoException {
+ InternalDontUse parcelable = bundle.getParcelable(key);
+ return (T) parcelable.getMessageUnsafe(defaultInstance);
+ }
+
+ /**
+ * Retrieve a proto from a trusted bundle
+ *
+ * @throws RuntimeException if the proto cannot be parsed
+ */
+ public static <T extends MessageNano> T getFromInstanceState(
+ Bundle bundle, String key, T defaultInstance) {
+ try {
+ return get(bundle, key, defaultInstance);
+ } catch (InvalidProtocolBufferNanoException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Stores a proto in a Bundle, for later retrieval by {@link #get(Bundle, String, MessageNano)} or
+ * {@link #getFromInstanceState(Bundle, String, MessageNano)}.
+ */
+ public static void put(Bundle bundle, String key, MessageNano message) {
+ bundle.putParcelable(key, new InternalDontUse<>(null, message));
+ }
+
+ /**
+ * Stores a proto in an Intent, for later retrieval by {@link #get(Bundle, String, MessageNano)}.
+ * Needs separate method because Intent has similar to but different API than Bundle.
+ */
+ public static void put(Intent intent, String key, MessageNano message) {
+ intent.putExtra(key, new InternalDontUse<>(null, message));
+ }
+
+ /** Returns a {@linkplain Parcelable} representation of this protobuf message. */
+ public static <T extends MessageNano> ParcelableProto<T> asParcelable(T message) {
+ return new InternalDontUse<>(null, message);
+ }
+
+ /**
+ * A protobuf message that can be stored in a {@link Parcel}.
+ *
+ * <p><b>Note:</b> This <code>Parcelable</code> can only be used in single app. Attempting to send
+ * it to another app through an <code>Intent</code> will result in an exception due to Proguard
+ * obfusation when the target application attempts to load the <code>ParcelableProto</code> class.
+ */
+ public interface ParcelableProto<T extends MessageNano> extends Parcelable {
+ /**
+ * @throws IllegalStateException if the parceled data does not correspond to the defaultInstance
+ * type.
+ */
+ T getMessage(T defaultInstance);
+ }
+
+ /** Public because of Parcelable requirements. Do not use. */
+ public static final class InternalDontUse<T extends MessageNano> implements ParcelableProto<T> {
+ /* One of these two fields is always populated - since the bytes field never escapes this
+ * object, there is no risk of concurrent modification by multiple threads, and volatile
+ * is sufficient to be thread-safe. */
+ private volatile byte[] bytes;
+ private volatile T message;
+
+ /**
+ * Ideally, we would have type safety here. However, a static field {@link Creator} is required
+ * by {@link Parcelable}. Static fields are inherently not type safe, since only 1 exists per
+ * class (rather than 1 per type).
+ */
+ public static final Parcelable.Creator<InternalDontUse<?>> CREATOR =
+ new Creator<InternalDontUse<?>>() {
+ @Override
+ public InternalDontUse<?> createFromParcel(Parcel parcel) {
+ int serializedSize = parcel.readInt();
+ byte[] array = new byte[serializedSize];
+ parcel.readByteArray(array);
+ return new InternalDontUse<>(array, null);
+ }
+
+ @Override
+ public InternalDontUse<?>[] newArray(int i) {
+ return new InternalDontUse[i];
+ }
+ };
+
+ private InternalDontUse(byte[] bytes, T message) {
+ Assert.checkArgument(bytes != null || message != null);
+ this.bytes = bytes;
+ this.message = message;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ if (bytes == null) {
+ final byte[] flatArray = new byte[message.getSerializedSize()];
+ try {
+ message.writeTo(CodedOutputByteBufferNano.newInstance(flatArray));
+ bytes = flatArray;
+ } catch (IOException impossible) {
+ throw new AssertionError(impossible);
+ }
+ }
+ parcel.writeInt(bytes.length);
+ parcel.writeByteArray(bytes);
+ }
+
+ @Override
+ public T getMessage(T defaultInstance) {
+ try {
+ // The proto should never be invalid if it came from our application, so if it is, throw.
+ return getMessageUnsafe(defaultInstance);
+ } catch (InvalidProtocolBufferNanoException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @SuppressWarnings("unchecked") // We're being deserialized, so there's no real type safety
+ T getMessageUnsafe(T defaultInstance) throws InvalidProtocolBufferNanoException {
+ // There's a risk that we'll double-parse the bytes, but that's OK, because it'll end up
+ // as the same immutable object anyway.
+ if (message == null) {
+ message = MessageNano.mergeFrom(defaultInstance, bytes);
+ }
+ return message;
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/AndroidManifest.xml b/java/com/android/dialer/shortcuts/AndroidManifest.xml
new file mode 100644
index 000000000..e731a3e68
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/AndroidManifest.xml
@@ -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
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.shortcuts">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <application>
+
+ <service
+ android:exported="false"
+ android:name=".PeriodicJobService"
+ android:permission="android.permission.BIND_JOB_SERVICE"/>
+
+ <!--
+ Comments for attributes in CallContactActivity:
+ taskAffinity="" -> Open the dialog without opening the dialer app behind it
+ noHistory="true" -> Navigating away finishes activity
+ excludeFromRecents="true" -> Don't show in "recent apps" screen
+
+ We do not export this activity and do not declare an intent filter as a security precaution
+ so that apps other than the dialer cannot attempt to make phone calls using it.
+ -->
+ <activity
+ android:name=".CallContactActivity"
+ android:taskAffinity=""
+ android:noHistory="true"
+ android:excludeFromRecents="true"
+ android:label=""
+ android:exported="false"
+ android:theme="@style/CallContactsTheme"/>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
new file mode 100644
index 000000000..ef995c816
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java
@@ -0,0 +1,161 @@
+/*
+ * 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.shortcuts;
+
+import android.support.annotation.NonNull;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_DialerShortcut extends DialerShortcut {
+
+ private final long contactId;
+ private final String lookupKey;
+ private final String displayName;
+ private final int rank;
+
+ private AutoValue_DialerShortcut(
+ long contactId,
+ String lookupKey,
+ String displayName,
+ int rank) {
+ this.contactId = contactId;
+ this.lookupKey = lookupKey;
+ this.displayName = displayName;
+ this.rank = rank;
+ }
+
+ @Override
+ long getContactId() {
+ return contactId;
+ }
+
+ @NonNull
+ @Override
+ String getLookupKey() {
+ return lookupKey;
+ }
+
+ @NonNull
+ @Override
+ String getDisplayName() {
+ return displayName;
+ }
+
+ @Override
+ int getRank() {
+ return rank;
+ }
+
+ @Override
+ public String toString() {
+ return "DialerShortcut{"
+ + "contactId=" + contactId + ", "
+ + "lookupKey=" + lookupKey + ", "
+ + "displayName=" + displayName + ", "
+ + "rank=" + rank
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof DialerShortcut) {
+ DialerShortcut that = (DialerShortcut) o;
+ return (this.contactId == that.getContactId())
+ && (this.lookupKey.equals(that.getLookupKey()))
+ && (this.displayName.equals(that.getDisplayName()))
+ && (this.rank == that.getRank());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= (this.contactId >>> 32) ^ this.contactId;
+ h *= 1000003;
+ h ^= this.lookupKey.hashCode();
+ h *= 1000003;
+ h ^= this.displayName.hashCode();
+ h *= 1000003;
+ h ^= this.rank;
+ return h;
+ }
+
+ static final class Builder extends DialerShortcut.Builder {
+ private Long contactId;
+ private String lookupKey;
+ private String displayName;
+ private Integer rank;
+ Builder() {
+ }
+ private Builder(DialerShortcut source) {
+ this.contactId = source.getContactId();
+ this.lookupKey = source.getLookupKey();
+ this.displayName = source.getDisplayName();
+ this.rank = source.getRank();
+ }
+ @Override
+ DialerShortcut.Builder setContactId(long contactId) {
+ this.contactId = contactId;
+ return this;
+ }
+ @Override
+ DialerShortcut.Builder setLookupKey(String lookupKey) {
+ this.lookupKey = lookupKey;
+ return this;
+ }
+ @Override
+ DialerShortcut.Builder setDisplayName(String displayName) {
+ this.displayName = displayName;
+ return this;
+ }
+ @Override
+ DialerShortcut.Builder setRank(int rank) {
+ this.rank = rank;
+ return this;
+ }
+ @Override
+ DialerShortcut build() {
+ String missing = "";
+ if (this.contactId == null) {
+ missing += " contactId";
+ }
+ if (this.lookupKey == null) {
+ missing += " lookupKey";
+ }
+ if (this.displayName == null) {
+ missing += " displayName";
+ }
+ if (this.rank == null) {
+ missing += " rank";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_DialerShortcut(
+ this.contactId,
+ this.lookupKey,
+ this.displayName,
+ this.rank);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/java/com/android/dialer/shortcuts/CallContactActivity.java b/java/com/android/dialer/shortcuts/CallContactActivity.java
new file mode 100644
index 000000000..1e9a01b39
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/CallContactActivity.java
@@ -0,0 +1,133 @@
+/*
+ * 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.shortcuts;
+
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.app.ActivityCompat;
+import android.widget.Toast;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.interactions.PhoneNumberInteraction;
+import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorCode;
+import com.android.dialer.util.TransactionSafeActivity;
+
+/**
+ * Invisible activity launched when a shortcut is selected by user. Calls a contact based on URI.
+ */
+public class CallContactActivity extends TransactionSafeActivity
+ implements PhoneNumberInteraction.DisambigDialogDismissedListener,
+ PhoneNumberInteraction.InteractionErrorListener,
+ ActivityCompat.OnRequestPermissionsResultCallback {
+
+ private static final String CONTACT_URI_KEY = "uri_key";
+
+ private Uri contactUri;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if ("com.android.dialer.shortcuts.CALL_CONTACT".equals(getIntent().getAction())) {
+ if (Shortcuts.areDynamicShortcutsEnabled(this)) {
+ LogUtil.i("CallContactActivity.onCreate", "shortcut clicked");
+ contactUri = getIntent().getData();
+ makeCall();
+ } else {
+ LogUtil.i("CallContactActivity.onCreate", "dynamic shortcuts disabled");
+ finish();
+ }
+ }
+ }
+
+ private void makeCall() {
+ CallSpecificAppData callSpecificAppData = new CallSpecificAppData();
+ callSpecificAppData.callInitiationType = CallInitiationType.Type.LAUNCHER_SHORTCUT;
+ PhoneNumberInteraction.startInteractionForPhoneCall(
+ this, contactUri, false /* isVideoCall */, callSpecificAppData);
+ }
+
+ @Override
+ public void onDisambigDialogDismissed() {
+ finish();
+ }
+
+ @Override
+ public void interactionError(@InteractionErrorCode int interactionErrorCode) {
+ // Note: There is some subtlety to how contact lookup keys work that make it difficult to
+ // distinguish the case of the contact missing from the case of the a contact not having a
+ // number. For example, if a contact's phone number is deleted, subsequent lookups based on
+ // lookup key will actually return no results because the phone number was part of the
+ // lookup key. In this case, it would be inaccurate to say the contact can't be found though, so
+ // in all cases we just say the contact can't be found or the contact doesn't have a number.
+ switch (interactionErrorCode) {
+ case InteractionErrorCode.CONTACT_NOT_FOUND:
+ case InteractionErrorCode.CONTACT_HAS_NO_NUMBER:
+ Toast.makeText(
+ this,
+ R.string.dialer_shortcut_contact_not_found_or_has_no_number,
+ Toast.LENGTH_SHORT)
+ .show();
+ break;
+ case InteractionErrorCode.USER_LEAVING_ACTIVITY:
+ case InteractionErrorCode.OTHER_ERROR:
+ default:
+ // If the user is leaving the activity or the error code was "other" there's no useful
+ // information to display but we still need to finish this invisible activity.
+ break;
+ }
+ finish();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(CONTACT_URI_KEY, contactUri);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState == null) {
+ return;
+ }
+ contactUri = savedInstanceState.getParcelable(CONTACT_URI_KEY);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, String[] permissions, int[] grantResults) {
+ switch (requestCode) {
+ case PhoneNumberInteraction.REQUEST_READ_CONTACTS:
+ {
+ // If request is cancelled, the result arrays are empty.
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ makeCall();
+ } else {
+ Toast.makeText(this, R.string.dialer_shortcut_no_permissions, Toast.LENGTH_SHORT)
+ .show();
+ }
+ finish();
+ break;
+ }
+ default:
+ throw new IllegalStateException("Unsupported request code: " + requestCode);
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/DialerShortcut.java b/java/com/android/dialer/shortcuts/DialerShortcut.java
new file mode 100644
index 000000000..f2fb3301a
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/DialerShortcut.java
@@ -0,0 +1,190 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.content.pm.ShortcutInfo;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.NonNull;
+
+
+/**
+ * Convenience data structure.
+ *
+ * <p>This differs from {@link ShortcutInfo} in that it doesn't hold an icon or intent, and provides
+ * convenience methods for doing things like constructing labels.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+
+abstract class DialerShortcut {
+
+ /** Marker value indicates that shortcut has no setRank. Used by pinned shortcuts. */
+ static final int NO_RANK = -1;
+
+ /**
+ * Contact ID from contacts provider. Note that this a numeric row ID from the
+ * ContactsContract.Contacts._ID column.
+ */
+ abstract long getContactId();
+
+ /**
+ * Lookup key from contacts provider. An example lookup key is: "0r8-47392D". This is the value
+ * from ContactsContract.Contacts.LOOKUP_KEY.
+ */
+ @NonNull
+ abstract String getLookupKey();
+
+ /** Display name from contacts provider. */
+ @NonNull
+ abstract String getDisplayName();
+
+ /**
+ * Rank for dynamic shortcuts. This value should be positive or {@link #NO_RANK}.
+ *
+ * <p>For floating shortcuts (pinned shortcuts with no corresponding dynamic shortcut), setRank
+ * has no meaning and the setRank may be set to {@link #NO_RANK}.
+ */
+ abstract int getRank();
+
+ /** The short label for the shortcut. Used when pinning shortcuts, for example. */
+ @NonNull
+ String getShortLabel() {
+ // Be sure to update getDisplayNameFromShortcutInfo when updating this.
+ return getDisplayName();
+ }
+
+ /**
+ * The long label for the shortcut. Used for shortcuts displayed when pressing and holding the app
+ * launcher icon, for example.
+ */
+ @NonNull
+ String getLongLabel() {
+ return getDisplayName();
+ }
+
+ /** The display name for the provided shortcut. */
+ static String getDisplayNameFromShortcutInfo(ShortcutInfo shortcutInfo) {
+ return shortcutInfo.getShortLabel().toString();
+ }
+
+ /**
+ * The id used to identify launcher shortcuts. Used for updating/deleting shortcuts.
+ *
+ * <p>Lookup keys are used for shortcut IDs. See {@link #getLookupKey()}.
+ *
+ * <p>If you change this, you probably also need to change {@link #getLookupKeyFromShortcutInfo}.
+ */
+ @NonNull
+ String getShortcutId() {
+ return getLookupKey();
+ }
+
+ /**
+ * Returns the contact lookup key from the provided {@link ShortcutInfo}.
+ *
+ * <p>Lookup keys are used for shortcut IDs. See {@link #getLookupKey()}.
+ */
+ @NonNull
+ static String getLookupKeyFromShortcutInfo(@NonNull ShortcutInfo shortcutInfo) {
+ return shortcutInfo.getId(); // Lookup keys are used for shortcut IDs.
+ }
+
+ /**
+ * Returns the lookup URI from the provided {@link ShortcutInfo}.
+ *
+ * <p>Lookup URIs are constructed from lookup key and contact ID. Here is an example lookup URI
+ * where lookup key is "0r8-47392D" and contact ID is 8:
+ *
+ * <p>"content://com.android.contacts/contacts/lookup/0r8-47392D/8"
+ */
+ @NonNull
+ static Uri getLookupUriFromShortcutInfo(@NonNull ShortcutInfo shortcutInfo) {
+ long contactId =
+ shortcutInfo.getIntent().getLongExtra(ShortcutInfoFactory.EXTRA_CONTACT_ID, -1);
+ if (contactId == -1) {
+ throw new IllegalStateException("No contact ID found for shortcut: " + shortcutInfo.getId());
+ }
+ String lookupKey = getLookupKeyFromShortcutInfo(shortcutInfo);
+ return Contacts.getLookupUri(contactId, lookupKey);
+ }
+
+ /**
+ * Contacts provider URI which uses the contact lookup key.
+ *
+ * <p>Lookup URIs are constructed from lookup key and contact ID. Here is an example lookup URI
+ * where lookup key is "0r8-47392D" and contact ID is 8:
+ *
+ * <p>"content://com.android.contacts/contacts/lookup/0r8-47392D/8"
+ */
+ @NonNull
+ Uri getLookupUri() {
+ return Contacts.getLookupUri(getContactId(), getLookupKey());
+ }
+
+ /**
+ * Given an existing shortcut with the same shortcut ID, returns true if the existing shortcut
+ * needs to be updated, e.g. if the contact's name or rank has changed.
+ *
+ * <p>Does not detect photo updates.
+ */
+ boolean needsUpdate(@NonNull ShortcutInfo oldInfo) {
+ if (this.getRank() != NO_RANK && oldInfo.getRank() != this.getRank()) {
+ return true;
+ }
+ if (!oldInfo.getShortLabel().equals(this.getShortLabel())) {
+ return true;
+ }
+ if (!oldInfo.getLongLabel().equals(this.getLongLabel())) {
+ return true;
+ }
+ return false;
+ }
+
+ static Builder builder() {
+ return new AutoValue_DialerShortcut.Builder().setRank(NO_RANK);
+ }
+
+
+ abstract static class Builder {
+
+ /**
+ * Sets the contact ID. This should be a value from the contact provider's Contact._ID column.
+ */
+ abstract Builder setContactId(long value);
+
+ /**
+ * Sets the lookup key. This should be a contact lookup key as provided by the contact provider.
+ */
+ abstract Builder setLookupKey(@NonNull String value);
+
+ /** Sets the display name. This should be a value provided by the contact provider. */
+ abstract Builder setDisplayName(@NonNull String value);
+
+ /**
+ * Sets the rank for the shortcut, used for ordering dynamic shortcuts. This is required for
+ * dynamic shortcuts but unused for floating shortcuts because rank has no meaning for floating
+ * shortcuts. (Floating shortcuts are shortcuts which are pinned but have no corresponding
+ * dynamic shortcut.)
+ */
+ abstract Builder setRank(int value);
+
+ /** Builds the immutable {@link DialerShortcut} object from this builder. */
+ abstract DialerShortcut build();
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/DynamicShortcuts.java b/java/com/android/dialer/shortcuts/DynamicShortcuts.java
new file mode 100644
index 000000000..be9e088e1
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/DynamicShortcuts.java
@@ -0,0 +1,243 @@
+/*
+ * 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.shortcuts;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.content.ContextCompat;
+import android.util.ArrayMap;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Handles refreshing of dialer dynamic shortcuts.
+ *
+ * <p>Dynamic shortcuts are the list of shortcuts which is accessible by tapping and holding the
+ * dialer launcher icon from the app drawer or a home screen.
+ *
+ * <p>Dynamic shortcuts are refreshed whenever the dialtacts activity detects changes to favorites
+ * tiles. This class compares the newly updated favorites tiles to the existing list of (previously
+ * published) dynamic shortcuts to compute a delta, which consists of lists of shortcuts which need
+ * to be updated, added, or deleted.
+ *
+ * <p>Dynamic shortcuts should mirror (in order) the contacts displayed in the "tiled favorites" tab
+ * of the dialer application. When selecting a dynamic shortcut, the behavior should be the same as
+ * if the user had tapped on the contact from the tiled favorites tab. Specifically, if the user has
+ * more than one phone number, a number picker should be displayed, and otherwise the contact should
+ * be called directly.
+ *
+ * <p>Note that an icon change by itself does not trigger a shortcut update, because it is not
+ * possible to detect an icon update and we don't want to constantly force update icons, because
+ * that is an expensive operation which requires storage I/O.
+ *
+ * <p>However, the job scheduler uses {@link #updateIcons()} to makes sure icons are forcefully
+ * updated periodically (about once a day).
+ *
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class DynamicShortcuts {
+
+ private static final int MAX_DYNAMIC_SHORTCUTS = 3;
+
+ private static class Delta {
+
+ final Map<String, DialerShortcut> shortcutsToUpdateById = new ArrayMap<>();
+ final List<String> shortcutIdsToRemove = new ArrayList<>();
+ final Map<String, DialerShortcut> shortcutsToAddById = new ArrayMap<>();
+ }
+
+ private final Context context;
+ private final ShortcutInfoFactory shortcutInfoFactory;
+
+ DynamicShortcuts(@NonNull Context context, IconFactory iconFactory) {
+ this.context = context;
+ this.shortcutInfoFactory = new ShortcutInfoFactory(context, iconFactory);
+ }
+
+ /**
+ * Performs a "complete refresh" of dynamic shortcuts. This is done by comparing the provided
+ * contact information with the existing dynamic shortcuts in order to compute a delta which
+ * contains shortcuts which should be added, updated, or removed.
+ *
+ * <p>If the delta is non-empty, it is applied by making appropriate calls to the {@link
+ * ShortcutManager} system service.
+ *
+ * <p>This is a slow blocking call which performs file I/O and should not be performed on the main
+ * thread.
+ */
+ @WorkerThread
+ public void refresh(List<ContactEntry> contacts) {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("DynamicShortcuts.refresh");
+
+ ShortcutManager shortcutManager = getShortcutManager(context);
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("DynamicShortcuts.refresh", "no contact permissions");
+ shortcutManager.removeAllDynamicShortcuts();
+ return;
+ }
+
+ // Fill the available shortcuts with dynamic shortcuts up to a maximum of 3 dynamic shortcuts.
+ int numDynamicShortcutsToCreate =
+ Math.min(
+ MAX_DYNAMIC_SHORTCUTS,
+ shortcutManager.getMaxShortcutCountPerActivity()
+ - shortcutManager.getManifestShortcuts().size());
+
+ Map<String, DialerShortcut> newDynamicShortcutsById =
+ new ArrayMap<>(numDynamicShortcutsToCreate);
+ int rank = 0;
+ for (ContactEntry entry : contacts) {
+ if (newDynamicShortcutsById.size() >= numDynamicShortcutsToCreate) {
+ break;
+ }
+
+ DialerShortcut shortcut =
+ DialerShortcut.builder()
+ .setContactId(entry.id)
+ .setLookupKey(entry.lookupKey)
+ .setDisplayName(entry.getPreferredDisplayName())
+ .setRank(rank++)
+ .build();
+ newDynamicShortcutsById.put(shortcut.getShortcutId(), shortcut);
+ }
+
+ List<ShortcutInfo> oldDynamicShortcuts = new ArrayList<>(shortcutManager.getDynamicShortcuts());
+ Delta delta = computeDelta(oldDynamicShortcuts, newDynamicShortcutsById);
+ applyDelta(delta);
+ }
+
+ /**
+ * Forces an update of all dynamic shortcut icons. This should only be done from job scheduler as
+ * updating icons requires storage I/O.
+ */
+ @WorkerThread
+ void updateIcons() {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("DynamicShortcuts.updateIcons");
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("DynamicShortcuts.updateIcons", "no contact permissions");
+ return;
+ }
+
+ ShortcutManager shortcutManager = getShortcutManager(context);
+
+ int maxDynamicShortcutsToCreate =
+ shortcutManager.getMaxShortcutCountPerActivity()
+ - shortcutManager.getManifestShortcuts().size();
+ int count = 0;
+
+ List<ShortcutInfo> newShortcuts = new ArrayList<>();
+ for (ShortcutInfo oldInfo : shortcutManager.getDynamicShortcuts()) {
+ newShortcuts.add(shortcutInfoFactory.withUpdatedIcon(oldInfo));
+ if (++count >= maxDynamicShortcutsToCreate) {
+ break;
+ }
+ }
+ LogUtil.i("DynamicShortcuts.updateIcons", "updating %d shortcut icons", newShortcuts.size());
+ shortcutManager.setDynamicShortcuts(newShortcuts);
+ }
+
+ @NonNull
+ private Delta computeDelta(
+ @NonNull List<ShortcutInfo> oldDynamicShortcuts,
+ @NonNull Map<String, DialerShortcut> newDynamicShortcutsById) {
+ Delta delta = new Delta();
+ if (oldDynamicShortcuts.isEmpty()) {
+ delta.shortcutsToAddById.putAll(newDynamicShortcutsById);
+ return delta;
+ }
+
+ for (ShortcutInfo oldInfo : oldDynamicShortcuts) {
+ // Check to see if the new shortcut list contains the existing shortcut.
+ DialerShortcut newShortcut = newDynamicShortcutsById.get(oldInfo.getId());
+ if (newShortcut != null) {
+ if (newShortcut.needsUpdate(oldInfo)) {
+ LogUtil.i("DynamicShortcuts.computeDelta", "contact updated");
+ delta.shortcutsToUpdateById.put(oldInfo.getId(), newShortcut);
+ } // else the shortcut hasn't changed, nothing to do to it
+ } else {
+ // The old shortcut is not in the new shortcut list, remove it.
+ LogUtil.i("DynamicShortcuts.computeDelta", "contact removed");
+ delta.shortcutIdsToRemove.add(oldInfo.getId());
+ }
+ }
+
+ // Add any new shortcuts that were not in the old shortcuts.
+ for (Entry<String, DialerShortcut> entry : newDynamicShortcutsById.entrySet()) {
+ String newId = entry.getKey();
+ DialerShortcut newShortcut = entry.getValue();
+ if (!containsShortcut(oldDynamicShortcuts, newId)) {
+ // The new shortcut was not found in the old shortcut list, so add it.
+ LogUtil.i("DynamicShortcuts.computeDelta", "contact added");
+ delta.shortcutsToAddById.put(newId, newShortcut);
+ }
+ }
+ return delta;
+ }
+
+ private void applyDelta(@NonNull Delta delta) {
+ ShortcutManager shortcutManager = getShortcutManager(context);
+ // Must perform remove before performing add to avoid adding more than supported by system.
+ if (!delta.shortcutIdsToRemove.isEmpty()) {
+ shortcutManager.removeDynamicShortcuts(delta.shortcutIdsToRemove);
+ }
+ if (!delta.shortcutsToUpdateById.isEmpty()) {
+ // Note: This may update pinned shortcuts as well. Pinned shortcuts which are also dynamic
+ // are not updated by the pinned shortcut logic. The reason that they are updated here
+ // instead of in the pinned shortcut logic is because setRank is required and only available
+ // here.
+ shortcutManager.updateShortcuts(
+ shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToUpdateById));
+ }
+ if (!delta.shortcutsToAddById.isEmpty()) {
+ shortcutManager.addDynamicShortcuts(
+ shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToAddById));
+ }
+ }
+
+ private boolean containsShortcut(
+ @NonNull List<ShortcutInfo> shortcutInfos, @NonNull String shortcutId) {
+ for (ShortcutInfo oldInfo : shortcutInfos) {
+ if (oldInfo.getId().equals(shortcutId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static ShortcutManager getShortcutManager(Context context) {
+ //noinspection WrongConstant
+ return (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/IconFactory.java b/java/com/android/dialer/shortcuts/IconFactory.java
new file mode 100644
index 000000000..a8c4ada4e
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/IconFactory.java
@@ -0,0 +1,112 @@
+/*
+ * 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.shortcuts;
+
+import android.content.Context;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.DrawableConverter;
+import java.io.InputStream;
+
+/** Constructs the icons for dialer shortcuts. */
+class IconFactory {
+
+ private final Context context;
+
+ IconFactory(@NonNull Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Creates an icon for the provided {@link DialerShortcut}.
+ *
+ * <p>The icon is a circle which contains a photo of the contact associated with the shortcut, if
+ * available. If a photo is not available, a circular colored icon with a single letter is instead
+ * created, where the letter is the first letter of the contact's name. If the contact has no
+ * name, a default colored "anonymous" avatar is used.
+ *
+ * <p>These icons should match exactly the favorites tiles in the starred tab of the dialer
+ * application, except that they are circular instead of rectangular.
+ */
+ @WorkerThread
+ @NonNull
+ public Icon create(@NonNull DialerShortcut shortcut) {
+ Assert.isWorkerThread();
+
+ return create(shortcut.getLookupUri(), shortcut.getDisplayName(), shortcut.getLookupKey());
+ }
+
+ /** Same as {@link #create(DialerShortcut)}, but accepts a {@link ShortcutInfo}. */
+ @WorkerThread
+ @NonNull
+ public Icon create(@NonNull ShortcutInfo shortcutInfo) {
+ Assert.isWorkerThread();
+ return create(
+ DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo),
+ DialerShortcut.getDisplayNameFromShortcutInfo(shortcutInfo),
+ DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo));
+ }
+
+ @WorkerThread
+ @NonNull
+ private Icon create(
+ @NonNull Uri lookupUri, @NonNull String displayName, @NonNull String lookupKey) {
+ Assert.isWorkerThread();
+
+ // In testing, there was no difference between high-res and thumbnail.
+ InputStream inputStream =
+ ContactsContract.Contacts.openContactPhotoInputStream(
+ context.getContentResolver(), lookupUri, false /* preferHighres */);
+
+ Drawable drawable;
+ if (inputStream == null) {
+ // No photo for contact; use a letter tile.
+ LetterTileDrawable letterTileDrawable = new LetterTileDrawable(context.getResources());
+ letterTileDrawable.setCanonicalDialerLetterTileDetails(
+ displayName, lookupKey, LetterTileDrawable.SHAPE_CIRCLE, LetterTileDrawable.TYPE_DEFAULT);
+ drawable = letterTileDrawable;
+ } else {
+ // There's a photo, create a circular drawable from it.
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ drawable = createCircularDrawable(bitmap);
+ }
+ int iconSize =
+ context.getResources().getDimensionPixelSize(R.dimen.launcher_shortcut_icon_size);
+ return Icon.createWithBitmap(
+ DrawableConverter.drawableToBitmap(drawable, iconSize /* width */, iconSize /* height */));
+ }
+
+ @NonNull
+ private Drawable createCircularDrawable(@NonNull Bitmap bitmap) {
+ RoundedBitmapDrawable roundedBitmapDrawable =
+ RoundedBitmapDrawableFactory.create(context.getResources(), bitmap);
+ roundedBitmapDrawable.setCircular(true);
+ roundedBitmapDrawable.setAntiAlias(true);
+ return roundedBitmapDrawable;
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/PeriodicJobService.java b/java/com/android/dialer/shortcuts/PeriodicJobService.java
new file mode 100644
index 000000000..62c9e37a0
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/PeriodicJobService.java
@@ -0,0 +1,118 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.app.job.JobInfo;
+import android.app.job.JobParameters;
+import android.app.job.JobScheduler;
+import android.app.job.JobService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.v4.os.UserManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.constants.ScheduledJobIds;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * {@link JobService} which starts the periodic job to refresh dynamic and pinned shortcuts.
+ *
+ * <p>Only {@link #schedulePeriodicJob(Context)} should be used by callers.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+public final class PeriodicJobService extends JobService {
+
+ private static final long REFRESH_PERIOD_MILLIS = TimeUnit.HOURS.toMillis(24);
+
+ private RefreshShortcutsTask refreshShortcutsTask;
+
+ /**
+ * Schedules the periodic job to refresh shortcuts. If called repeatedly, the job will just be
+ * rescheduled.
+ *
+ * <p>The job will not be scheduled if the build version is not at least N MR1 or if the user is
+ * locked.
+ */
+ @MainThread
+ public static void schedulePeriodicJob(@NonNull Context context) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.schedulePeriodicJob");
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1 && UserManagerCompat.isUserUnlocked(context)) {
+ JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
+ if (jobScheduler.getPendingJob(ScheduledJobIds.SHORTCUT_PERIODIC_JOB) != null) {
+ LogUtil.i("PeriodicJobService.schedulePeriodicJob", "job already scheduled.");
+ return;
+ }
+ JobInfo jobInfo =
+ new JobInfo.Builder(
+ ScheduledJobIds.SHORTCUT_PERIODIC_JOB,
+ new ComponentName(context, PeriodicJobService.class))
+ .setPeriodic(REFRESH_PERIOD_MILLIS)
+ .setPersisted(true)
+ .setRequiresCharging(true)
+ .setRequiresDeviceIdle(true)
+ .build();
+ jobScheduler.schedule(jobInfo);
+ }
+ }
+
+ /** Cancels the periodic job. */
+ @MainThread
+ public static void cancelJob(@NonNull Context context) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.cancelJob");
+
+ context.getSystemService(JobScheduler.class).cancel(ScheduledJobIds.SHORTCUT_PERIODIC_JOB);
+ }
+
+ @Override
+ @MainThread
+ public boolean onStartJob(@NonNull JobParameters params) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.onStartJob");
+
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ (refreshShortcutsTask = new RefreshShortcutsTask(this)).execute(params);
+ } else {
+ // It is possible for the job to have been scheduled on NMR1+ and then the system was
+ // downgraded to < NMR1. In this case, shortcuts are no longer supported so we cancel the job
+ // which creates them.
+ LogUtil.i("PeriodicJobService.onStartJob", "not running on NMR1, cancelling job");
+ cancelJob(this);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ @MainThread
+ public boolean onStopJob(@NonNull JobParameters params) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("PeriodicJobService.onStopJob");
+
+ if (refreshShortcutsTask != null) {
+ refreshShortcutsTask.cancel(false /* mayInterruptIfRunning */);
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/PinnedShortcuts.java b/java/com/android/dialer/shortcuts/PinnedShortcuts.java
new file mode 100644
index 000000000..bfcc3df81
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/PinnedShortcuts.java
@@ -0,0 +1,159 @@
+/*
+ * 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.shortcuts;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.content.ContextCompat;
+import android.util.ArrayMap;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Handles refreshing of dialer pinned shortcuts.
+ *
+ * <p>Pinned shortcuts are icons that the user has dragged to their home screen from the dialer
+ * application launcher shortcut menu, which is accessible by tapping and holding the dialer
+ * launcher icon from the app drawer or a home screen.
+ *
+ * <p>When refreshing pinned shortcuts, we check to make sure that pinned contact information is
+ * still up to date (e.g. photo and name). We also check to see if the contact has been deleted from
+ * the user's contacts, and if so, we disable the pinned shortcut.
+ *
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class PinnedShortcuts {
+
+ private static final String[] PROJECTION =
+ new String[] {
+ Contacts._ID, Contacts.DISPLAY_NAME_PRIMARY, Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
+ };
+
+ private static class Delta {
+
+ final List<String> shortcutIdsToDisable = new ArrayList<>();
+ final Map<String, DialerShortcut> shortcutsToUpdateById = new ArrayMap<>();
+ }
+
+ private final Context context;
+ private final ShortcutInfoFactory shortcutInfoFactory;
+
+ PinnedShortcuts(@NonNull Context context) {
+ this.context = context;
+ this.shortcutInfoFactory = new ShortcutInfoFactory(context, new IconFactory(context));
+ }
+
+ /**
+ * Performs a "complete refresh" of pinned shortcuts. This is done by (synchronously) querying for
+ * all contacts which currently have pinned shortcuts. The query results are used to compute a
+ * delta which contains a list of shortcuts which need to be updated (e.g. because of name/photo
+ * changes) or disabled (if contacts were deleted). Note that pinned shortcuts cannot be deleted
+ * programmatically and must be deleted by the user.
+ *
+ * <p>If the delta is non-empty, it is applied by making appropriate calls to the {@link
+ * ShortcutManager} system service.
+ *
+ * <p>This is a slow blocking call which performs file I/O and should not be performed on the main
+ * thread.
+ */
+ @WorkerThread
+ public void refresh() {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("PinnedShortcuts.refresh");
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("PinnedShortcuts.refresh", "no contact permissions");
+ return;
+ }
+
+ Delta delta = new Delta();
+ ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
+ for (ShortcutInfo shortcutInfo : shortcutManager.getPinnedShortcuts()) {
+ if (shortcutInfo.isDeclaredInManifest()) {
+ // We never update/disable the manifest shortcut (the "create new contact" shortcut).
+ continue;
+ }
+ if (shortcutInfo.isDynamic()) {
+ // If the shortcut is both pinned and dynamic, let the logic which updates dynamic shortcuts
+ // handle the update. It would be problematic to try and apply the update here, because the
+ // setRank is nonsensical for pinned shortcuts and therefore could not be calculated.
+ continue;
+ }
+
+ String lookupKey = DialerShortcut.getLookupKeyFromShortcutInfo(shortcutInfo);
+ Uri lookupUri = DialerShortcut.getLookupUriFromShortcutInfo(shortcutInfo);
+
+ try (Cursor cursor =
+ context.getContentResolver().query(lookupUri, PROJECTION, null, null, null)) {
+
+ if (cursor == null || !cursor.moveToNext()) {
+ LogUtil.i("PinnedShortcuts.refresh", "contact disabled");
+ delta.shortcutIdsToDisable.add(shortcutInfo.getId());
+ continue;
+ }
+
+ // Note: The lookup key may have changed but we cannot refresh it because that would require
+ // changing the shortcut ID, which can only be accomplished with a remove and add; but
+ // pinned shortcuts cannot be added or removed.
+ DialerShortcut shortcut =
+ DialerShortcut.builder()
+ .setContactId(cursor.getLong(cursor.getColumnIndexOrThrow(Contacts._ID)))
+ .setLookupKey(lookupKey)
+ .setDisplayName(
+ cursor.getString(cursor.getColumnIndexOrThrow(Contacts.DISPLAY_NAME_PRIMARY)))
+ .build();
+
+ if (shortcut.needsUpdate(shortcutInfo)) {
+ LogUtil.i("PinnedShortcuts.refresh", "contact updated");
+ delta.shortcutsToUpdateById.put(shortcutInfo.getId(), shortcut);
+ }
+ }
+ }
+ applyDelta(delta);
+ }
+
+ private void applyDelta(@NonNull Delta delta) {
+ ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class);
+ String shortcutDisabledMessage =
+ context.getResources().getString(R.string.dialer_shortcut_disabled_message);
+ if (!delta.shortcutIdsToDisable.isEmpty()) {
+ shortcutManager.disableShortcuts(delta.shortcutIdsToDisable, shortcutDisabledMessage);
+ }
+ if (!delta.shortcutsToUpdateById.isEmpty()) {
+ // Note: This call updates both pinned and dynamic shortcuts, but the delta should contain
+ // no dynamic shortcuts.
+ if (!shortcutManager.updateShortcuts(
+ shortcutInfoFactory.buildShortcutInfos(delta.shortcutsToUpdateById))) {
+ LogUtil.i("PinnedShortcuts.applyDelta", "shortcutManager rate limited.");
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/RefreshShortcutsTask.java b/java/com/android/dialer/shortcuts/RefreshShortcutsTask.java
new file mode 100644
index 000000000..086d1dc7a
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/RefreshShortcutsTask.java
@@ -0,0 +1,71 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** {@link AsyncTask} used by the periodic job service to refresh dynamic and pinned shortcuts. */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class RefreshShortcutsTask extends AsyncTask<JobParameters, Void, JobParameters> {
+
+ private final JobService jobService;
+
+ RefreshShortcutsTask(@NonNull JobService jobService) {
+ this.jobService = jobService;
+ }
+
+ /** @param params array with length 1, provided from PeriodicJobService */
+ @Override
+ @NonNull
+ @WorkerThread
+ protected JobParameters doInBackground(JobParameters... params) {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("RefreshShortcutsTask.doInBackground");
+
+ // Dynamic shortcuts are refreshed from the UI but icons can become stale, so update them
+ // periodically using the job service.
+ //
+ // The reason that icons can become is stale is that there is no last updated timestamp for
+ // pictures; there is only a last updated timestamp for the entire contact row, which changes
+ // frequently (for example, when they are called their "times_contacted" is incremented).
+ // Relying on such a spuriously updated timestamp would result in too frequent shortcut updates,
+ // so instead we just allow the icon to become stale in the case that the contact's photo is
+ // updated, and then rely on the job service to periodically force update it.
+ new DynamicShortcuts(jobService, new IconFactory(jobService)).updateIcons(); // Blocking
+ new PinnedShortcuts(jobService).refresh(); // Blocking
+
+ return params[0];
+ }
+
+ @Override
+ @MainThread
+ protected void onPostExecute(JobParameters params) {
+ Assert.isMainThread();
+ LogUtil.enterBlock("RefreshShortcutsTask.onPostExecute");
+
+ jobService.jobFinished(params, false /* needsReschedule */);
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutInfoFactory.java b/java/com/android/dialer/shortcuts/ShortcutInfoFactory.java
new file mode 100644
index 000000000..cf780bbd7
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutInfoFactory.java
@@ -0,0 +1,100 @@
+/*
+ * 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.shortcuts;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Creates {@link ShortcutInfo} objects (which are required by shortcut manager system service) from
+ * {@link DialerShortcut} objects (which are package-private convenience data structures).
+ *
+ * <p>The main work this factory does is create shortcut intents. It also delegates to the {@link
+ * IconFactory} to create icons.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N MR1
+final class ShortcutInfoFactory {
+
+ /** Key for the contact ID extra (a long) stored as part of the shortcut intent. */
+ static final String EXTRA_CONTACT_ID = "contactId";
+
+ private final Context context;
+ private final IconFactory iconFactory;
+
+ ShortcutInfoFactory(@NonNull Context context, IconFactory iconFactory) {
+ this.context = context;
+ this.iconFactory = iconFactory;
+ }
+
+ /**
+ * Builds a list {@link ShortcutInfo} objects from the provided collection of {@link
+ * DialerShortcut} objects. This primarily means setting the intent and adding the icon, which
+ * {@link DialerShortcut} objects do not hold.
+ */
+ @WorkerThread
+ @NonNull
+ List<ShortcutInfo> buildShortcutInfos(@NonNull Map<String, DialerShortcut> shortcutsById) {
+ Assert.isWorkerThread();
+ List<ShortcutInfo> shortcuts = new ArrayList<>(shortcutsById.size());
+ for (DialerShortcut shortcut : shortcutsById.values()) {
+ Intent intent = new Intent();
+ intent.setClassName(context, "com.android.dialer.shortcuts.CallContactActivity");
+ intent.setData(shortcut.getLookupUri());
+ intent.setAction("com.android.dialer.shortcuts.CALL_CONTACT");
+ intent.putExtra(EXTRA_CONTACT_ID, shortcut.getContactId());
+
+ ShortcutInfo.Builder shortcutInfo =
+ new ShortcutInfo.Builder(context, shortcut.getShortcutId())
+ .setIntent(intent)
+ .setShortLabel(shortcut.getShortLabel())
+ .setLongLabel(shortcut.getLongLabel())
+ .setIcon(iconFactory.create(shortcut));
+
+ if (shortcut.getRank() != DialerShortcut.NO_RANK) {
+ shortcutInfo.setRank(shortcut.getRank());
+ }
+ shortcuts.add(shortcutInfo.build());
+ }
+ return shortcuts;
+ }
+
+ /**
+ * Creates a copy of the provided {@link ShortcutInfo} but with an updated icon fetched from
+ * contacts provider.
+ */
+ @WorkerThread
+ @NonNull
+ ShortcutInfo withUpdatedIcon(ShortcutInfo info) {
+ Assert.isWorkerThread();
+ return new ShortcutInfo.Builder(context, info.getId())
+ .setIntent(info.getIntent())
+ .setShortLabel(info.getShortLabel())
+ .setLongLabel(info.getLongLabel())
+ .setRank(info.getRank())
+ .setIcon(iconFactory.create(info))
+ .build();
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutRefresher.java b/java/com/android/dialer/shortcuts/ShortcutRefresher.java
new file mode 100644
index 000000000..f5ff64874
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutRefresher.java
@@ -0,0 +1,86 @@
+/*
+ * 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.shortcuts;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.contacts.common.list.ContactEntry;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.FallibleAsyncTask;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Refreshes launcher shortcuts from UI components using provided list of contacts. */
+public final class ShortcutRefresher {
+
+ private static final AsyncTaskExecutor EXECUTOR = AsyncTaskExecutors.createThreadPoolExecutor();
+
+ /** Asynchronously updates launcher shortcuts using the provided list of contacts. */
+ @MainThread
+ public static void refresh(@NonNull Context context, List<ContactEntry> contacts) {
+ Assert.isMainThread();
+ Assert.isNotNull(context);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) {
+ return;
+ }
+
+ if (!Shortcuts.areDynamicShortcutsEnabled(context)) {
+ return;
+ }
+
+ //noinspection unchecked
+ EXECUTOR.submit(Task.ID, new Task(context), new ArrayList<>(contacts));
+ }
+
+ private static final class Task extends FallibleAsyncTask<List<ContactEntry>, Void, Void> {
+ private static final String ID = "ShortcutRefresher.Task";
+
+ private final Context context;
+
+ Task(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * @param params array containing exactly one element, the list of contacts from favorites
+ * tiles, ordered in tile order.
+ */
+ @SafeVarargs
+ @Override
+ @NonNull
+ @WorkerThread
+ protected final Void doInBackgroundFallible(List<ContactEntry>... params) {
+ Assert.isWorkerThread();
+ LogUtil.enterBlock("ShortcutRefresher.Task.doInBackground");
+
+ // Only dynamic shortcuts are maintained from UI components. Pinned shortcuts are maintained
+ // by the job scheduler. This is because a pinned contact may not necessarily still be in the
+ // favorites tiles, so refreshing it would require an additional database query. We don't want
+ // to incur the cost of that extra database query every time the favorites tiles change.
+ new DynamicShortcuts(context, new IconFactory(context)).refresh(params[0]); // Blocking
+
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutUsageReporter.java b/java/com/android/dialer/shortcuts/ShortcutUsageReporter.java
new file mode 100644
index 000000000..50130fc49
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutUsageReporter.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.shortcuts;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Reports outgoing calls as shortcut usage.
+ *
+ * <p>Note that all outgoing calls are considered shortcut usage, no matter where they are initiated
+ * from (i.e. from anywhere in the dialer app, or even from other apps).
+ *
+ * <p>This allows launcher applications to provide users with shortcut suggestions, even if the user
+ * isn't already using shortcuts.
+ */
+@TargetApi(VERSION_CODES.N_MR1) // Shortcuts introduced in N_MR1
+public class ShortcutUsageReporter {
+
+ private static final AsyncTaskExecutor EXECUTOR = AsyncTaskExecutors.createThreadPoolExecutor();
+
+ /**
+ * Called when an outgoing call is added to the call list in order to report outgoing calls as
+ * shortcut usage. This should be called exactly once for each outgoing call.
+ *
+ * <p>Asynchronously queries the contacts database for the contact's lookup key which corresponds
+ * to the provided phone number, and uses that to report shortcut usage.
+ *
+ * @param context used to access ShortcutManager system service
+ * @param phoneNumber the phone number being called
+ */
+ @MainThread
+ public static void onOutgoingCallAdded(@NonNull Context context, @Nullable String phoneNumber) {
+ Assert.isMainThread();
+ Assert.isNotNull(context);
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1 || TextUtils.isEmpty(phoneNumber)) {
+ return;
+ }
+
+ EXECUTOR.submit(Task.ID, new Task(context), phoneNumber);
+ }
+
+ private static final class Task extends AsyncTask<String, Void, Void> {
+ private static final String ID = "ShortcutUsageReporter.Task";
+
+ private final Context context;
+
+ public Task(Context context) {
+ this.context = context;
+ }
+
+ /** @param phoneNumbers array with exactly one non-empty phone number */
+ @Override
+ @WorkerThread
+ protected Void doInBackground(@NonNull String... phoneNumbers) {
+ Assert.isWorkerThread();
+
+ String lookupKey = queryForLookupKey(phoneNumbers[0]);
+ if (!TextUtils.isEmpty(lookupKey)) {
+ LogUtil.i("ShortcutUsageReporter.backgroundLogUsage", "%s", lookupKey);
+ ShortcutManager shortcutManager =
+ (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE);
+
+ // Note: There may not currently exist a shortcut with the provided key, but it is logged
+ // anyway, so that launcher applications at least have the information should the shortcut
+ // be created in the future.
+ shortcutManager.reportShortcutUsed(lookupKey);
+ }
+ return null;
+ }
+
+ @Nullable
+ @WorkerThread
+ private String queryForLookupKey(String phoneNumber) {
+ Assert.isWorkerThread();
+
+ if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("ShortcutUsageReporter.queryForLookupKey", "No contact permissions");
+ return null;
+ }
+
+ Uri uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(phoneNumber));
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(uri, new String[] {Contacts.LOOKUP_KEY}, null, null, null)) {
+
+ if (cursor == null || !cursor.moveToNext()) {
+ return null; // No contact for dialed number
+ }
+ // Arbitrarily use first result.
+ return cursor.getString(cursor.getColumnIndex(Contacts.LOOKUP_KEY));
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/Shortcuts.java b/java/com/android/dialer/shortcuts/Shortcuts.java
new file mode 100644
index 000000000..b6a7fa82a
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/Shortcuts.java
@@ -0,0 +1,34 @@
+/*
+ * 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.shortcuts;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** Checks if dynamic shortcuts should be enabled. */
+public class Shortcuts {
+
+ /** Key for boolean config value which determines whether or not to enable dynamic shortcuts. */
+ private static final String DYNAMIC_SHORTCUTS_ENABLED = "dynamic_shortcuts_enabled";
+
+ static boolean areDynamicShortcutsEnabled(@NonNull Context context) {
+ return ConfigProviderBindings.get(context).getBoolean(DYNAMIC_SHORTCUTS_ENABLED, true);
+ }
+
+ private Shortcuts() {}
+}
diff --git a/java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java b/java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java
new file mode 100644
index 000000000..4cfc4361c
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/ShortcutsJobScheduler.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.shortcuts;
+
+import android.content.Context;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * Schedules dialer shortcut jobs.
+ *
+ * <p>A {@link ConfigProvider} value controls whether the jobs which creates shortcuts should be
+ * scheduled or cancelled.
+ */
+public class ShortcutsJobScheduler {
+
+ @MainThread
+ public static void scheduleAllJobs(@NonNull Context context) {
+ LogUtil.enterBlock("ShortcutsJobScheduler.scheduleAllJobs");
+ Assert.isMainThread();
+
+ if (Shortcuts.areDynamicShortcutsEnabled(context)) {
+ LogUtil.i("ShortcutsJobScheduler.scheduleAllJobs", "enabling shortcuts");
+
+ PeriodicJobService.schedulePeriodicJob(context);
+ } else {
+ LogUtil.i("ShortcutsJobScheduler.scheduleAllJobs", "disabling shortcuts");
+
+ PeriodicJobService.cancelJob(context);
+ }
+ }
+}
diff --git a/java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml b/java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml
new file mode 100644
index 000000000..c06aec82f
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:bottom="2dp"
+ android:left="2dp"
+ android:right="2dp"
+ android:top="2dp">
+ <shape android:shape="oval">
+ <size
+ android:height="44dp"
+ android:width="44dp"/>
+ <solid android:color="@color/shortcut_add_contact_background_color"/>
+ </shape>
+ </item>
+
+ <item
+ android:bottom="12dp"
+ android:left="10dp"
+ android:right="14dp"
+ android:top="12dp">
+ <bitmap android:src="@drawable/quantum_ic_person_add_white_24"
+ android:tint="@color/shortcut_add_contact_foreground_color"/>
+ </item>
+</layer-list>
diff --git a/java/com/android/dialer/shortcuts/res/values/colors.xml b/java/com/android/dialer/shortcuts/res/values/colors.xml
new file mode 100644
index 000000000..e20309b56
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/colors.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <color name="shortcut_add_contact_foreground_color">#2A56C6</color>
+ <color name="shortcut_add_contact_background_color">#f5f5f5</color>
+</resources>
diff --git a/java/com/android/dialer/shortcuts/res/values/dimens.xml b/java/com/android/dialer/shortcuts/res/values/dimens.xml
new file mode 100644
index 000000000..232125653
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <dimen name="launcher_shortcut_icon_size">48dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/shortcuts/res/values/strings.xml b/java/com/android/dialer/shortcuts/res/values/strings.xml
new file mode 100644
index 000000000..1e2c87f12
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/strings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <!-- Text to display in launcher shortcut for adding a new contact. Short version. [CHAR LIMIT=10] -->
+ <string name="dialer_shortcut_add_contact_short">New contact</string>
+
+ <!-- Text to display in launcher shortcut for adding a new contact. Long version. [CHAR LIMIT=25] -->
+ <string name="dialer_shortcut_add_contact_long">New contact</string>
+
+ <!-- Message to display when the user taps a pinned launcher shortcut (on a
+ homescreen) which has been disabled. A shortcut may be disabled if the
+ contact has been deleted or if it is invalid for some other reason. [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_disabled_message">Shortcut not working. Drag to remove.</string>
+
+ <!-- Error message to display when a tapping a shortcut fails because the specified contact can't
+ be found or doesn't have any phone numbers. [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_contact_not_found_or_has_no_number">Contact no longer available.</string>
+
+ <!-- Error message to display when a tapping a shortcut fails because contact permissions are
+ missing. [CHAR LIMIT=70] -->
+ <string name="dialer_shortcut_no_permissions">Cannot call without contact permissions.</string>
+
+</resources>
diff --git a/java/com/android/dialer/shortcuts/res/values/themes.xml b/java/com/android/dialer/shortcuts/res/values/themes.xml
new file mode 100644
index 000000000..085854d89
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/values/themes.xml
@@ -0,0 +1,39 @@
+<resources>
+ <!--
+ ~ 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
+ -->
+
+ <!-- CallContactsActivity is an invisible trampoline activity for launcher shortcuts to jump into
+ the calling activity. When the user taps a shortcut they will be taken to either the phone
+ number disambiguation dialog or directly into the incall UI via this activity, but this
+ activity itself should be completely transparent to the user.
+
+ Note that this must inherit from Theme.AppCompat. We inherit from Theme.AppCompat.Light so
+ that the colors of the disambiguation dialog match the colors when it is shown via the
+ favorites tiles tab. -->
+ <style name="CallContactsTheme" parent="Theme.AppCompat.Light">
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:backgroundDimEnabled">false</item>
+ <item name="android:windowBackground">@null</item>
+ <item name="android:windowFrame">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowIsFloating">true</item>
+ <item name="android:windowActionBar">false</item>
+ <item name="android:windowDisablePreview">true</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
new file mode 100644
index 000000000..5e8f58d1f
--- /dev/null
+++ b/java/com/android/dialer/shortcuts/res/xml/shortcuts.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
+ <shortcut
+ android:enabled="true"
+ android:icon="@drawable/ic_shortcut_add_contact"
+ android:shortcutId="dialer-shortcut-add-contact"
+ android:shortcutLongLabel="@string/dialer_shortcut_add_contact_long"
+ android:shortcutShortLabel="@string/dialer_shortcut_add_contact_short">
+
+ <intent
+ android:action="android.intent.action.INSERT"
+ android:data="content://com.android.contacts/contacts"
+ android:targetPackage="com.google.android.contacts"
+ android:targetClass="com.android.contacts.activities.CompactContactEditorActivity"/>
+ </shortcut>
+</shortcuts>
diff --git a/java/com/android/dialer/simulator/Simulator.java b/java/com/android/dialer/simulator/Simulator.java
new file mode 100644
index 000000000..78058a48f
--- /dev/null
+++ b/java/com/android/dialer/simulator/Simulator.java
@@ -0,0 +1,27 @@
+/*
+ * 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.simulator;
+
+import android.content.Context;
+import android.view.ActionProvider;
+
+/** Used to add menu items to the Dialer menu to test the app using simulated calls and data. */
+public interface Simulator {
+ boolean shouldShow();
+
+ ActionProvider getActionProvider(Context context);
+}
diff --git a/java/com/android/dialer/simulator/impl/AndroidManifest.xml b/java/com/android/dialer/simulator/impl/AndroidManifest.xml
new file mode 100644
index 000000000..a30504d3f
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.simulator.impl">
+
+ <application>
+
+ <service
+ android:exported="true"
+ android:name=".SimulatorConnectionService"
+ android:permission="android.permission.BIND_CONNECTION_SERVICE">
+ <intent-filter>
+ <action android:name="android.telecomm.ConnectionService"/>
+ </intent-filter>
+ </service>
+
+ </application>
+
+</manifest>
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
new file mode 100644
index 000000000..591819819
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java
@@ -0,0 +1,160 @@
+/*
+ * 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.simulator.impl;
+
+import android.support.annotation.NonNull;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_SimulatorCallLog_CallEntry extends SimulatorCallLog.CallEntry {
+
+ private final String number;
+ private final int type;
+ private final int presentation;
+ private final long timeMillis;
+
+ private AutoValue_SimulatorCallLog_CallEntry(
+ String number,
+ int type,
+ int presentation,
+ long timeMillis) {
+ this.number = number;
+ this.type = type;
+ this.presentation = presentation;
+ this.timeMillis = timeMillis;
+ }
+
+ @NonNull
+ @Override
+ String getNumber() {
+ return number;
+ }
+
+ @Override
+ int getType() {
+ return type;
+ }
+
+ @Override
+ int getPresentation() {
+ return presentation;
+ }
+
+ @Override
+ long getTimeMillis() {
+ return timeMillis;
+ }
+
+ @Override
+ public String toString() {
+ return "CallEntry{"
+ + "number=" + number + ", "
+ + "type=" + type + ", "
+ + "presentation=" + presentation + ", "
+ + "timeMillis=" + timeMillis
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SimulatorCallLog.CallEntry) {
+ SimulatorCallLog.CallEntry that = (SimulatorCallLog.CallEntry) o;
+ return (this.number.equals(that.getNumber()))
+ && (this.type == that.getType())
+ && (this.presentation == that.getPresentation())
+ && (this.timeMillis == that.getTimeMillis());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.number.hashCode();
+ h *= 1000003;
+ h ^= this.type;
+ h *= 1000003;
+ h ^= this.presentation;
+ h *= 1000003;
+ h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
+ return h;
+ }
+
+ static final class Builder extends SimulatorCallLog.CallEntry.Builder {
+ private String number;
+ private Integer type;
+ private Integer presentation;
+ private Long timeMillis;
+ Builder() {
+ }
+ private Builder(SimulatorCallLog.CallEntry source) {
+ this.number = source.getNumber();
+ this.type = source.getType();
+ this.presentation = source.getPresentation();
+ this.timeMillis = source.getTimeMillis();
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setNumber(String number) {
+ this.number = number;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setType(int type) {
+ this.type = type;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setPresentation(int presentation) {
+ this.presentation = presentation;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry.Builder setTimeMillis(long timeMillis) {
+ this.timeMillis = timeMillis;
+ return this;
+ }
+ @Override
+ SimulatorCallLog.CallEntry build() {
+ String missing = "";
+ if (this.number == null) {
+ missing += " number";
+ }
+ if (this.type == null) {
+ missing += " type";
+ }
+ if (this.presentation == null) {
+ missing += " presentation";
+ }
+ if (this.timeMillis == null) {
+ missing += " timeMillis";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_SimulatorCallLog_CallEntry(
+ this.number,
+ this.type,
+ this.presentation,
+ this.timeMillis);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
new file mode 100644
index 000000000..00295f359
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java
@@ -0,0 +1,231 @@
+/*
+ * 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.simulator.impl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_SimulatorContacts_Contact extends SimulatorContacts.Contact {
+
+ private final String accountType;
+ private final String accountName;
+ private final String name;
+ private final boolean isStarred;
+ private final ByteArrayOutputStream photoStream;
+ private final List<SimulatorContacts.PhoneNumber> phoneNumbers;
+ private final List<SimulatorContacts.Email> emails;
+
+ private AutoValue_SimulatorContacts_Contact(
+ String accountType,
+ String accountName,
+ @Nullable String name,
+ boolean isStarred,
+ @Nullable ByteArrayOutputStream photoStream,
+ List<SimulatorContacts.PhoneNumber> phoneNumbers,
+ List<SimulatorContacts.Email> emails) {
+ this.accountType = accountType;
+ this.accountName = accountName;
+ this.name = name;
+ this.isStarred = isStarred;
+ this.photoStream = photoStream;
+ this.phoneNumbers = phoneNumbers;
+ this.emails = emails;
+ }
+
+ @NonNull
+ @Override
+ String getAccountType() {
+ return accountType;
+ }
+
+ @NonNull
+ @Override
+ String getAccountName() {
+ return accountName;
+ }
+
+ @Nullable
+ @Override
+ String getName() {
+ return name;
+ }
+
+ @Override
+ boolean getIsStarred() {
+ return isStarred;
+ }
+
+ @Nullable
+ @Override
+ ByteArrayOutputStream getPhotoStream() {
+ return photoStream;
+ }
+
+ @NonNull
+ @Override
+ List<SimulatorContacts.PhoneNumber> getPhoneNumbers() {
+ return phoneNumbers;
+ }
+
+ @NonNull
+ @Override
+ List<SimulatorContacts.Email> getEmails() {
+ return emails;
+ }
+
+ @Override
+ public String toString() {
+ return "Contact{"
+ + "accountType=" + accountType + ", "
+ + "accountName=" + accountName + ", "
+ + "name=" + name + ", "
+ + "isStarred=" + isStarred + ", "
+ + "photoStream=" + photoStream + ", "
+ + "phoneNumbers=" + phoneNumbers + ", "
+ + "emails=" + emails
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SimulatorContacts.Contact) {
+ SimulatorContacts.Contact that = (SimulatorContacts.Contact) o;
+ return (this.accountType.equals(that.getAccountType()))
+ && (this.accountName.equals(that.getAccountName()))
+ && ((this.name == null) ? (that.getName() == null) : this.name.equals(that.getName()))
+ && (this.isStarred == that.getIsStarred())
+ && ((this.photoStream == null) ? (that.getPhotoStream() == null) : this.photoStream.equals(that.getPhotoStream()))
+ && (this.phoneNumbers.equals(that.getPhoneNumbers()))
+ && (this.emails.equals(that.getEmails()));
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.accountType.hashCode();
+ h *= 1000003;
+ h ^= this.accountName.hashCode();
+ h *= 1000003;
+ h ^= (name == null) ? 0 : this.name.hashCode();
+ h *= 1000003;
+ h ^= this.isStarred ? 1231 : 1237;
+ h *= 1000003;
+ h ^= (photoStream == null) ? 0 : this.photoStream.hashCode();
+ h *= 1000003;
+ h ^= this.phoneNumbers.hashCode();
+ h *= 1000003;
+ h ^= this.emails.hashCode();
+ return h;
+ }
+
+ static final class Builder extends SimulatorContacts.Contact.Builder {
+ private String accountType;
+ private String accountName;
+ private String name;
+ private Boolean isStarred;
+ private ByteArrayOutputStream photoStream;
+ private List<SimulatorContacts.PhoneNumber> phoneNumbers;
+ private List<SimulatorContacts.Email> emails;
+ Builder() {
+ }
+ private Builder(SimulatorContacts.Contact source) {
+ this.accountType = source.getAccountType();
+ this.accountName = source.getAccountName();
+ this.name = source.getName();
+ this.isStarred = source.getIsStarred();
+ this.photoStream = source.getPhotoStream();
+ this.phoneNumbers = source.getPhoneNumbers();
+ this.emails = source.getEmails();
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setAccountType(String accountType) {
+ this.accountType = accountType;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setAccountName(String accountName) {
+ this.accountName = accountName;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setName(@Nullable String name) {
+ this.name = name;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setIsStarred(boolean isStarred) {
+ this.isStarred = isStarred;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setPhotoStream(@Nullable ByteArrayOutputStream photoStream) {
+ this.photoStream = photoStream;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setPhoneNumbers(List<SimulatorContacts.PhoneNumber> phoneNumbers) {
+ this.phoneNumbers = phoneNumbers;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact.Builder setEmails(List<SimulatorContacts.Email> emails) {
+ this.emails = emails;
+ return this;
+ }
+ @Override
+ SimulatorContacts.Contact build() {
+ String missing = "";
+ if (this.accountType == null) {
+ missing += " accountType";
+ }
+ if (this.accountName == null) {
+ missing += " accountName";
+ }
+ if (this.isStarred == null) {
+ missing += " isStarred";
+ }
+ if (this.phoneNumbers == null) {
+ missing += " phoneNumbers";
+ }
+ if (this.emails == null) {
+ missing += " emails";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_SimulatorContacts_Contact(
+ this.accountType,
+ this.accountName,
+ this.name,
+ this.isStarred,
+ this.photoStream,
+ this.phoneNumbers,
+ this.emails);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
new file mode 100644
index 000000000..58934801c
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java
@@ -0,0 +1,184 @@
+/*
+ * 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.simulator.impl;
+
+import android.support.annotation.NonNull;
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_SimulatorVoicemail_Voicemail extends SimulatorVoicemail.Voicemail {
+
+ private final String phoneNumber;
+ private final String transcription;
+ private final long durationSeconds;
+ private final long timeMillis;
+ private final boolean isRead;
+
+ private AutoValue_SimulatorVoicemail_Voicemail(
+ String phoneNumber,
+ String transcription,
+ long durationSeconds,
+ long timeMillis,
+ boolean isRead) {
+ this.phoneNumber = phoneNumber;
+ this.transcription = transcription;
+ this.durationSeconds = durationSeconds;
+ this.timeMillis = timeMillis;
+ this.isRead = isRead;
+ }
+
+ @NonNull
+ @Override
+ String getPhoneNumber() {
+ return phoneNumber;
+ }
+
+ @NonNull
+ @Override
+ String getTranscription() {
+ return transcription;
+ }
+
+ @Override
+ long getDurationSeconds() {
+ return durationSeconds;
+ }
+
+ @Override
+ long getTimeMillis() {
+ return timeMillis;
+ }
+
+ @Override
+ boolean getIsRead() {
+ return isRead;
+ }
+
+ @Override
+ public String toString() {
+ return "Voicemail{"
+ + "phoneNumber=" + phoneNumber + ", "
+ + "transcription=" + transcription + ", "
+ + "durationSeconds=" + durationSeconds + ", "
+ + "timeMillis=" + timeMillis + ", "
+ + "isRead=" + isRead
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof SimulatorVoicemail.Voicemail) {
+ SimulatorVoicemail.Voicemail that = (SimulatorVoicemail.Voicemail) o;
+ return (this.phoneNumber.equals(that.getPhoneNumber()))
+ && (this.transcription.equals(that.getTranscription()))
+ && (this.durationSeconds == that.getDurationSeconds())
+ && (this.timeMillis == that.getTimeMillis())
+ && (this.isRead == that.getIsRead());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.phoneNumber.hashCode();
+ h *= 1000003;
+ h ^= this.transcription.hashCode();
+ h *= 1000003;
+ h ^= (this.durationSeconds >>> 32) ^ this.durationSeconds;
+ h *= 1000003;
+ h ^= (this.timeMillis >>> 32) ^ this.timeMillis;
+ h *= 1000003;
+ h ^= this.isRead ? 1231 : 1237;
+ return h;
+ }
+
+ static final class Builder extends SimulatorVoicemail.Voicemail.Builder {
+ private String phoneNumber;
+ private String transcription;
+ private Long durationSeconds;
+ private Long timeMillis;
+ private Boolean isRead;
+ Builder() {
+ }
+ private Builder(SimulatorVoicemail.Voicemail source) {
+ this.phoneNumber = source.getPhoneNumber();
+ this.transcription = source.getTranscription();
+ this.durationSeconds = source.getDurationSeconds();
+ this.timeMillis = source.getTimeMillis();
+ this.isRead = source.getIsRead();
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setPhoneNumber(String phoneNumber) {
+ this.phoneNumber = phoneNumber;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setTranscription(String transcription) {
+ this.transcription = transcription;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setDurationSeconds(long durationSeconds) {
+ this.durationSeconds = durationSeconds;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setTimeMillis(long timeMillis) {
+ this.timeMillis = timeMillis;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail.Builder setIsRead(boolean isRead) {
+ this.isRead = isRead;
+ return this;
+ }
+ @Override
+ SimulatorVoicemail.Voicemail build() {
+ String missing = "";
+ if (this.phoneNumber == null) {
+ missing += " phoneNumber";
+ }
+ if (this.transcription == null) {
+ missing += " transcription";
+ }
+ if (this.durationSeconds == null) {
+ missing += " durationSeconds";
+ }
+ if (this.timeMillis == null) {
+ missing += " timeMillis";
+ }
+ if (this.isRead == null) {
+ missing += " isRead";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_SimulatorVoicemail_Voicemail(
+ this.phoneNumber,
+ this.transcription,
+ this.durationSeconds,
+ this.timeMillis,
+ this.isRead);
+ }
+ }
+
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java b/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java
new file mode 100644
index 000000000..6cd573361
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorActionProvider.java
@@ -0,0 +1,88 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+import android.view.ActionProvider;
+import android.view.MenuItem;
+import android.view.SubMenu;
+import android.view.View;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** Implements the simulator submenu. */
+final class SimulatorActionProvider extends ActionProvider {
+ @NonNull private final Context context;
+
+ public SimulatorActionProvider(@NonNull Context context) {
+ super(Assert.isNotNull(context));
+ this.context = context;
+ }
+
+ @Override
+ public View onCreateActionView() {
+ LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(null)");
+ return null;
+ }
+
+ @Override
+ public View onCreateActionView(MenuItem forItem) {
+ LogUtil.enterBlock("SimulatorActionProvider.onCreateActionView(MenuItem)");
+ return null;
+ }
+
+ @Override
+ public boolean hasSubMenu() {
+ LogUtil.enterBlock("SimulatorActionProvider.hasSubMenu");
+ return true;
+ }
+
+ @Override
+ public void onPrepareSubMenu(SubMenu subMenu) {
+ super.onPrepareSubMenu(subMenu);
+ LogUtil.enterBlock("SimulatorActionProvider.onPrepareSubMenu");
+ subMenu.clear();
+ subMenu
+ .add("Add call")
+ .setOnMenuItemClickListener(
+ (item) -> {
+ SimulatorVoiceCall.addNewIncomingCall(context);
+ return true;
+ });
+ subMenu
+ .add("Populate database")
+ .setOnMenuItemClickListener(
+ (item) -> {
+ populateDatabase();
+ return true;
+ });
+ }
+
+ private void populateDatabase() {
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ public Void doInBackground(Void... params) {
+ SimulatorContacts.populateContacts(context);
+ SimulatorCallLog.populateCallLog(context);
+ SimulatorVoicemail.populateVoicemail(context);
+ return null;
+ }
+ }.execute();
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorCallLog.java b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
new file mode 100644
index 000000000..9ace047d0
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorCallLog.java
@@ -0,0 +1,139 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/** Populates the device database with call log entries. */
+final class SimulatorCallLog {
+ // Phone numbers from https://www.google.com/about/company/facts/locations/
+ private static final CallEntry.Builder[] SIMPLE_CALL_LOG = {
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder()
+ .setType(Calls.MISSED_TYPE)
+ .setNumber("")
+ .setPresentation(Calls.PRESENTATION_UNKNOWN),
+ CallEntry.builder().setType(Calls.REJECTED_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder()
+ .setType(Calls.MISSED_TYPE)
+ .setNumber("1234")
+ .setPresentation(Calls.PRESENTATION_RESTRICTED),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder().setType(Calls.BLOCKED_TYPE).setNumber("+1-302-6365454"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("(425) 739-5600"),
+ CallEntry.builder().setType(Calls.ANSWERED_EXTERNALLY_TYPE).setNumber("(425) 739-5600"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("+1 (425) 739-5600"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("739-5600"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("711"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("711"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("(425) 739-5600"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("+44 (0) 20 7031 3000"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1-650-2530000"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1 303-245-0086;123,456"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1 303-245-0086"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+1-650-2530000"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("650-2530000"),
+ CallEntry.builder().setType(Calls.REJECTED_TYPE).setNumber("2530000"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+1 404-487-9000"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+61 2 9374 4001"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("+33 (0)1 42 68 53 00"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("972-74-746-6245"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+971 4 4509500"),
+ CallEntry.builder().setType(Calls.INCOMING_TYPE).setNumber("+971 4 4509500"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("55-31-2128-6800"),
+ CallEntry.builder().setType(Calls.MISSED_TYPE).setNumber("611"),
+ CallEntry.builder().setType(Calls.OUTGOING_TYPE).setNumber("*86 512-343-5283"),
+ };
+
+ @WorkerThread
+ public static void populateCallLog(@NonNull Context context) {
+ Assert.isWorkerThread();
+ ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+ // Do this 4 times to make the call log 4 times bigger.
+ long timeMillis = System.currentTimeMillis();
+ for (int i = 0; i < 4; i++) {
+ for (CallEntry.Builder builder : SIMPLE_CALL_LOG) {
+ CallEntry callEntry = builder.setTimeMillis(timeMillis).build();
+ operations.add(
+ ContentProviderOperation.newInsert(Calls.CONTENT_URI)
+ .withValues(callEntry.getAsContentValues())
+ .withYieldAllowed(true)
+ .build());
+ timeMillis -= TimeUnit.HOURS.toMillis(1);
+ }
+ }
+ try {
+ context.getContentResolver().applyBatch(CallLog.AUTHORITY, operations);
+ } catch (RemoteException | OperationApplicationException e) {
+ Assert.fail("error adding call entries: " + e);
+ }
+ }
+
+
+ abstract static class CallEntry {
+ @NonNull
+ abstract String getNumber();
+
+ abstract int getType();
+
+ abstract int getPresentation();
+
+ abstract long getTimeMillis();
+
+ static Builder builder() {
+ return new AutoValue_SimulatorCallLog_CallEntry.Builder()
+ .setPresentation(Calls.PRESENTATION_ALLOWED);
+ }
+
+ ContentValues getAsContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(Calls.TYPE, getType());
+ values.put(Calls.NUMBER, getNumber());
+ values.put(Calls.NUMBER_PRESENTATION, getPresentation());
+ values.put(Calls.DATE, getTimeMillis());
+ return values;
+ }
+
+
+ abstract static class Builder {
+ abstract Builder setNumber(@NonNull String number);
+
+ abstract Builder setType(int type);
+
+ abstract Builder setPresentation(int presentation);
+
+ abstract Builder setTimeMillis(long timeMillis);
+
+ abstract CallEntry build();
+ }
+ }
+
+ private SimulatorCallLog() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnection.java b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
new file mode 100644
index 000000000..12d095890
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnection.java
@@ -0,0 +1,56 @@
+/*
+ * 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.simulator.impl;
+
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import com.android.dialer.common.LogUtil;
+
+/** Represents a single phone call on the device. */
+final class SimulatorConnection extends Connection {
+
+ @Override
+ public void onAnswer() {
+ LogUtil.enterBlock("SimulatorConnection.onAnswer");
+ setActive();
+ }
+
+ @Override
+ public void onReject() {
+ LogUtil.enterBlock("SimulatorConnection.onReject");
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ }
+
+ @Override
+ public void onHold() {
+ LogUtil.enterBlock("SimulatorConnection.onHold");
+ setOnHold();
+ }
+
+ @Override
+ public void onUnhold() {
+ LogUtil.enterBlock("SimulatorConnection.onUnhold");
+ setActive();
+ }
+
+ @Override
+ public void onDisconnect() {
+ LogUtil.enterBlock("SimulatorConnection.onDisconnect");
+ setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+ destroy();
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
new file mode 100644
index 000000000..322360786
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorConnectionService.java
@@ -0,0 +1,87 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.ConnectionService;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Simple connection provider to create an incoming call. This is useful for emulators. */
+public final class SimulatorConnectionService extends ConnectionService {
+
+ private static final String PHONE_ACCOUNT_ID = "SIMULATOR_ACCOUNT_ID";
+
+ public static void register(Context context) {
+ LogUtil.enterBlock("SimulatorConnectionService.register");
+ context.getSystemService(TelecomManager.class).registerPhoneAccount(buildPhoneAccount(context));
+ }
+
+ private static PhoneAccount buildPhoneAccount(Context context) {
+ PhoneAccount.Builder builder =
+ new PhoneAccount.Builder(
+ getConnectionServiceHandle(context), "Simulator connection service");
+ List<String> uriSchemes = new ArrayList<>();
+ uriSchemes.add(PhoneAccount.SCHEME_TEL);
+
+ return builder
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
+ .setShortDescription("Simulator Connection Service")
+ .setSupportedUriSchemes(uriSchemes)
+ .build();
+ }
+
+ public static PhoneAccountHandle getConnectionServiceHandle(Context context) {
+ return new PhoneAccountHandle(
+ new ComponentName(context, SimulatorConnectionService.class), PHONE_ACCOUNT_ID);
+ }
+
+ private static Uri getPhoneNumber(ConnectionRequest request) {
+ String phoneNumber = request.getExtras().getString(TelephonyManager.EXTRA_INCOMING_NUMBER);
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, phoneNumber, null);
+ }
+
+ @Override
+ public Connection onCreateOutgoingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ LogUtil.i(
+ "SimulatorConnectionService.onCreateOutgoingConnection",
+ "outgoing calls not supported yet");
+ return null;
+ }
+
+ @Override
+ public Connection onCreateIncomingConnection(
+ PhoneAccountHandle phoneAccount, ConnectionRequest request) {
+ LogUtil.enterBlock("SimulatorConnectionService.onCreateIncomingConnection");
+ SimulatorConnection connection = new SimulatorConnection();
+ connection.setRinging();
+ connection.setAddress(getPhoneNumber(request), TelecomManager.PRESENTATION_ALLOWED);
+ connection.setConnectionCapabilities(
+ Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
+ return connection;
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorContacts.java b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
new file mode 100644
index 000000000..89315094a
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorContacts.java
@@ -0,0 +1,319 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Populates the device database with contacts. */
+final class SimulatorContacts {
+ // Phone numbers from https://www.google.com/about/company/facts/locations/
+ private static final Contact[] SIMPLE_CONTACTS = {
+ // US, contact with e164 number.
+ Contact.builder()
+ .setName("Michelangelo")
+ .addPhoneNumber(new PhoneNumber("+1-302-6365454", Phone.TYPE_MOBILE))
+ .addEmail(new Email("m@example.com"))
+ .setIsStarred(true)
+ .setOrangePhoto()
+ .build(),
+ // US, contact with a non-e164 number.
+ Contact.builder()
+ .setName("Leonardo da Vinci")
+ .addPhoneNumber(new PhoneNumber("(425) 739-5600", Phone.TYPE_MOBILE))
+ .addEmail(new Email("l@example.com"))
+ .setIsStarred(true)
+ .setBluePhoto()
+ .build(),
+ // UK, number where the (0) should be dropped.
+ Contact.builder()
+ .setName("Raphael")
+ .addPhoneNumber(new PhoneNumber("+44 (0) 20 7031 3000", Phone.TYPE_MOBILE))
+ .addEmail(new Email("r@example.com"))
+ .setIsStarred(true)
+ .setRedPhoto()
+ .build(),
+ // US and Australia, contact with a long name and multiple phone numbers.
+ Contact.builder()
+ .setName("Donatello di Niccolò di Betto Bardi")
+ .addPhoneNumber(new PhoneNumber("+1-650-2530000", Phone.TYPE_HOME))
+ .addPhoneNumber(new PhoneNumber("+1 404-487-9000", Phone.TYPE_WORK))
+ .addPhoneNumber(new PhoneNumber("+61 2 9374 4001", Phone.TYPE_FAX_HOME))
+ .setIsStarred(true)
+ .setPurplePhoto()
+ .build(),
+ // US, phone number shared with another contact and 2nd phone number with wait and pause.
+ Contact.builder()
+ .setName("Splinter")
+ .addPhoneNumber(new PhoneNumber("+1-650-2530000", Phone.TYPE_HOME))
+ .addPhoneNumber(new PhoneNumber("+1 303-245-0086;123,456", Phone.TYPE_WORK))
+ .build(),
+ // France, number with Japanese name.
+ Contact.builder()
+ .setName("スパイク・スピーゲル")
+ .addPhoneNumber(new PhoneNumber("+33 (0)1 42 68 53 00", Phone.TYPE_MOBILE))
+ .build(),
+ // Israel, RTL name and non-e164 number.
+ Contact.builder()
+ .setName("עקב אריה טברסק")
+ .addPhoneNumber(new PhoneNumber("+33 (0)1 42 68 53 00", Phone.TYPE_MOBILE))
+ .build(),
+ // UAE, RTL name.
+ Contact.builder()
+ .setName("سلام دنیا")
+ .addPhoneNumber(new PhoneNumber("+971 4 4509500", Phone.TYPE_MOBILE))
+ .build(),
+ // Brazil, contact with no name.
+ Contact.builder()
+ .addPhoneNumber(new PhoneNumber("+55-31-2128-6800", Phone.TYPE_MOBILE))
+ .build(),
+ // Short number, contact with no name.
+ Contact.builder().addPhoneNumber(new PhoneNumber("611", Phone.TYPE_MOBILE)).build(),
+ // US, number with an anonymous prefix.
+ Contact.builder()
+ .setName("Anonymous")
+ .addPhoneNumber(new PhoneNumber("*86 512-343-5283", Phone.TYPE_MOBILE))
+ .build(),
+ // None, contact with no phone number.
+ Contact.builder()
+ .setName("No Phone Number")
+ .addEmail(new Email("no@example.com"))
+ .setIsStarred(true)
+ .build(),
+ };
+
+ @WorkerThread
+ static void populateContacts(@NonNull Context context) {
+ Assert.isWorkerThread();
+ ArrayList<ContentProviderOperation> operations = new ArrayList<>();
+ for (Contact contact : SIMPLE_CONTACTS) {
+ addContact(contact, operations);
+ }
+ try {
+ context.getContentResolver().applyBatch(ContactsContract.AUTHORITY, operations);
+ } catch (RemoteException | OperationApplicationException e) {
+ Assert.fail("error adding contacts: " + e);
+ }
+ }
+
+ private static void addContact(Contact contact, List<ContentProviderOperation> operations) {
+ int index = operations.size();
+
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
+ .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, contact.getAccountType())
+ .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, contact.getAccountName())
+ .withValue(ContactsContract.RawContacts.STARRED, contact.getIsStarred())
+ .withYieldAllowed(true)
+ .build());
+
+ if (!TextUtils.isEmpty(contact.getName())) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
+ .withValue(
+ ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, contact.getName())
+ .build());
+ }
+
+ if (contact.getPhotoStream() != null) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)
+ .withValue(
+ ContactsContract.CommonDataKinds.Photo.PHOTO,
+ contact.getPhotoStream().toByteArray())
+ .build());
+ }
+
+ for (PhoneNumber phoneNumber : contact.getPhoneNumbers()) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phoneNumber.value)
+ .withValue(ContactsContract.CommonDataKinds.Phone.TYPE, phoneNumber.type)
+ .withValue(ContactsContract.CommonDataKinds.Phone.LABEL, phoneNumber.label)
+ .build());
+ }
+
+ for (Email email : contact.getEmails()) {
+ operations.add(
+ ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
+ .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, index)
+ .withValue(
+ ContactsContract.Data.MIMETYPE,
+ ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)
+ .withValue(ContactsContract.CommonDataKinds.Email.DATA, email.value)
+ .withValue(ContactsContract.CommonDataKinds.Email.TYPE, email.type)
+ .withValue(ContactsContract.CommonDataKinds.Email.LABEL, email.label)
+ .build());
+ }
+ }
+
+
+ abstract static class Contact {
+ @NonNull
+ abstract String getAccountType();
+
+ @NonNull
+ abstract String getAccountName();
+
+ @Nullable
+ abstract String getName();
+
+ abstract boolean getIsStarred();
+
+ @Nullable
+ abstract ByteArrayOutputStream getPhotoStream();
+
+ @NonNull
+ abstract List<PhoneNumber> getPhoneNumbers();
+
+ @NonNull
+ abstract List<Email> getEmails();
+
+ static Builder builder() {
+ return new AutoValue_SimulatorContacts_Contact.Builder()
+ .setAccountType("com.google")
+ .setAccountName("foo@example")
+ .setIsStarred(false)
+ .setPhoneNumbers(new ArrayList<>())
+ .setEmails(new ArrayList<>());
+ }
+
+
+ abstract static class Builder {
+ @NonNull private final List<PhoneNumber> phoneNumbers = new ArrayList<>();
+ @NonNull private final List<Email> emails = new ArrayList<>();
+
+ abstract Builder setAccountType(@NonNull String accountType);
+
+ abstract Builder setAccountName(@NonNull String accountName);
+
+ abstract Builder setName(@NonNull String name);
+
+ abstract Builder setIsStarred(boolean isStarred);
+
+ abstract Builder setPhotoStream(ByteArrayOutputStream photoStream);
+
+ abstract Builder setPhoneNumbers(@NonNull List<PhoneNumber> phoneNumbers);
+
+ abstract Builder setEmails(@NonNull List<Email> emails);
+
+ abstract Contact build();
+
+ Builder addPhoneNumber(PhoneNumber phoneNumber) {
+ phoneNumbers.add(phoneNumber);
+ return setPhoneNumbers(phoneNumbers);
+ }
+
+ Builder addEmail(Email email) {
+ emails.add(email);
+ return setEmails(emails);
+ }
+
+ Builder setRedPhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0xe3, 0x33, 0x1c)));
+ return this;
+ }
+
+ Builder setBluePhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0x00, 0xaa, 0xe6)));
+ return this;
+ }
+
+ Builder setOrangePhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0xea, 0x95, 0x00)));
+ return this;
+ }
+
+ Builder setPurplePhoto() {
+ setPhotoStream(getPhotoStreamWithColor(Color.rgb(0x99, 0x5a, 0xa0)));
+ return this;
+ }
+
+ /** Creates a contact photo with a green background and a circle of the given color. */
+ private static ByteArrayOutputStream getPhotoStreamWithColor(int color) {
+ int width = 300;
+ int height = 300;
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ canvas.drawColor(Color.argb(0xff, 0x4c, 0x9c, 0x23));
+ Paint paint = new Paint();
+ paint.setColor(color);
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawCircle(width / 2, height / 2, width / 3, paint);
+
+ ByteArrayOutputStream photoStream = new ByteArrayOutputStream();
+ bitmap.compress(Bitmap.CompressFormat.PNG, 75, photoStream);
+ return photoStream;
+ }
+ }
+ }
+
+ static class PhoneNumber {
+ public final String value;
+ public final int type;
+ public final String label;
+
+ PhoneNumber(String value, int type) {
+ this.value = value;
+ this.type = type;
+ label = "simulator phone number";
+ }
+ }
+
+ static class Email {
+ public final String value;
+ public final int type;
+ public final String label;
+
+ Email(String simpleEmail) {
+ value = simpleEmail;
+ type = ContactsContract.CommonDataKinds.Email.TYPE_WORK;
+ label = "simulator email";
+ }
+ }
+
+ private SimulatorContacts() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorModule.java b/java/com/android/dialer/simulator/impl/SimulatorModule.java
new file mode 100644
index 000000000..0f8ad3954
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorModule.java
@@ -0,0 +1,34 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.Context;
+import android.view.ActionProvider;
+import com.android.dialer.simulator.Simulator;
+
+/** The entry point for the simulator module. */
+public final class SimulatorModule implements Simulator {
+ @Override
+ public boolean shouldShow() {
+ return true;
+ }
+
+ @Override
+ public ActionProvider getActionProvider(Context context) {
+ return new SimulatorActionProvider(context);
+ }
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
new file mode 100644
index 000000000..39c1d02a5
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java
@@ -0,0 +1,47 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** Utilities to simulate phone calls. */
+final class SimulatorVoiceCall {
+ public static void addNewIncomingCall(@NonNull Context context) {
+ LogUtil.enterBlock("SimulatorVoiceCall.addNewIncomingCall");
+ SimulatorConnectionService.register(context);
+
+ Bundle bundle = new Bundle();
+ // Set the caller ID to the Google London office.
+ bundle.putString(TelephonyManager.EXTRA_INCOMING_NUMBER, "+44 (0) 20 7031 3000");
+ try {
+ context
+ .getSystemService(TelecomManager.class)
+ .addNewIncomingCall(
+ SimulatorConnectionService.getConnectionServiceHandle(context), bundle);
+ } catch (SecurityException e) {
+ Assert.fail("unable to add call: " + e);
+ }
+ }
+
+ private SimulatorVoiceCall() {}
+}
diff --git a/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
new file mode 100644
index 000000000..ffb9191dc
--- /dev/null
+++ b/java/com/android/dialer/simulator/impl/SimulatorVoicemail.java
@@ -0,0 +1,154 @@
+/*
+ * 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.simulator.impl;
+
+import android.content.ComponentName;
+import android.content.ContentValues;
+import android.content.Context;
+import android.provider.VoicemailContract.Status;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import com.android.dialer.common.Assert;
+
+import java.util.concurrent.TimeUnit;
+
+/** Populates the device database with voicemail entries. */
+final class SimulatorVoicemail {
+ private static final String ACCOUNT_ID = "ACCOUNT_ID";
+
+ private static final Voicemail.Builder[] SIMPLE_VOICEMAILS = {
+ // Long transcription with an embedded phone number.
+ Voicemail.builder()
+ .setPhoneNumber("+1-302-6365454")
+ .setTranscription(
+ "Hi, this is a very long voicemail. Please call me back at 650 253 0000. "
+ + "I hope you listen to all of it. This is very important. "
+ + "Hi, this is a very long voicemail. "
+ + "I hope you listen to all of it. It's very important.")
+ .setDurationSeconds(10)
+ .setIsRead(false),
+ // RTL transcription.
+ Voicemail.builder()
+ .setPhoneNumber("+1-302-6365454")
+ .setTranscription("هزاران دوست کم اند و یک دشمن زیاد")
+ .setDurationSeconds(60)
+ .setIsRead(true),
+ // Empty number.
+ Voicemail.builder()
+ .setPhoneNumber("")
+ .setTranscription("")
+ .setDurationSeconds(60)
+ .setIsRead(true),
+ // No duration.
+ Voicemail.builder()
+ .setPhoneNumber("+1-302-6365454")
+ .setTranscription("")
+ .setDurationSeconds(0)
+ .setIsRead(true),
+ // Short number.
+ Voicemail.builder()
+ .setPhoneNumber("711")
+ .setTranscription("This is a short voicemail.")
+ .setDurationSeconds(12)
+ .setIsRead(true),
+ };
+
+ @WorkerThread
+ public static void populateVoicemail(@NonNull Context context) {
+ Assert.isWorkerThread();
+ enableVoicemail(context);
+
+ // Do this 4 times to make the voicemail database 4 times bigger.
+ long timeMillis = System.currentTimeMillis();
+ for (int i = 0; i < 4; i++) {
+ for (Voicemail.Builder builder : SIMPLE_VOICEMAILS) {
+ Voicemail voicemail = builder.setTimeMillis(timeMillis).build();
+ context
+ .getContentResolver()
+ .insert(
+ Voicemails.buildSourceUri(context.getPackageName()),
+ voicemail.getAsContentValues(context));
+ timeMillis -= TimeUnit.HOURS.toMillis(2);
+ }
+ }
+ }
+
+ private static void enableVoicemail(@NonNull Context context) {
+ PhoneAccountHandle handle =
+ new PhoneAccountHandle(new ComponentName(context, SimulatorVoicemail.class), ACCOUNT_ID);
+
+ ContentValues values = new ContentValues();
+ values.put(Status.SOURCE_PACKAGE, handle.getComponentName().getPackageName());
+ values.put(Status.SOURCE_TYPE, TelephonyManager.VVM_TYPE_OMTP);
+ values.put(Status.PHONE_ACCOUNT_COMPONENT_NAME, handle.getComponentName().flattenToString());
+ values.put(Status.PHONE_ACCOUNT_ID, handle.getId());
+ values.put(Status.CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+ values.put(Status.DATA_CHANNEL_STATE, Status.DATA_CHANNEL_STATE_OK);
+ values.put(Status.NOTIFICATION_CHANNEL_STATE, Status.NOTIFICATION_CHANNEL_STATE_OK);
+ context.getContentResolver().insert(Status.buildSourceUri(context.getPackageName()), values);
+ }
+
+
+ abstract static class Voicemail {
+ @NonNull
+ abstract String getPhoneNumber();
+
+ @NonNull
+ abstract String getTranscription();
+
+ abstract long getDurationSeconds();
+
+ abstract long getTimeMillis();
+
+ abstract boolean getIsRead();
+
+ static Builder builder() {
+ return new AutoValue_SimulatorVoicemail_Voicemail.Builder();
+ }
+
+ ContentValues getAsContentValues(Context context) {
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.DATE, getTimeMillis());
+ values.put(Voicemails.NUMBER, getPhoneNumber());
+ values.put(Voicemails.DURATION, getDurationSeconds());
+ values.put(Voicemails.SOURCE_PACKAGE, context.getPackageName());
+ values.put(Voicemails.IS_READ, getIsRead() ? 1 : 0);
+ values.put(Voicemails.TRANSCRIPTION, getTranscription());
+ return values;
+ }
+
+
+ abstract static class Builder {
+ abstract Builder setPhoneNumber(@NonNull String phoneNumber);
+
+ abstract Builder setTranscription(@NonNull String transcription);
+
+ abstract Builder setDurationSeconds(long durationSeconds);
+
+ abstract Builder setTimeMillis(long timeMillis);
+
+ abstract Builder setIsRead(boolean isRead);
+
+ abstract Voicemail build();
+ }
+ }
+
+ private SimulatorVoicemail() {}
+}
diff --git a/java/com/android/dialer/smartdial/LatinSmartDialMap.java b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
new file mode 100644
index 000000000..c512c5d4a
--- /dev/null
+++ b/java/com/android/dialer/smartdial/LatinSmartDialMap.java
@@ -0,0 +1,784 @@
+/*
+ * 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.smartdial;
+
+public class LatinSmartDialMap implements SmartDialMap {
+
+ private static final char[] LATIN_LETTERS_TO_DIGITS = {
+ '2',
+ '2',
+ '2', // A,B,C -> 2
+ '3',
+ '3',
+ '3', // D,E,F -> 3
+ '4',
+ '4',
+ '4', // G,H,I -> 4
+ '5',
+ '5',
+ '5', // J,K,L -> 5
+ '6',
+ '6',
+ '6', // M,N,O -> 6
+ '7',
+ '7',
+ '7',
+ '7', // P,Q,R,S -> 7
+ '8',
+ '8',
+ '8', // T,U,V -> 8
+ '9',
+ '9',
+ '9',
+ '9' // W,X,Y,Z -> 9
+ };
+
+ @Override
+ public boolean isValidDialpadAlphabeticChar(char ch) {
+ return (ch >= 'a' && ch <= 'z');
+ }
+
+ @Override
+ public boolean isValidDialpadNumericChar(char ch) {
+ return (ch >= '0' && ch <= '9');
+ }
+
+ @Override
+ public boolean isValidDialpadCharacter(char ch) {
+ return (isValidDialpadAlphabeticChar(ch) || isValidDialpadNumericChar(ch));
+ }
+
+ /*
+ * The switch statement in this function was generated using the python code:
+ * from unidecode import unidecode
+ * for i in range(192, 564):
+ * char = unichr(i)
+ * decoded = unidecode(char)
+ * # Unicode characters that decompose into multiple characters i.e.
+ * # into ss are not supported for now
+ * if (len(decoded) == 1 and decoded.isalpha()):
+ * print "case '" + char + "': return '" + unidecode(char) + "';"
+ *
+ * This gives us a way to map characters containing accents/diacritics to their
+ * alphabetic equivalents. The unidecode library can be found at:
+ * http://pypi.python.org/pypi/Unidecode/0.04.1
+ *
+ * Also remaps all upper case latin characters to their lower case equivalents.
+ */
+ @Override
+ public char normalizeCharacter(char ch) {
+ switch (ch) {
+ case 'À':
+ return 'a';
+ case 'Á':
+ return 'a';
+ case 'Â':
+ return 'a';
+ case 'Ã':
+ return 'a';
+ case 'Ä':
+ return 'a';
+ case 'Å':
+ return 'a';
+ case 'Ç':
+ return 'c';
+ case 'È':
+ return 'e';
+ case 'É':
+ return 'e';
+ case 'Ê':
+ return 'e';
+ case 'Ë':
+ return 'e';
+ case 'Ì':
+ return 'i';
+ case 'Í':
+ return 'i';
+ case 'Î':
+ return 'i';
+ case 'Ï':
+ return 'i';
+ case 'Ð':
+ return 'd';
+ case 'Ñ':
+ return 'n';
+ case 'Ò':
+ return 'o';
+ case 'Ó':
+ return 'o';
+ case 'Ô':
+ return 'o';
+ case 'Õ':
+ return 'o';
+ case 'Ö':
+ return 'o';
+ case '×':
+ return 'x';
+ case 'Ø':
+ return 'o';
+ case 'Ù':
+ return 'u';
+ case 'Ú':
+ return 'u';
+ case 'Û':
+ return 'u';
+ case 'Ü':
+ return 'u';
+ case 'Ý':
+ return 'u';
+ case 'à':
+ return 'a';
+ case 'á':
+ return 'a';
+ case 'â':
+ return 'a';
+ case 'ã':
+ return 'a';
+ case 'ä':
+ return 'a';
+ case 'å':
+ return 'a';
+ case 'ç':
+ return 'c';
+ case 'è':
+ return 'e';
+ case 'é':
+ return 'e';
+ case 'ê':
+ return 'e';
+ case 'ë':
+ return 'e';
+ case 'ì':
+ return 'i';
+ case 'í':
+ return 'i';
+ case 'î':
+ return 'i';
+ case 'ï':
+ return 'i';
+ case 'ð':
+ return 'd';
+ case 'ñ':
+ return 'n';
+ case 'ò':
+ return 'o';
+ case 'ó':
+ return 'o';
+ case 'ô':
+ return 'o';
+ case 'õ':
+ return 'o';
+ case 'ö':
+ return 'o';
+ case 'ø':
+ return 'o';
+ case 'ù':
+ return 'u';
+ case 'ú':
+ return 'u';
+ case 'û':
+ return 'u';
+ case 'ü':
+ return 'u';
+ case 'ý':
+ return 'y';
+ case 'ÿ':
+ return 'y';
+ case 'Ā':
+ return 'a';
+ case 'ā':
+ return 'a';
+ case 'Ă':
+ return 'a';
+ case 'ă':
+ return 'a';
+ case 'Ą':
+ return 'a';
+ case 'ą':
+ return 'a';
+ case 'Ć':
+ return 'c';
+ case 'ć':
+ return 'c';
+ case 'Ĉ':
+ return 'c';
+ case 'ĉ':
+ return 'c';
+ case 'Ċ':
+ return 'c';
+ case 'ċ':
+ return 'c';
+ case 'Č':
+ return 'c';
+ case 'č':
+ return 'c';
+ case 'Ď':
+ return 'd';
+ case 'ď':
+ return 'd';
+ case 'Đ':
+ return 'd';
+ case 'đ':
+ return 'd';
+ case 'Ē':
+ return 'e';
+ case 'ē':
+ return 'e';
+ case 'Ĕ':
+ return 'e';
+ case 'ĕ':
+ return 'e';
+ case 'Ė':
+ return 'e';
+ case 'ė':
+ return 'e';
+ case 'Ę':
+ return 'e';
+ case 'ę':
+ return 'e';
+ case 'Ě':
+ return 'e';
+ case 'ě':
+ return 'e';
+ case 'Ĝ':
+ return 'g';
+ case 'ĝ':
+ return 'g';
+ case 'Ğ':
+ return 'g';
+ case 'ğ':
+ return 'g';
+ case 'Ġ':
+ return 'g';
+ case 'ġ':
+ return 'g';
+ case 'Ģ':
+ return 'g';
+ case 'ģ':
+ return 'g';
+ case 'Ĥ':
+ return 'h';
+ case 'ĥ':
+ return 'h';
+ case 'Ħ':
+ return 'h';
+ case 'ħ':
+ return 'h';
+ case 'Ĩ':
+ return 'i';
+ case 'ĩ':
+ return 'i';
+ case 'Ī':
+ return 'i';
+ case 'ī':
+ return 'i';
+ case 'Ĭ':
+ return 'i';
+ case 'ĭ':
+ return 'i';
+ case 'Į':
+ return 'i';
+ case 'į':
+ return 'i';
+ case 'İ':
+ return 'i';
+ case 'ı':
+ return 'i';
+ case 'Ĵ':
+ return 'j';
+ case 'ĵ':
+ return 'j';
+ case 'Ķ':
+ return 'k';
+ case 'ķ':
+ return 'k';
+ case 'ĸ':
+ return 'k';
+ case 'Ĺ':
+ return 'l';
+ case 'ĺ':
+ return 'l';
+ case 'Ļ':
+ return 'l';
+ case 'ļ':
+ return 'l';
+ case 'Ľ':
+ return 'l';
+ case 'ľ':
+ return 'l';
+ case 'Ŀ':
+ return 'l';
+ case 'ŀ':
+ return 'l';
+ case 'Ł':
+ return 'l';
+ case 'ł':
+ return 'l';
+ case 'Ń':
+ return 'n';
+ case 'ń':
+ return 'n';
+ case 'Ņ':
+ return 'n';
+ case 'ņ':
+ return 'n';
+ case 'Ň':
+ return 'n';
+ case 'ň':
+ return 'n';
+ case 'Ō':
+ return 'o';
+ case 'ō':
+ return 'o';
+ case 'Ŏ':
+ return 'o';
+ case 'ŏ':
+ return 'o';
+ case 'Ő':
+ return 'o';
+ case 'ő':
+ return 'o';
+ case 'Ŕ':
+ return 'r';
+ case 'ŕ':
+ return 'r';
+ case 'Ŗ':
+ return 'r';
+ case 'ŗ':
+ return 'r';
+ case 'Ř':
+ return 'r';
+ case 'ř':
+ return 'r';
+ case 'Ś':
+ return 's';
+ case 'ś':
+ return 's';
+ case 'Ŝ':
+ return 's';
+ case 'ŝ':
+ return 's';
+ case 'Ş':
+ return 's';
+ case 'ş':
+ return 's';
+ case 'Š':
+ return 's';
+ case 'š':
+ return 's';
+ case 'Ţ':
+ return 't';
+ case 'ţ':
+ return 't';
+ case 'Ť':
+ return 't';
+ case 'ť':
+ return 't';
+ case 'Ŧ':
+ return 't';
+ case 'ŧ':
+ return 't';
+ case 'Ũ':
+ return 'u';
+ case 'ũ':
+ return 'u';
+ case 'Ū':
+ return 'u';
+ case 'ū':
+ return 'u';
+ case 'Ŭ':
+ return 'u';
+ case 'ŭ':
+ return 'u';
+ case 'Ů':
+ return 'u';
+ case 'ů':
+ return 'u';
+ case 'Ű':
+ return 'u';
+ case 'ű':
+ return 'u';
+ case 'Ų':
+ return 'u';
+ case 'ų':
+ return 'u';
+ case 'Ŵ':
+ return 'w';
+ case 'ŵ':
+ return 'w';
+ case 'Ŷ':
+ return 'y';
+ case 'ŷ':
+ return 'y';
+ case 'Ÿ':
+ return 'y';
+ case 'Ź':
+ return 'z';
+ case 'ź':
+ return 'z';
+ case 'Ż':
+ return 'z';
+ case 'ż':
+ return 'z';
+ case 'Ž':
+ return 'z';
+ case 'ž':
+ return 'z';
+ case 'ſ':
+ return 's';
+ case 'ƀ':
+ return 'b';
+ case 'Ɓ':
+ return 'b';
+ case 'Ƃ':
+ return 'b';
+ case 'ƃ':
+ return 'b';
+ case 'Ɔ':
+ return 'o';
+ case 'Ƈ':
+ return 'c';
+ case 'ƈ':
+ return 'c';
+ case 'Ɖ':
+ return 'd';
+ case 'Ɗ':
+ return 'd';
+ case 'Ƌ':
+ return 'd';
+ case 'ƌ':
+ return 'd';
+ case 'ƍ':
+ return 'd';
+ case 'Ɛ':
+ return 'e';
+ case 'Ƒ':
+ return 'f';
+ case 'ƒ':
+ return 'f';
+ case 'Ɠ':
+ return 'g';
+ case 'Ɣ':
+ return 'g';
+ case 'Ɩ':
+ return 'i';
+ case 'Ɨ':
+ return 'i';
+ case 'Ƙ':
+ return 'k';
+ case 'ƙ':
+ return 'k';
+ case 'ƚ':
+ return 'l';
+ case 'ƛ':
+ return 'l';
+ case 'Ɯ':
+ return 'w';
+ case 'Ɲ':
+ return 'n';
+ case 'ƞ':
+ return 'n';
+ case 'Ɵ':
+ return 'o';
+ case 'Ơ':
+ return 'o';
+ case 'ơ':
+ return 'o';
+ case 'Ƥ':
+ return 'p';
+ case 'ƥ':
+ return 'p';
+ case 'ƫ':
+ return 't';
+ case 'Ƭ':
+ return 't';
+ case 'ƭ':
+ return 't';
+ case 'Ʈ':
+ return 't';
+ case 'Ư':
+ return 'u';
+ case 'ư':
+ return 'u';
+ case 'Ʊ':
+ return 'y';
+ case 'Ʋ':
+ return 'v';
+ case 'Ƴ':
+ return 'y';
+ case 'ƴ':
+ return 'y';
+ case 'Ƶ':
+ return 'z';
+ case 'ƶ':
+ return 'z';
+ case 'ƿ':
+ return 'w';
+ case 'Ǎ':
+ return 'a';
+ case 'ǎ':
+ return 'a';
+ case 'Ǐ':
+ return 'i';
+ case 'ǐ':
+ return 'i';
+ case 'Ǒ':
+ return 'o';
+ case 'ǒ':
+ return 'o';
+ case 'Ǔ':
+ return 'u';
+ case 'ǔ':
+ return 'u';
+ case 'Ǖ':
+ return 'u';
+ case 'ǖ':
+ return 'u';
+ case 'Ǘ':
+ return 'u';
+ case 'ǘ':
+ return 'u';
+ case 'Ǚ':
+ return 'u';
+ case 'ǚ':
+ return 'u';
+ case 'Ǜ':
+ return 'u';
+ case 'ǜ':
+ return 'u';
+ case 'Ǟ':
+ return 'a';
+ case 'ǟ':
+ return 'a';
+ case 'Ǡ':
+ return 'a';
+ case 'ǡ':
+ return 'a';
+ case 'Ǥ':
+ return 'g';
+ case 'ǥ':
+ return 'g';
+ case 'Ǧ':
+ return 'g';
+ case 'ǧ':
+ return 'g';
+ case 'Ǩ':
+ return 'k';
+ case 'ǩ':
+ return 'k';
+ case 'Ǫ':
+ return 'o';
+ case 'ǫ':
+ return 'o';
+ case 'Ǭ':
+ return 'o';
+ case 'ǭ':
+ return 'o';
+ case 'ǰ':
+ return 'j';
+ case 'Dz':
+ return 'd';
+ case 'Ǵ':
+ return 'g';
+ case 'ǵ':
+ return 'g';
+ case 'Ƿ':
+ return 'w';
+ case 'Ǹ':
+ return 'n';
+ case 'ǹ':
+ return 'n';
+ case 'Ǻ':
+ return 'a';
+ case 'ǻ':
+ return 'a';
+ case 'Ǿ':
+ return 'o';
+ case 'ǿ':
+ return 'o';
+ case 'Ȁ':
+ return 'a';
+ case 'ȁ':
+ return 'a';
+ case 'Ȃ':
+ return 'a';
+ case 'ȃ':
+ return 'a';
+ case 'Ȅ':
+ return 'e';
+ case 'ȅ':
+ return 'e';
+ case 'Ȇ':
+ return 'e';
+ case 'ȇ':
+ return 'e';
+ case 'Ȉ':
+ return 'i';
+ case 'ȉ':
+ return 'i';
+ case 'Ȋ':
+ return 'i';
+ case 'ȋ':
+ return 'i';
+ case 'Ȍ':
+ return 'o';
+ case 'ȍ':
+ return 'o';
+ case 'Ȏ':
+ return 'o';
+ case 'ȏ':
+ return 'o';
+ case 'Ȑ':
+ return 'r';
+ case 'ȑ':
+ return 'r';
+ case 'Ȓ':
+ return 'r';
+ case 'ȓ':
+ return 'r';
+ case 'Ȕ':
+ return 'u';
+ case 'ȕ':
+ return 'u';
+ case 'Ȗ':
+ return 'u';
+ case 'ȗ':
+ return 'u';
+ case 'Ș':
+ return 's';
+ case 'ș':
+ return 's';
+ case 'Ț':
+ return 't';
+ case 'ț':
+ return 't';
+ case 'Ȝ':
+ return 'y';
+ case 'ȝ':
+ return 'y';
+ case 'Ȟ':
+ return 'h';
+ case 'ȟ':
+ return 'h';
+ case 'Ȥ':
+ return 'z';
+ case 'ȥ':
+ return 'z';
+ case 'Ȧ':
+ return 'a';
+ case 'ȧ':
+ return 'a';
+ case 'Ȩ':
+ return 'e';
+ case 'ȩ':
+ return 'e';
+ case 'Ȫ':
+ return 'o';
+ case 'ȫ':
+ return 'o';
+ case 'Ȭ':
+ return 'o';
+ case 'ȭ':
+ return 'o';
+ case 'Ȯ':
+ return 'o';
+ case 'ȯ':
+ return 'o';
+ case 'Ȱ':
+ return 'o';
+ case 'ȱ':
+ return 'o';
+ case 'Ȳ':
+ return 'y';
+ case 'ȳ':
+ return 'y';
+ case 'A':
+ return 'a';
+ case 'B':
+ return 'b';
+ case 'C':
+ return 'c';
+ case 'D':
+ return 'd';
+ case 'E':
+ return 'e';
+ case 'F':
+ return 'f';
+ case 'G':
+ return 'g';
+ case 'H':
+ return 'h';
+ case 'I':
+ return 'i';
+ case 'J':
+ return 'j';
+ case 'K':
+ return 'k';
+ case 'L':
+ return 'l';
+ case 'M':
+ return 'm';
+ case 'N':
+ return 'n';
+ case 'O':
+ return 'o';
+ case 'P':
+ return 'p';
+ case 'Q':
+ return 'q';
+ case 'R':
+ return 'r';
+ case 'S':
+ return 's';
+ case 'T':
+ return 't';
+ case 'U':
+ return 'u';
+ case 'V':
+ return 'v';
+ case 'W':
+ return 'w';
+ case 'X':
+ return 'x';
+ case 'Y':
+ return 'y';
+ case 'Z':
+ return 'z';
+ default:
+ return ch;
+ }
+ }
+
+ @Override
+ public byte getDialpadIndex(char ch) {
+ if (ch >= '0' && ch <= '9') {
+ return (byte) (ch - '0');
+ } else if (ch >= 'a' && ch <= 'z') {
+ return (byte) (LATIN_LETTERS_TO_DIGITS[ch - 'a'] - '0');
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public char getDialpadNumericCharacter(char ch) {
+ if (ch >= 'a' && ch <= 'z') {
+ return LATIN_LETTERS_TO_DIGITS[ch - 'a'];
+ }
+ return ch;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMap.java b/java/com/android/dialer/smartdial/SmartDialMap.java
new file mode 100644
index 000000000..9638929a6
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialMap.java
@@ -0,0 +1,60 @@
+/*
+ * 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.smartdial;
+
+/**
+ * Note: These methods currently take characters as arguments. For future planned language support,
+ * they will need to be changed to use codepoints instead of characters.
+ *
+ * <p>http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#codePointAt(int)
+ *
+ * <p>If/when this change is made, LatinSmartDialMap(which operates on chars) will continue to work
+ * by simply casting from a codepoint to a character.
+ */
+public interface SmartDialMap {
+
+ /*
+ * Returns true if the provided character can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadCharacter(char ch);
+
+ /*
+ * Returns true if the provided character is a letter, and can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadAlphabeticChar(char ch);
+
+ /*
+ * Returns true if the provided character is a digit, and can be mapped to a key on the dialpad
+ */
+ boolean isValidDialpadNumericChar(char ch);
+
+ /*
+ * Get the index of the key on the dialpad which the character corresponds to
+ */
+ byte getDialpadIndex(char ch);
+
+ /*
+ * Get the actual numeric character on the dialpad which the character corresponds to
+ */
+ char getDialpadNumericCharacter(char ch);
+
+ /*
+ * Converts uppercase characters to lower case ones, and on a best effort basis, strips accents
+ * from accented characters.
+ */
+ char normalizeCharacter(char ch);
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialMatchPosition.java b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java
new file mode 100644
index 000000000..8056ad723
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialMatchPosition.java
@@ -0,0 +1,70 @@
+/*
+ * 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.smartdial;
+
+import android.util.Log;
+import java.util.ArrayList;
+
+/**
+ * Stores information about a range of characters matched in a display name The integers start and
+ * end indicate that the range start to end (exclusive) correspond to some characters in the query.
+ * Used to highlight certain parts of the contact's display name to indicate that those ranges
+ * matched the user's query.
+ */
+public class SmartDialMatchPosition {
+
+ private static final String TAG = SmartDialMatchPosition.class.getSimpleName();
+
+ public int start;
+ public int end;
+
+ public SmartDialMatchPosition(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Used by {@link SmartDialNameMatcher} to advance the positions of a match position found in a
+ * sub query.
+ *
+ * @param inList ArrayList of SmartDialMatchPositions to modify.
+ * @param toAdvance Offset to modify by.
+ */
+ public static void advanceMatchPositions(
+ ArrayList<SmartDialMatchPosition> inList, int toAdvance) {
+ for (int i = 0; i < inList.size(); i++) {
+ inList.get(i).advance(toAdvance);
+ }
+ }
+
+ /**
+ * Used mainly for debug purposes. Displays contents of an ArrayList of SmartDialMatchPositions.
+ *
+ * @param list ArrayList of SmartDialMatchPositions to print out in a human readable fashion.
+ */
+ public static void print(ArrayList<SmartDialMatchPosition> list) {
+ for (int i = 0; i < list.size(); i++) {
+ SmartDialMatchPosition m = list.get(i);
+ Log.d(TAG, "[" + m.start + "," + m.end + "]");
+ }
+ }
+
+ private void advance(int toAdvance) {
+ this.start += toAdvance;
+ this.end += toAdvance;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialNameMatcher.java b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
new file mode 100644
index 000000000..a1580a0ce
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialNameMatcher.java
@@ -0,0 +1,434 @@
+/*
+ * 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.smartdial;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.dialer.smartdial.SmartDialPrefix.PhoneNumberTokens;
+import java.util.ArrayList;
+
+/**
+ * {@link #SmartDialNameMatcher} contains utility functions to remove accents from accented
+ * characters and normalize a phone number. It also contains the matching logic that determines if a
+ * contact's display name matches a numeric query. The boolean variable {@link #ALLOW_INITIAL_MATCH}
+ * controls the behavior of the matching logic and determines whether we allow matches like 57 -
+ * (J)ohn (S)mith.
+ */
+public class SmartDialNameMatcher {
+
+ public static final SmartDialMap LATIN_SMART_DIAL_MAP = new LatinSmartDialMap();
+ // Whether or not we allow matches like 57 - (J)ohn (S)mith
+ private static final boolean ALLOW_INITIAL_MATCH = true;
+
+ // The maximum length of the initial we will match - typically set to 1 to minimize false
+ // positives
+ private static final int INITIAL_LENGTH_LIMIT = 1;
+
+ private final ArrayList<SmartDialMatchPosition> mMatchPositions = new ArrayList<>();
+ private final SmartDialMap mMap;
+ private String mQuery;
+ private String mNameMatchMask = "";
+ private String mPhoneNumberMatchMask = "";
+
+ // Controls whether to treat an empty query as a match (with anything).
+ private boolean mShouldMatchEmptyQuery = false;
+
+ @VisibleForTesting
+ public SmartDialNameMatcher(String query) {
+ this(query, LATIN_SMART_DIAL_MAP);
+ }
+
+ public SmartDialNameMatcher(String query, SmartDialMap map) {
+ mQuery = query;
+ mMap = map;
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @return Phone number consisting of digits from 0-9
+ */
+ public static String normalizeNumber(String number, SmartDialMap map) {
+ return normalizeNumber(number, 0, map);
+ }
+
+ /**
+ * Strips a phone number of unnecessary characters (spaces, dashes, etc.)
+ *
+ * @param number Phone number we want to normalize
+ * @param offset Offset to start from
+ * @return Phone number consisting of digits from 0-9
+ */
+ public static String normalizeNumber(String number, int offset, SmartDialMap map) {
+ final StringBuilder s = new StringBuilder();
+ for (int i = offset; i < number.length(); i++) {
+ char ch = number.charAt(i);
+ if (map.isValidDialpadNumericChar(ch)) {
+ s.append(ch);
+ }
+ }
+ return s.toString();
+ }
+
+ /**
+ * Constructs empty highlight mask. Bit 0 at a position means there is no match, Bit 1 means there
+ * is a match and should be highlighted in the TextView.
+ *
+ * @param builder StringBuilder object
+ * @param length Length of the desired mask.
+ */
+ private void constructEmptyMask(StringBuilder builder, int length) {
+ for (int i = 0; i < length; ++i) {
+ builder.append("0");
+ }
+ }
+
+ /**
+ * Replaces the 0-bit at a position with 1-bit, indicating that there is a match.
+ *
+ * @param builder StringBuilder object.
+ * @param matchPos Match Positions to mask as 1.
+ */
+ private void replaceBitInMask(StringBuilder builder, SmartDialMatchPosition matchPos) {
+ for (int i = matchPos.start; i < matchPos.end; ++i) {
+ builder.replace(i, i + 1, "1");
+ }
+ }
+
+ /**
+ * Matches a phone number against a query. Let the test application overwrite the NANP setting.
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @param useNanp - Overwriting nanp setting boolean, used for testing.
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ @VisibleForTesting
+ @Nullable
+ public SmartDialMatchPosition matchesNumber(String phoneNumber, String query, boolean useNanp) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(0, 0) : null;
+ }
+ StringBuilder builder = new StringBuilder();
+ constructEmptyMask(builder, phoneNumber.length());
+ mPhoneNumberMatchMask = builder.toString();
+
+ // Try matching the number as is
+ SmartDialMatchPosition matchPos = matchesNumberWithOffset(phoneNumber, query, 0);
+ if (matchPos == null) {
+ final PhoneNumberTokens phoneNumberTokens = SmartDialPrefix.parsePhoneNumber(phoneNumber);
+
+ if (phoneNumberTokens == null) {
+ return matchPos;
+ }
+ if (phoneNumberTokens.countryCodeOffset != 0) {
+ matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.countryCodeOffset);
+ }
+ if (matchPos == null && phoneNumberTokens.nanpCodeOffset != 0 && useNanp) {
+ matchPos = matchesNumberWithOffset(phoneNumber, query, phoneNumberTokens.nanpCodeOffset);
+ }
+ }
+ if (matchPos != null) {
+ replaceBitInMask(builder, matchPos);
+ mPhoneNumberMatchMask = builder.toString();
+ }
+ return matchPos;
+ }
+
+ /**
+ * Matches a phone number against the saved query, taking care of formatting characters and also
+ * taking into account country code prefixes and special NANP number treatment.
+ *
+ * @param phoneNumber - Raw phone number
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ public SmartDialMatchPosition matchesNumber(String phoneNumber) {
+ return matchesNumber(phoneNumber, mQuery, true);
+ }
+
+ /**
+ * Matches a phone number against a query, taking care of formatting characters and also taking
+ * into account country code prefixes and special NANP number treatment.
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ public SmartDialMatchPosition matchesNumber(String phoneNumber, String query) {
+ return matchesNumber(phoneNumber, query, true);
+ }
+
+ /**
+ * Matches a phone number against a query, taking care of formatting characters
+ *
+ * @param phoneNumber - Raw phone number
+ * @param query - Normalized query (only contains numbers from 0-9)
+ * @param offset - The position in the number to start the match against (used to ignore leading
+ * prefixes/country codes)
+ * @return {@literal null} if the number and the query don't match, a valid SmartDialMatchPosition
+ * with the matching positions otherwise
+ */
+ private SmartDialMatchPosition matchesNumberWithOffset(
+ String phoneNumber, String query, int offset) {
+ if (TextUtils.isEmpty(phoneNumber) || TextUtils.isEmpty(query)) {
+ return mShouldMatchEmptyQuery ? new SmartDialMatchPosition(offset, offset) : null;
+ }
+ int queryAt = 0;
+ int numberAt = offset;
+ for (int i = offset; i < phoneNumber.length(); i++) {
+ if (queryAt == query.length()) {
+ break;
+ }
+ char ch = phoneNumber.charAt(i);
+ if (mMap.isValidDialpadNumericChar(ch)) {
+ if (ch != query.charAt(queryAt)) {
+ return null;
+ }
+ queryAt++;
+ } else {
+ if (queryAt == 0) {
+ // Found a separator before any part of the query was matched, so advance the
+ // offset to avoid prematurely highlighting separators before the rest of the
+ // query.
+ // E.g. don't highlight the first '-' if we're matching 1-510-111-1111 with
+ // '510'.
+ // However, if the current offset is 0, just include the beginning separators
+ // anyway, otherwise the highlighting ends up looking weird.
+ // E.g. if we're matching (510)-111-1111 with '510', we should include the
+ // first '('.
+ if (offset != 0) {
+ offset++;
+ }
+ }
+ }
+ numberAt++;
+ }
+ return new SmartDialMatchPosition(0 + offset, numberAt);
+ }
+
+ /**
+ * This function iterates through each token in the display name, trying to match the query to the
+ * numeric equivalent of the token.
+ *
+ * <p>A token is defined as a range in the display name delimited by characters that have no latin
+ * alphabet equivalents (e.g. spaces - ' ', periods - ',', underscores - '_' or chinese characters
+ * - '王'). Transliteration from non-latin characters to latin character will be done on a best
+ * effort basis - e.g. 'Ü' - 'u'.
+ *
+ * <p>For example, the display name "Phillips Thomas Jr" contains three tokens: "phillips",
+ * "thomas", and "jr".
+ *
+ * <p>A match must begin at the start of a token. For example, typing 846(Tho) would match
+ * "Phillips Thomas", but 466(hom) would not.
+ *
+ * <p>Also, a match can extend across tokens. For example, typing 37337(FredS) would match (Fred
+ * S)mith.
+ *
+ * @param displayName The normalized(no accented characters) display name we intend to match
+ * against.
+ * @param query The string of digits that we want to match the display name to.
+ * @param matchList An array list of {@link SmartDialMatchPosition}s that we add matched positions
+ * to.
+ * @return Returns true if a combination of the tokens in displayName match the query string
+ * contained in query. If the function returns true, matchList will contain an ArrayList of
+ * match positions (multiple matches correspond to initial matches).
+ */
+ @VisibleForTesting
+ boolean matchesCombination(
+ String displayName, String query, ArrayList<SmartDialMatchPosition> matchList) {
+ StringBuilder builder = new StringBuilder();
+ constructEmptyMask(builder, displayName.length());
+ mNameMatchMask = builder.toString();
+ final int nameLength = displayName.length();
+ final int queryLength = query.length();
+
+ if (nameLength < queryLength) {
+ return false;
+ }
+
+ if (queryLength == 0) {
+ return false;
+ }
+
+ // The current character index in displayName
+ // E.g. 3 corresponds to 'd' in "Fred Smith"
+ int nameStart = 0;
+
+ // The current character in the query we are trying to match the displayName against
+ int queryStart = 0;
+
+ // The start position of the current token we are inspecting
+ int tokenStart = 0;
+
+ // The number of non-alphabetic characters we've encountered so far in the current match.
+ // E.g. if we've currently matched 3733764849 to (Fred Smith W)illiam, then the
+ // seperatorCount should be 2. This allows us to correctly calculate offsets for the match
+ // positions
+ int seperatorCount = 0;
+
+ ArrayList<SmartDialMatchPosition> partial = new ArrayList<SmartDialMatchPosition>();
+ // Keep going until we reach the end of displayName
+ while (nameStart < nameLength && queryStart < queryLength) {
+ char ch = displayName.charAt(nameStart);
+ // Strip diacritics from accented characters if any
+ ch = mMap.normalizeCharacter(ch);
+ if (mMap.isValidDialpadCharacter(ch)) {
+ if (mMap.isValidDialpadAlphabeticChar(ch)) {
+ ch = mMap.getDialpadNumericCharacter(ch);
+ }
+ if (ch != query.charAt(queryStart)) {
+ // Failed to match the current character in the query.
+
+ // Case 1: Failed to match the first character in the query. Skip to the next
+ // token since there is no chance of this token matching the query.
+
+ // Case 2: Previous characters in the query matched, but the current character
+ // failed to match. This happened in the middle of a token. Skip to the next
+ // token since there is no chance of this token matching the query.
+
+ // Case 3: Previous characters in the query matched, but the current character
+ // failed to match. This happened right at the start of the current token. In
+ // this case, we should restart the query and try again with the current token.
+ // Otherwise, we would fail to match a query like "964"(yog) against a name
+ // Yo-Yoghurt because the query match would fail on the 3rd character, and
+ // then skip to the end of the "Yoghurt" token.
+
+ if (queryStart == 0
+ || mMap.isValidDialpadCharacter(
+ mMap.normalizeCharacter(displayName.charAt(nameStart - 1)))) {
+ // skip to the next token, in the case of 1 or 2.
+ while (nameStart < nameLength
+ && mMap.isValidDialpadCharacter(
+ mMap.normalizeCharacter(displayName.charAt(nameStart)))) {
+ nameStart++;
+ }
+ nameStart++;
+ }
+
+ // Restart the query and set the correct token position
+ queryStart = 0;
+ seperatorCount = 0;
+ tokenStart = nameStart;
+ } else {
+ if (queryStart == queryLength - 1) {
+
+ // As much as possible, we prioritize a full token match over a sub token
+ // one so if we find a full token match, we can return right away
+ matchList.add(
+ new SmartDialMatchPosition(tokenStart, queryLength + tokenStart + seperatorCount));
+ for (SmartDialMatchPosition match : matchList) {
+ replaceBitInMask(builder, match);
+ }
+ mNameMatchMask = builder.toString();
+ return true;
+ } else if (ALLOW_INITIAL_MATCH && queryStart < INITIAL_LENGTH_LIMIT) {
+ // we matched the first character.
+ // branch off and see if we can find another match with the remaining
+ // characters in the query string and the remaining tokens
+ // find the next separator in the query string
+ int j;
+ for (j = nameStart; j < nameLength; j++) {
+ if (!mMap.isValidDialpadCharacter(mMap.normalizeCharacter(displayName.charAt(j)))) {
+ break;
+ }
+ }
+ // this means there is at least one character left after the separator
+ if (j < nameLength - 1) {
+ final String remainder = displayName.substring(j + 1);
+ final ArrayList<SmartDialMatchPosition> partialTemp = new ArrayList<>();
+ if (matchesCombination(remainder, query.substring(queryStart + 1), partialTemp)) {
+
+ // store the list of possible match positions
+ SmartDialMatchPosition.advanceMatchPositions(partialTemp, j + 1);
+ partialTemp.add(0, new SmartDialMatchPosition(nameStart, nameStart + 1));
+ // we found a partial token match, store the data in a
+ // temp buffer and return it if we end up not finding a full
+ // token match
+ partial = partialTemp;
+ }
+ }
+ }
+ nameStart++;
+ queryStart++;
+ // we matched the current character in the name against one in the query,
+ // continue and see if the rest of the characters match
+ }
+ } else {
+ // found a separator, we skip this character and continue to the next one
+ nameStart++;
+ if (queryStart == 0) {
+ // This means we found a separator before the start of a token,
+ // so we should increment the token's start position to reflect its true
+ // start position
+ tokenStart = nameStart;
+ } else {
+ // Otherwise this separator was found in the middle of a token being matched,
+ // so increase the separator count
+ seperatorCount++;
+ }
+ }
+ }
+ // if we have no complete match at this point, then we attempt to fall back to the partial
+ // token match(if any). If we don't allow initial matching (ALLOW_INITIAL_MATCH = false)
+ // then partial will always be empty.
+ if (!partial.isEmpty()) {
+ matchList.addAll(partial);
+ for (SmartDialMatchPosition match : matchList) {
+ replaceBitInMask(builder, match);
+ }
+ mNameMatchMask = builder.toString();
+ return true;
+ }
+ return false;
+ }
+
+ public boolean matches(String displayName) {
+ mMatchPositions.clear();
+ return matchesCombination(displayName, mQuery, mMatchPositions);
+ }
+
+ public ArrayList<SmartDialMatchPosition> getMatchPositions() {
+ // Return a clone of mMatchPositions so that the caller can use it without
+ // worrying about it changing
+ return new ArrayList<SmartDialMatchPosition>(mMatchPositions);
+ }
+
+ public String getNameMatchPositionsInString() {
+ return mNameMatchMask;
+ }
+
+ public String getNumberMatchPositionsInString() {
+ return mPhoneNumberMatchMask;
+ }
+
+ public String getQuery() {
+ return mQuery;
+ }
+
+ public void setQuery(String query) {
+ mQuery = query;
+ }
+
+ public void setShouldMatchEmptyQuery(boolean matches) {
+ mShouldMatchEmptyQuery = matches;
+ }
+}
diff --git a/java/com/android/dialer/smartdial/SmartDialPrefix.java b/java/com/android/dialer/smartdial/SmartDialPrefix.java
new file mode 100644
index 000000000..a000e21c5
--- /dev/null
+++ b/java/com/android/dialer/smartdial/SmartDialPrefix.java
@@ -0,0 +1,605 @@
+/*
+ * 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.smartdial;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Smart Dial utility class to find prefixes of contacts. It contains both methods to find supported
+ * prefix combinations for contact names, and also methods to find supported prefix combinations for
+ * contacts' phone numbers. Each contact name is separated into several tokens, such as first name,
+ * middle name, family name etc. Each phone number is also separated into country code, NANP area
+ * code, and local number if such separation is possible.
+ */
+public class SmartDialPrefix {
+
+ /**
+ * The number of starting and ending tokens in a contact's name considered for initials. For
+ * example, if both constants are set to 2, and a contact's name is "Albert Ben Charles Daniel Ed
+ * Foster", the first two tokens "Albert" "Ben", and last two tokens "Ed" "Foster" can be replaced
+ * by their initials in contact name matching. Users can look up this contact by combinations of
+ * his initials such as "AF" "BF" "EF" "ABF" "BEF" "ABEF" etc, but can not use combinations such
+ * as "CF" "DF" "ACF" "ADF" etc.
+ */
+ private static final int LAST_TOKENS_FOR_INITIALS = 2;
+
+ private static final int FIRST_TOKENS_FOR_INITIALS = 2;
+
+ /** The country code of the user's sim card obtained by calling getSimCountryIso */
+ private static final String PREF_USER_SIM_COUNTRY_CODE =
+ "DialtactsActivity_user_sim_country_code";
+
+ private static final String PREF_USER_SIM_COUNTRY_CODE_DEFAULT = null;
+ /** Dialpad mapping. */
+ private static final SmartDialMap mMap = new LatinSmartDialMap();
+
+ private static String sUserSimCountryCode = PREF_USER_SIM_COUNTRY_CODE_DEFAULT;
+ /** Indicates whether user is in NANP regions. */
+ private static boolean sUserInNanpRegion = false;
+ /** Set of country names that use NANP code. */
+ private static Set<String> sNanpCountries = null;
+ /** Set of supported country codes in front of the phone number. */
+ private static Set<String> sCountryCodes = null;
+
+ private static boolean sNanpInitialized = false;
+
+ /** Initializes the Nanp settings, and finds out whether user is in a NANP region. */
+ public static void initializeNanpSettings(Context context) {
+ final TelephonyManager manager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ if (manager != null) {
+ sUserSimCountryCode = manager.getSimCountryIso();
+ }
+
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ if (sUserSimCountryCode != null) {
+ /** Updates shared preferences with the latest country obtained from getSimCountryIso. */
+ prefs.edit().putString(PREF_USER_SIM_COUNTRY_CODE, sUserSimCountryCode).apply();
+ } else {
+ /** Uses previously stored country code if loading fails. */
+ sUserSimCountryCode =
+ prefs.getString(PREF_USER_SIM_COUNTRY_CODE, PREF_USER_SIM_COUNTRY_CODE_DEFAULT);
+ }
+ /** Queries the NANP country list to find out whether user is in a NANP region. */
+ sUserInNanpRegion = isCountryNanp(sUserSimCountryCode);
+ sNanpInitialized = true;
+ }
+
+ /**
+ * Parses a contact's name into a list of separated tokens.
+ *
+ * @param contactName Contact's name stored in string.
+ * @return A list of name tokens, for example separated first names, last name, etc.
+ */
+ public static ArrayList<String> parseToIndexTokens(String contactName) {
+ final int length = contactName.length();
+ final ArrayList<String> result = new ArrayList<>();
+ char c;
+ final StringBuilder currentIndexToken = new StringBuilder();
+ /**
+ * Iterates through the whole name string. If the current character is a valid character, append
+ * it to the current token. If the current character is not a valid character, for example space
+ * " ", mark the current token as complete and add it to the list of tokens.
+ */
+ for (int i = 0; i < length; i++) {
+ c = mMap.normalizeCharacter(contactName.charAt(i));
+ if (mMap.isValidDialpadCharacter(c)) {
+ /** Converts a character into the number on dialpad that represents the character. */
+ currentIndexToken.append(mMap.getDialpadIndex(c));
+ } else {
+ if (currentIndexToken.length() != 0) {
+ result.add(currentIndexToken.toString());
+ }
+ currentIndexToken.delete(0, currentIndexToken.length());
+ }
+ }
+
+ /** Adds the last token in case it has not been added. */
+ if (currentIndexToken.length() != 0) {
+ result.add(currentIndexToken.toString());
+ }
+ return result;
+ }
+
+ /**
+ * Generates a list of strings that any prefix of any string in the list can be used to look up
+ * the contact's name.
+ *
+ * @param index The contact's name in string.
+ * @return A List of strings, whose prefix can be used to look up the contact.
+ */
+ public static ArrayList<String> generateNamePrefixes(String index) {
+ final ArrayList<String> result = new ArrayList<>();
+
+ /** Parses the name into a list of tokens. */
+ final ArrayList<String> indexTokens = parseToIndexTokens(index);
+
+ if (indexTokens.size() > 0) {
+ /**
+ * Adds the full token combinations to the list. For example, a contact with name "Albert Ben
+ * Ed Foster" can be looked up by any prefix of the following strings "Foster" "EdFoster"
+ * "BenEdFoster" and "AlbertBenEdFoster". This covers all cases of look up that contains only
+ * one token, and that spans multiple continuous tokens.
+ */
+ final StringBuilder fullNameToken = new StringBuilder();
+ for (int i = indexTokens.size() - 1; i >= 0; i--) {
+ fullNameToken.insert(0, indexTokens.get(i));
+ result.add(fullNameToken.toString());
+ }
+
+ /**
+ * Adds initial combinations to the list, with the number of initials restricted by {@link
+ * #LAST_TOKENS_FOR_INITIALS} and {@link #FIRST_TOKENS_FOR_INITIALS}. For example, a contact
+ * with name "Albert Ben Ed Foster" can be looked up by any prefix of the following strings
+ * "EFoster" "BFoster" "BEFoster" "AFoster" "ABFoster" "AEFoster" and "ABEFoster". This covers
+ * all cases of initial lookup.
+ */
+ ArrayList<String> fullNames = new ArrayList<>();
+ fullNames.add(indexTokens.get(indexTokens.size() - 1));
+ final int recursiveNameStart = result.size();
+ int recursiveNameEnd = result.size();
+ String initial = "";
+ for (int i = indexTokens.size() - 2; i >= 0; i--) {
+ if ((i >= indexTokens.size() - LAST_TOKENS_FOR_INITIALS)
+ || (i < FIRST_TOKENS_FOR_INITIALS)) {
+ initial = indexTokens.get(i).substring(0, 1);
+
+ /** Recursively adds initial combinations to the list. */
+ for (int j = 0; j < fullNames.size(); ++j) {
+ result.add(initial + fullNames.get(j));
+ }
+ for (int j = recursiveNameStart; j < recursiveNameEnd; ++j) {
+ result.add(initial + result.get(j));
+ }
+ recursiveNameEnd = result.size();
+ final String currentFullName = fullNames.get(fullNames.size() - 1);
+ fullNames.add(indexTokens.get(i) + currentFullName);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Computes a list of number strings based on tokens of a given phone number. Any prefix of any
+ * string in the list can be used to look up the phone number. The list include the full phone
+ * number, the national number if there is a country code in the phone number, and the local
+ * number if there is an area code in the phone number following the NANP format. For example, if
+ * a user has phone number +41 71 394 8392, the list will contain 41713948392 and 713948392. Any
+ * prefix to either of the strings can be used to look up the phone number. If a user has a phone
+ * number +1 555-302-3029 (NANP format), the list will contain 15553023029, 5553023029, and
+ * 3023029.
+ *
+ * @param number String of user's phone number.
+ * @return A list of strings where any prefix of any entry can be used to look up the number.
+ */
+ public static ArrayList<String> parseToNumberTokens(String number) {
+ final ArrayList<String> result = new ArrayList<>();
+ if (!TextUtils.isEmpty(number)) {
+ /** Adds the full number to the list. */
+ result.add(SmartDialNameMatcher.normalizeNumber(number, mMap));
+
+ final PhoneNumberTokens phoneNumberTokens = parsePhoneNumber(number);
+ if (phoneNumberTokens == null) {
+ return result;
+ }
+
+ if (phoneNumberTokens.countryCodeOffset != 0) {
+ result.add(
+ SmartDialNameMatcher.normalizeNumber(
+ number, phoneNumberTokens.countryCodeOffset, mMap));
+ }
+
+ if (phoneNumberTokens.nanpCodeOffset != 0) {
+ result.add(
+ SmartDialNameMatcher.normalizeNumber(number, phoneNumberTokens.nanpCodeOffset, mMap));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Parses a phone number to find out whether it has country code and NANP area code.
+ *
+ * @param number Raw phone number.
+ * @return a PhoneNumberToken instance with country code, NANP code information.
+ */
+ public static PhoneNumberTokens parsePhoneNumber(String number) {
+ String countryCode = "";
+ int countryCodeOffset = 0;
+ int nanpNumberOffset = 0;
+
+ if (!TextUtils.isEmpty(number)) {
+ String normalizedNumber = SmartDialNameMatcher.normalizeNumber(number, mMap);
+ if (number.charAt(0) == '+') {
+ /** If the number starts with '+', tries to find valid country code. */
+ for (int i = 1; i <= 1 + 3; i++) {
+ if (number.length() <= i) {
+ break;
+ }
+ countryCode = number.substring(1, i);
+ if (isValidCountryCode(countryCode)) {
+ countryCodeOffset = i;
+ break;
+ }
+ }
+ } else {
+ /**
+ * If the number does not start with '+', finds out whether it is in NANP format and has '1'
+ * preceding the number.
+ */
+ if ((normalizedNumber.length() == 11)
+ && (normalizedNumber.charAt(0) == '1')
+ && (sUserInNanpRegion)) {
+ countryCode = "1";
+ countryCodeOffset = number.indexOf(normalizedNumber.charAt(1));
+ if (countryCodeOffset == -1) {
+ countryCodeOffset = 0;
+ }
+ }
+ }
+
+ /** If user is in NANP region, finds out whether a number is in NANP format. */
+ if (sUserInNanpRegion) {
+ String areaCode = "";
+ if (countryCode.equals("") && normalizedNumber.length() == 10) {
+ /**
+ * if the number has no country code but fits the NANP format, extracts the NANP area
+ * code, and finds out offset of the local number.
+ */
+ areaCode = normalizedNumber.substring(0, 3);
+ } else if (countryCode.equals("1") && normalizedNumber.length() == 11) {
+ /**
+ * If the number has country code '1', finds out area code and offset of the local number.
+ */
+ areaCode = normalizedNumber.substring(1, 4);
+ }
+ if (!areaCode.equals("")) {
+ final int areaCodeIndex = number.indexOf(areaCode);
+ if (areaCodeIndex != -1) {
+ nanpNumberOffset = number.indexOf(areaCode) + 3;
+ }
+ }
+ }
+ }
+ return new PhoneNumberTokens(countryCode, countryCodeOffset, nanpNumberOffset);
+ }
+
+ /** Checkes whether a country code is valid. */
+ private static boolean isValidCountryCode(String countryCode) {
+ if (sCountryCodes == null) {
+ sCountryCodes = initCountryCodes();
+ }
+ return sCountryCodes.contains(countryCode);
+ }
+
+ private static Set<String> initCountryCodes() {
+ final HashSet<String> result = new HashSet<String>();
+ result.add("1");
+ result.add("7");
+ result.add("20");
+ result.add("27");
+ result.add("30");
+ result.add("31");
+ result.add("32");
+ result.add("33");
+ result.add("34");
+ result.add("36");
+ result.add("39");
+ result.add("40");
+ result.add("41");
+ result.add("43");
+ result.add("44");
+ result.add("45");
+ result.add("46");
+ result.add("47");
+ result.add("48");
+ result.add("49");
+ result.add("51");
+ result.add("52");
+ result.add("53");
+ result.add("54");
+ result.add("55");
+ result.add("56");
+ result.add("57");
+ result.add("58");
+ result.add("60");
+ result.add("61");
+ result.add("62");
+ result.add("63");
+ result.add("64");
+ result.add("65");
+ result.add("66");
+ result.add("81");
+ result.add("82");
+ result.add("84");
+ result.add("86");
+ result.add("90");
+ result.add("91");
+ result.add("92");
+ result.add("93");
+ result.add("94");
+ result.add("95");
+ result.add("98");
+ result.add("211");
+ result.add("212");
+ result.add("213");
+ result.add("216");
+ result.add("218");
+ result.add("220");
+ result.add("221");
+ result.add("222");
+ result.add("223");
+ result.add("224");
+ result.add("225");
+ result.add("226");
+ result.add("227");
+ result.add("228");
+ result.add("229");
+ result.add("230");
+ result.add("231");
+ result.add("232");
+ result.add("233");
+ result.add("234");
+ result.add("235");
+ result.add("236");
+ result.add("237");
+ result.add("238");
+ result.add("239");
+ result.add("240");
+ result.add("241");
+ result.add("242");
+ result.add("243");
+ result.add("244");
+ result.add("245");
+ result.add("246");
+ result.add("247");
+ result.add("248");
+ result.add("249");
+ result.add("250");
+ result.add("251");
+ result.add("252");
+ result.add("253");
+ result.add("254");
+ result.add("255");
+ result.add("256");
+ result.add("257");
+ result.add("258");
+ result.add("260");
+ result.add("261");
+ result.add("262");
+ result.add("263");
+ result.add("264");
+ result.add("265");
+ result.add("266");
+ result.add("267");
+ result.add("268");
+ result.add("269");
+ result.add("290");
+ result.add("291");
+ result.add("297");
+ result.add("298");
+ result.add("299");
+ result.add("350");
+ result.add("351");
+ result.add("352");
+ result.add("353");
+ result.add("354");
+ result.add("355");
+ result.add("356");
+ result.add("357");
+ result.add("358");
+ result.add("359");
+ result.add("370");
+ result.add("371");
+ result.add("372");
+ result.add("373");
+ result.add("374");
+ result.add("375");
+ result.add("376");
+ result.add("377");
+ result.add("378");
+ result.add("379");
+ result.add("380");
+ result.add("381");
+ result.add("382");
+ result.add("385");
+ result.add("386");
+ result.add("387");
+ result.add("389");
+ result.add("420");
+ result.add("421");
+ result.add("423");
+ result.add("500");
+ result.add("501");
+ result.add("502");
+ result.add("503");
+ result.add("504");
+ result.add("505");
+ result.add("506");
+ result.add("507");
+ result.add("508");
+ result.add("509");
+ result.add("590");
+ result.add("591");
+ result.add("592");
+ result.add("593");
+ result.add("594");
+ result.add("595");
+ result.add("596");
+ result.add("597");
+ result.add("598");
+ result.add("599");
+ result.add("670");
+ result.add("672");
+ result.add("673");
+ result.add("674");
+ result.add("675");
+ result.add("676");
+ result.add("677");
+ result.add("678");
+ result.add("679");
+ result.add("680");
+ result.add("681");
+ result.add("682");
+ result.add("683");
+ result.add("685");
+ result.add("686");
+ result.add("687");
+ result.add("688");
+ result.add("689");
+ result.add("690");
+ result.add("691");
+ result.add("692");
+ result.add("800");
+ result.add("808");
+ result.add("850");
+ result.add("852");
+ result.add("853");
+ result.add("855");
+ result.add("856");
+ result.add("870");
+ result.add("878");
+ result.add("880");
+ result.add("881");
+ result.add("882");
+ result.add("883");
+ result.add("886");
+ result.add("888");
+ result.add("960");
+ result.add("961");
+ result.add("962");
+ result.add("963");
+ result.add("964");
+ result.add("965");
+ result.add("966");
+ result.add("967");
+ result.add("968");
+ result.add("970");
+ result.add("971");
+ result.add("972");
+ result.add("973");
+ result.add("974");
+ result.add("975");
+ result.add("976");
+ result.add("977");
+ result.add("979");
+ result.add("992");
+ result.add("993");
+ result.add("994");
+ result.add("995");
+ result.add("996");
+ result.add("998");
+ return result;
+ }
+
+ public static SmartDialMap getMap() {
+ return mMap;
+ }
+
+ /**
+ * Indicates whether the given country uses NANP numbers
+ *
+ * @param country ISO 3166 country code (case doesn't matter)
+ * @return True if country uses NANP numbers (e.g. US, Canada), false otherwise
+ * @see <a href="https://en.wikipedia.org/wiki/North_American_Numbering_Plan">
+ * https://en.wikipedia.org/wiki/North_American_Numbering_Plan</a>
+ */
+ @VisibleForTesting
+ public static boolean isCountryNanp(String country) {
+ if (TextUtils.isEmpty(country)) {
+ return false;
+ }
+ if (sNanpCountries == null) {
+ sNanpCountries = initNanpCountries();
+ }
+ return sNanpCountries.contains(country.toUpperCase());
+ }
+
+ private static Set<String> initNanpCountries() {
+ final HashSet<String> result = new HashSet<String>();
+ result.add("US"); // United States
+ result.add("CA"); // Canada
+ result.add("AS"); // American Samoa
+ result.add("AI"); // Anguilla
+ result.add("AG"); // Antigua and Barbuda
+ result.add("BS"); // Bahamas
+ result.add("BB"); // Barbados
+ result.add("BM"); // Bermuda
+ result.add("VG"); // British Virgin Islands
+ result.add("KY"); // Cayman Islands
+ result.add("DM"); // Dominica
+ result.add("DO"); // Dominican Republic
+ result.add("GD"); // Grenada
+ result.add("GU"); // Guam
+ result.add("JM"); // Jamaica
+ result.add("PR"); // Puerto Rico
+ result.add("MS"); // Montserrat
+ result.add("MP"); // Northern Mariana Islands
+ result.add("KN"); // Saint Kitts and Nevis
+ result.add("LC"); // Saint Lucia
+ result.add("VC"); // Saint Vincent and the Grenadines
+ result.add("TT"); // Trinidad and Tobago
+ result.add("TC"); // Turks and Caicos Islands
+ result.add("VI"); // U.S. Virgin Islands
+ return result;
+ }
+
+ /**
+ * Returns whether the user is in a region that uses Nanp format based on the sim location.
+ *
+ * @return Whether user is in Nanp region.
+ */
+ public static boolean getUserInNanpRegion() {
+ return sUserInNanpRegion;
+ }
+
+ /** Explicitly setting the user Nanp to the given boolean */
+ @VisibleForTesting
+ public static void setUserInNanpRegion(boolean userInNanpRegion) {
+ sUserInNanpRegion = userInNanpRegion;
+ }
+
+ /** Class to record phone number parsing information. */
+ public static class PhoneNumberTokens {
+
+ /** Country code of the phone number. */
+ final String countryCode;
+
+ /** Offset of national number after the country code. */
+ final int countryCodeOffset;
+
+ /** Offset of local number after NANP area code. */
+ final int nanpCodeOffset;
+
+ public PhoneNumberTokens(String countryCode, int countryCodeOffset, int nanpCodeOffset) {
+ this.countryCode = countryCode;
+ this.countryCodeOffset = countryCodeOffset;
+ this.nanpCodeOffset = nanpCodeOffset;
+ }
+ }
+}
diff --git a/java/com/android/dialer/spam/Spam.java b/java/com/android/dialer/spam/Spam.java
new file mode 100644
index 000000000..692a1a0ad
--- /dev/null
+++ b/java/com/android/dialer/spam/Spam.java
@@ -0,0 +1,49 @@
+/*
+ * 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.spam;
+
+import android.content.Context;
+import java.util.Objects;
+
+/** Accessor for the spam bindings. */
+public class Spam {
+
+ private static SpamBindings spamBindings;
+
+ private Spam() {}
+
+ public static SpamBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (spamBindings != null) {
+ return spamBindings;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof SpamBindingsFactory) {
+ spamBindings = ((SpamBindingsFactory) application).newSpamBindings();
+ }
+
+ if (spamBindings == null) {
+ spamBindings = new SpamBindingsStub();
+ }
+ return spamBindings;
+ }
+
+ public static void setForTesting(SpamBindings spamBindings) {
+ Spam.spamBindings = spamBindings;
+ }
+}
diff --git a/java/com/android/dialer/spam/SpamBindings.java b/java/com/android/dialer/spam/SpamBindings.java
new file mode 100644
index 000000000..b5d18b828
--- /dev/null
+++ b/java/com/android/dialer/spam/SpamBindings.java
@@ -0,0 +1,146 @@
+/*
+ * 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.spam;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+/** Allows the container application to mark calls as spam. */
+public interface SpamBindings {
+
+ boolean isSpamEnabled();
+
+ boolean isSpamNotificationEnabled();
+
+ boolean isDialogEnabledForSpamNotification();
+
+ boolean isDialogReportSpamCheckedByDefault();
+
+ /** @return what percentage of aftercall notifications to show to the user */
+ int percentOfSpamNotificationsToShow();
+
+ int percentOfNonSpamNotificationsToShow();
+
+ /**
+ * Checks if the given number is suspected of being a spamer.
+ *
+ * @param number The phone number of the call.
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkSpamStatus(String number, String countryIso, Listener listener);
+
+ /**
+ * @param number The number to check if the number is in the user's white list (non spam list)
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkUserMarkedNonSpamStatus(
+ String number, @Nullable String countryIso, @NonNull Listener listener);
+
+ /**
+ * @param number The number to check if it is in user's spam list
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkUserMarkedSpamStatus(
+ String number, @Nullable String countryIso, @NonNull Listener listener);
+
+ /**
+ * @param number The number to check if it is in the global spam list
+ * @param countryIso The country ISO of the call.
+ * @param listener The callback to be invoked after {@code Info} is fetched.
+ */
+ void checkGlobalSpamListStatus(
+ String number, @Nullable String countryIso, @NonNull Listener listener);
+
+ /**
+ * Synchronously checks if the given number is suspected of being a spamer.
+ *
+ * @param number The phone number of the call.
+ * @param countryIso The country ISO of the call.
+ * @return True if the number is spam.
+ */
+ boolean checkSpamStatusSynchronous(String number, String countryIso);
+
+ /**
+ * Reports number as spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactLookupResultType The result of the contact lookup for this phone number. Must be
+ * one of {@link com.android.dialer.logging.nano.ContactLookupResult}.
+ */
+ void reportSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType);
+
+ /**
+ * Reports number as spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactSourceType If we have cached contact information for the phone number, this
+ * indicates its source. Must be one of {@link com.android.dialer.logging.nano.ContactSource}.
+ */
+ void reportSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType);
+
+ /**
+ * Reports number as not spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactLookupResultType The result of the contact lookup for this phone number. Must be
+ * one of {@link com.android.dialer.logging.nano.ContactLookupResult}.
+ */
+ void reportNotSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType);
+
+ /**
+ * Reports number as not spam.
+ *
+ * @param number The number to be reported.
+ * @param countryIso The country ISO of the number.
+ * @param callType Whether the type of call is missed, voicemail, etc. Example of this is {@link
+ * android.provider.CallLog.Calls#VOICEMAIL_TYPE}.
+ * @param from Where in the dialer this was reported from. Must be one of {@link
+ * com.android.dialer.logging.nano.ReportingLocation}.
+ * @param contactSourceType If we have cached contact information for the phone number, this
+ * indicates its source. Must be one of {@link com.android.dialer.logging.nano.ContactSource}.
+ */
+ void reportNotSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType);
+
+ /** Callback to be invoked when data is fetched. */
+ interface Listener {
+
+ /** Called when data is fetched. */
+ void onComplete(boolean isSpam);
+ }
+}
diff --git a/java/com/android/dialer/spam/SpamBindingsFactory.java b/java/com/android/dialer/spam/SpamBindingsFactory.java
new file mode 100644
index 000000000..41144e1ee
--- /dev/null
+++ b/java/com/android/dialer/spam/SpamBindingsFactory.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.spam;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows this module to get
+ * references to the SpamBindings.
+ */
+public interface SpamBindingsFactory {
+
+ SpamBindings newSpamBindings();
+}
diff --git a/java/com/android/dialer/spam/SpamBindingsStub.java b/java/com/android/dialer/spam/SpamBindingsStub.java
new file mode 100644
index 000000000..08939530c
--- /dev/null
+++ b/java/com/android/dialer/spam/SpamBindingsStub.java
@@ -0,0 +1,92 @@
+/*
+ * 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.spam;
+
+/** Default implementation of SpamBindings. */
+public class SpamBindingsStub implements SpamBindings {
+
+ @Override
+ public boolean isSpamEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isSpamNotificationEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isDialogEnabledForSpamNotification() {
+ return false;
+ }
+
+ @Override
+ public boolean isDialogReportSpamCheckedByDefault() {
+ return false;
+ }
+
+ @Override
+ public int percentOfSpamNotificationsToShow() {
+ return 0;
+ }
+
+ @Override
+ public int percentOfNonSpamNotificationsToShow() {
+ return 0;
+ }
+
+ @Override
+ public void checkSpamStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public void checkUserMarkedNonSpamStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public void checkUserMarkedSpamStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public void checkGlobalSpamListStatus(String number, String countryIso, Listener listener) {
+ listener.onComplete(false);
+ }
+
+ @Override
+ public boolean checkSpamStatusSynchronous(String number, String countryIso) {
+ return false;
+ }
+
+ @Override
+ public void reportSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType) {}
+
+ @Override
+ public void reportSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType) {}
+
+ @Override
+ public void reportNotSpamFromAfterCallNotification(
+ String number, String countryIso, int callType, int from, int contactLookupResultType) {}
+
+ @Override
+ public void reportNotSpamFromCallHistory(
+ String number, String countryIso, int callType, int from, int contactSourceType) {}
+}
diff --git a/java/com/android/dialer/telecom/TelecomUtil.java b/java/com/android/dialer/telecom/TelecomUtil.java
new file mode 100644
index 000000000..a11e7f77a
--- /dev/null
+++ b/java/com/android/dialer/telecom/TelecomUtil.java
@@ -0,0 +1,212 @@
+/*
+ * 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.telecom;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Performs permission checks before calling into TelecomManager. Each method is self-explanatory -
+ * perform the required check and return the fallback default if the permission is missing,
+ * otherwise return the value from TelecomManager.
+ */
+public class TelecomUtil {
+
+ private static final String TAG = "TelecomUtil";
+ private static boolean sWarningLogged = false;
+
+ public static void showInCallScreen(Context context, boolean showDialpad) {
+ if (hasReadPhoneStatePermission(context)) {
+ try {
+ getTelecomManager(context).showInCallScreen(showDialpad);
+ } catch (SecurityException e) {
+ // Just in case
+ Log.w(TAG, "TelecomManager.showInCallScreen called without permission.");
+ }
+ }
+ }
+
+ public static void silenceRinger(Context context) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ getTelecomManager(context).silenceRinger();
+ } catch (SecurityException e) {
+ // Just in case
+ Log.w(TAG, "TelecomManager.silenceRinger called without permission.");
+ }
+ }
+ }
+
+ public static void cancelMissedCallsNotification(Context context) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ getTelecomManager(context).cancelMissedCallsNotification();
+ } catch (SecurityException e) {
+ Log.w(TAG, "TelecomManager.cancelMissedCalls called without permission.");
+ }
+ }
+ }
+
+ public static Uri getAdnUriForPhoneAccount(Context context, PhoneAccountHandle handle) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ return getTelecomManager(context).getAdnUriForPhoneAccount(handle);
+ } catch (SecurityException e) {
+ Log.w(TAG, "TelecomManager.getAdnUriForPhoneAccount called without permission.");
+ }
+ }
+ return null;
+ }
+
+ public static boolean handleMmi(
+ Context context, String dialString, @Nullable PhoneAccountHandle handle) {
+ if (hasModifyPhoneStatePermission(context)) {
+ try {
+ if (handle == null) {
+ return getTelecomManager(context).handleMmi(dialString);
+ } else {
+ return getTelecomManager(context).handleMmi(dialString, handle);
+ }
+ } catch (SecurityException e) {
+ Log.w(TAG, "TelecomManager.handleMmi called without permission.");
+ }
+ }
+ return false;
+ }
+
+ @Nullable
+ public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
+ Context context, String uriScheme) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).getDefaultOutgoingPhoneAccount(uriScheme);
+ }
+ return null;
+ }
+
+ public static PhoneAccount getPhoneAccount(Context context, PhoneAccountHandle handle) {
+ return getTelecomManager(context).getPhoneAccount(handle);
+ }
+
+ public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(Context context) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).getCallCapablePhoneAccounts();
+ }
+ return new ArrayList<>();
+ }
+
+ public static boolean isInCall(Context context) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).isInCall();
+ }
+ return false;
+ }
+
+ public static boolean isVoicemailNumber(
+ Context context, PhoneAccountHandle accountHandle, String number) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).isVoiceMailNumber(accountHandle, number);
+ }
+ return false;
+ }
+
+ @Nullable
+ public static String getVoicemailNumber(Context context, PhoneAccountHandle accountHandle) {
+ if (hasReadPhoneStatePermission(context)) {
+ return getTelecomManager(context).getVoiceMailNumber(accountHandle);
+ }
+ return null;
+ }
+
+ /**
+ * Tries to place a call using the {@link TelecomManager}.
+ *
+ * @param context context.
+ * @param intent the call intent.
+ * @return {@code true} if we successfully attempted to place the call, {@code false} if it failed
+ * due to a permission check.
+ */
+ public static boolean placeCall(Context context, Intent intent) {
+ if (hasCallPhonePermission(context)) {
+ getTelecomManager(context).placeCall(intent.getData(), intent.getExtras());
+ return true;
+ }
+ return false;
+ }
+
+ public static Uri getCallLogUri(Context context) {
+ return hasReadWriteVoicemailPermissions(context)
+ ? Calls.CONTENT_URI_WITH_VOICEMAIL
+ : Calls.CONTENT_URI;
+ }
+
+ public static boolean hasReadWriteVoicemailPermissions(Context context) {
+ return isDefaultDialer(context)
+ || (hasPermission(context, Manifest.permission.READ_VOICEMAIL)
+ && hasPermission(context, Manifest.permission.WRITE_VOICEMAIL));
+ }
+
+ public static boolean hasModifyPhoneStatePermission(Context context) {
+ return isDefaultDialer(context)
+ || hasPermission(context, Manifest.permission.MODIFY_PHONE_STATE);
+ }
+
+ public static boolean hasReadPhoneStatePermission(Context context) {
+ return isDefaultDialer(context) || hasPermission(context, Manifest.permission.READ_PHONE_STATE);
+ }
+
+ public static boolean hasCallPhonePermission(Context context) {
+ return isDefaultDialer(context) || hasPermission(context, Manifest.permission.CALL_PHONE);
+ }
+
+ private static boolean hasPermission(Context context, String permission) {
+ return ContextCompat.checkSelfPermission(context, permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean isDefaultDialer(Context context) {
+ final boolean result =
+ TextUtils.equals(
+ context.getPackageName(), getTelecomManager(context).getDefaultDialerPackage());
+ if (result) {
+ sWarningLogged = false;
+ } else {
+ if (!sWarningLogged) {
+ // Log only once to prevent spam.
+ Log.w(TAG, "Dialer is not currently set to be default dialer");
+ sWarningLogged = true;
+ }
+ }
+ return result;
+ }
+
+ private static TelecomManager getTelecomManager(Context context) {
+ return (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ }
+}
diff --git a/java/com/android/dialer/theme/AndroidManifest.xml b/java/com/android/dialer/theme/AndroidManifest.xml
new file mode 100644
index 000000000..7c1e4effd
--- /dev/null
+++ b/java/com/android/dialer/theme/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.theme">
+</manifest>
diff --git a/java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml b/java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml
new file mode 100644
index 000000000..30986457b
--- /dev/null
+++ b/java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <set
+ android:ordering="sequentially">
+ <objectAnimator
+ android:duration="500"
+ android:propertyName="rotation"
+ android:valueFrom="0.0"
+ android:valueTo="-180.0"
+ android:valueType="floatType"
+ android:interpolator="@android:interpolator/fast_out_slow_in"/>
+ </set>
+</set> \ No newline at end of file
diff --git a/java/com/android/dialer/theme/res/animator/activated_button_elevation.xml b/java/com/android/dialer/theme/res/animator/activated_button_elevation.xml
new file mode 100644
index 000000000..b8ea4e8e6
--- /dev/null
+++ b/java/com/android/dialer/theme/res/animator/activated_button_elevation.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_activated="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="0dp"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="4dp"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/java/com/android/dialer/theme/res/animator/button_elevation.xml b/java/com/android/dialer/theme/res/animator/button_elevation.xml
new file mode 100644
index 000000000..8dd019e14
--- /dev/null
+++ b/java/com/android/dialer/theme/res/animator/button_elevation.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_pressed="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="0dp"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueFrom="4dp"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml b/java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml
new file mode 100644
index 000000000..2dc3eb1fa
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:name="front_back_switch_button"
+ android:width="56dp"
+ android:viewportWidth="56"
+ android:height="56dp"
+ android:viewportHeight="56">
+ <group
+ android:name="layer_3_outlines"
+ android:translateX="32.0015"
+ android:translateY="27.00208">
+ <group
+ android:name="layer_3_outlines_pivot"
+ android:translateX="-4.25"
+ android:translateY="-7.25">
+ <group
+ android:name="group_1"
+ android:translateX="4.25"
+ android:translateY="7.25001">
+ <path
+ android:name="path_1"
+ android:pathData="M 2.0,-5.0 c 0.0,0.0 -2.16999816895,0.0 -2.16999816895,0.0 c 0.0,0.0 -1.83000183105,-2.0 -1.83000183105,-2.0 c 0.0,0.0 -2.0,0.0 -2.0,0.0 c 0.0,0.0 0.0,5.0 0.0,5.0 c 1.65600585938,0.0 3.0,1.34399414062 3.0,3.0 c 0.0,1.65600585938 -1.34399414062,3.0 -3.0,3.0 c 0.0,0.0 -3.0,0.00181579589844 -3.0,0.00181579589844 c 0.0,0.0 0.001953125,2.99415588379 0.001953125,2.99415588379 c 0.0,0.0 2.998046875,0.0040283203125 2.998046875,0.0040283203125 c 0.0,0.0 6.0,0.0 6.0,0.0 c 1.10000610352,0.0 2.0,-0.899993896484 2.0,-2.0 c 0.0,0.0 0.0,-8.0 0.0,-8.0 c 0.0,-1.10000610352 -0.899993896484,-2.0 -2.0,-2.0 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="layer_1_outlines"
+ android:translateX="24.00099"
+ android:translateY="26.99992">
+ <group
+ android:name="layer_1_outlines_pivot"
+ android:translateX="-4.249"
+ android:translateY="-7.25">
+ <group
+ android:name="group_2"
+ android:translateX="4.249"
+ android:translateY="7.25">
+ <path
+ android:name="path_2"
+ android:pathData="M 3.99900817871,4.0 c -1.65501403809,-0.00099182128906 -2.99800109863,-1.34399414062 -2.99800109863,-3.0 c 0.0,-1.6549987793 1.34298706055,-2.99899291992 2.99800109863,-3.0 c 0.0,0.0 1.0,0.00008 1.0,0.00008 c 0.0,0.0 0.0,-5.0 0.0,-5.0 c 0.0,0.0 -1.0,-0.00008 -1.0,-0.00008 c 0.0,0.0 -1.99800109863,0.0 -1.99800109863,0.0 c 0.0,0.0 -1.83000183105,2.0 -1.83000183105,2.0 c 0.0,0.0 -2.17001342773,0.0 -2.17001342773,0.0 c -1.1009979248,0.0 -2.0,0.899993896484 -2.0,2.0 c 0.0,0.0 0.0,8.0 0.0,8.0 c 0.0,1.10000610352 0.899002075195,2.0 2.0,2.0 c 0.0,0.0 5.99801635742,0.0 5.99801635742,0.0 c 0.0,0.0 0.0,-3.0 0.0,-3.0 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ </group>
+ </group>
+ <group
+ android:name="layer_10_outlines"
+ android:translateX="28.00001"
+ android:translateY="27.99999">
+ <group
+ android:name="layer_10_outlines_pivot"
+ android:translateX="-19.25"
+ android:translateY="-19.25005">
+ <group
+ android:name="group_3"
+ android:translateX="20.25"
+ android:translateY="9.75001">
+ <path
+ android:name="path_3"
+ android:pathData="M 12.4349975586,-3.93499755859 c -3.70999145508,-3.71000671387 -8.57299804688,-5.56500244141 -13.4349975586,-5.56500244141 c -4.8630065918,0.0 -9.72500610352,1.85499572754 -13.4349975586,5.56500244141 c -0.337005615234,0.336990356445 -0.652008056641,0.688003540039 -0.956008911133,1.04399108887 c 0.0,0.0 -2.60899353027,-2.60899353027 -2.60899353027,-2.60899353027 c 0.0,0.0 0.0,7.0 0.0,7.0 c 0.0,0.0 7.0,0.0 7.0,0.0 c 0.0,0.0 -2.97300720215,-2.97300720215 -2.97300720215,-2.97300720215 c 0.300003051758,-0.360992431641 0.616012573242,-0.711990356445 0.952011108398,-1.0479888916 c 3.21099853516,-3.21099853516 7.47999572754,-4.97900390625 12.0209960938,-4.97900390625 c 4.54100036621,0.0 8.80999755859,1.76800537109 12.0209960938,4.97900390625 c 3.31401062012,3.31399536133 4.97100830078,7.66799926758 4.97100830078,12.0209960938 c 0.0,0.0 2.00799560547,0.0 2.00799560547,0.0 c 0.0,-4.86199951172 -1.85499572754,-9.72500610352 -5.56500244141,-13.4349975586 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ <group
+ android:name="group_4"
+ android:translateX="18.25"
+ android:translateY="28.75011">
+ <path
+ android:name="path_4"
+ android:pathData="M 18.0,-1.5 c 0.0,0.0 -7.0,0.0 -7.0,0.0 c 0.0,0.0 2.97300720215,2.97300720215 2.97300720215,2.97300720215 c -0.300003051758,0.360992431641 -0.616012573242,0.711990356445 -0.952011108398,1.0479888916 c -3.21099853516,3.21099853516 -7.47999572754,4.97900390625 -12.0209960938,4.97900390625 c -4.54100036621,0.0 -8.80999755859,-1.76800537109 -12.0209960938,-4.97900390625 c -3.31401062012,-3.31399536133 -4.97100830078,-7.66799926758 -4.97100830078,-12.0209960938 c 0.0,0.0 -2.00799560547,0.0 -2.00799560547,0.0 c 0.0,4.86199951172 1.85499572754,9.72500610352 5.56500244141,13.4349975586 c 3.70999145508,3.71000671387 8.57299804688,5.56500244141 13.4349975586,5.56500244141 c 4.8630065918,0.0 9.72500610352,-1.85499572754 13.4349975586,-5.56500244141 c 0.337005615234,-0.336990356445 0.652008056641,-0.688003540039 0.956008911133,-1.04400634766 c 0.0,0.0 2.60899353027,2.60900878906 2.60899353027,2.60900878906 c 0.0,0.0 0.0,-7.0 0.0,-7.0 Z"
+ android:fillColor="#FFFFFFFF"/>
+ </group>
+ </group>
+ </group>
+</vector> \ No newline at end of file
diff --git a/java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml b/java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml
new file mode 100644
index 000000000..14cda1ba8
--- /dev/null
+++ b/java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<animated-vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:drawable="@drawable/front_back_switch_button">
+ <target
+ android:name="layer_10_outlines"
+ android:animation="@anim/front_back_switch_button_animation"/>
+</animated-vector> \ No newline at end of file
diff --git a/java/com/android/dialer/theme/res/values/colors.xml b/java/com/android/dialer/theme/res/values/colors.xml
new file mode 100644
index 000000000..bf43e01af
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/colors.xml
@@ -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
+ -->
+
+<resources>
+ <!-- Note: The following colors are used in the Dialer settings screens. Since Dialer's settings
+ link into the Telephony settings as well, changes to these colors should be mirrored in
+ Telephony:
+
+ Android source path: packages/apps/PhoneCommon/res/values/colors.xml
+ - Local: dialer_theme_color Android Source: dialer_theme_color
+ - Local: dialer_theme_color_dark Android Source: dialer_theme_color_dark
+ Android source path: packages/services/Telecomm/res/values/colors.xml
+ - Local: dialer_theme_color Android Source: theme_color
+ - Local: dialer_theme_color_dark Android Source: dialer_settings_color_dark
+ -->
+ <color name="dialer_theme_color">#2A56C6</color>
+ <color name="dialer_theme_color_dark">#1C3AA9</color>
+
+ <color name="dialer_snackbar_action_text_color">#4285F4</color>
+ <color name="dialer_theme_color_20pct">#332A56C6</color>
+
+ <color name="dialer_secondary_color">#e91e63</color>
+
+ <!-- Primary text color in the Phone app -->
+ <color name="dialer_primary_text_color">#333333</color>
+ <color name="dialer_edit_text_hint_color">#DE78909C</color>
+
+ <!-- Secondary text color in the Phone app -->
+ <color name="dialer_secondary_text_color">#636363</color>
+
+ <!-- Color of the theme of the Dialer app -->
+ <color name="dialtacts_theme_color">@color/dialer_theme_color</color>
+
+ <!-- White background for dialer -->
+ <color name="background_dialer_white">#ffffff</color>
+ <color name="background_dialer_call_log_list_item">@color/background_dialer_white</color>
+
+ <!-- Colors for the notification actions -->
+ <color name="notification_action_accept">#097138</color>
+ <color name="notification_action_dismiss">#A52714</color>
+ <color name="notification_action_end_call">#FFFFFF</color>
+ <color name="notification_action_answer_video">#097138</color>
+
+ <!-- Background color of action bars -->
+ <color name="actionbar_background_color">@color/dialer_theme_color</color>
+
+ <!-- Background color of title bars in recents -->
+ <color name="titlebar_in_recents_background_color">@color/dialer_theme_color_dark</color>
+
+ <color name="blue_grey_100">#CFD8DC</color>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/dimens.xml b/java/com/android/dialer/theme/res/values/dimens.xml
new file mode 100644
index 000000000..2d11ecc84
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/dimens.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="call_log_action_icon_margin_start">16dp</dimen>
+ <dimen name="call_log_action_icon_dimen">24dp</dimen>
+ <dimen name="call_log_action_horizontal_padding">24dp</dimen>
+
+ <dimen name="call_log_actions_left_padding">64dp</dimen>
+ <dimen name="call_log_actions_top_padding">8dp</dimen>
+ <dimen name="call_log_actions_bottom_padding">8dp</dimen>
+ <dimen name="call_log_primary_text_size">16sp</dimen>
+ <dimen name="call_log_detail_text_size">12sp</dimen>
+ <dimen name="call_log_day_group_heading_size">14sp</dimen>
+ <dimen name="call_log_voicemail_transcription_text_size">14sp</dimen>
+ <!-- Height of the call log actions section for each call log entry -->
+ <dimen name="call_log_action_height">48dp</dimen>
+ <dimen name="call_log_day_group_padding_top">15dp</dimen>
+ <dimen name="call_log_day_group_padding_bottom">9dp</dimen>
+
+ <!-- Height of the actionBar - this is 8dps bigger than the platform standard to give more
+ room to the search box-->
+ <dimen name="action_bar_height">56dp</dimen>
+ <dimen name="action_bar_height_large">64dp</dimen>
+ <dimen name="action_bar_elevation">3dp</dimen>
+ <dimen name="tab_height">48dp</dimen>
+ <!-- actionbar height + tab height -->
+ <dimen name="actionbar_and_tab_height">107dp</dimen>
+ <dimen name="actionbar_contentInsetStart">72dp</dimen>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/strings.xml b/java/com/android/dialer/theme/res/values/strings.xml
new file mode 100644
index 000000000..3a954ae14
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/strings.xml
@@ -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
+ -->
+
+<resources>
+ <!-- String used to display calls from unknown numbers in the call log -->
+ <string name="unknown">Unknown</string>
+
+ <!-- String used to display calls from pay phone in the call log -->
+ <string name="payphone">Payphone</string>
+
+ <!-- Title for the activity that dials the phone. This is the name
+ used in the Launcher icon. -->
+ <string name="launcherActivityLabel">Phone</string>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/styles.xml b/java/com/android/dialer/theme/res/values/styles.xml
new file mode 100644
index 000000000..ac94d0687
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/styles.xml
@@ -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
+ -->
+
+<resources>
+
+ <style name="CallLogCardStyle" parent="CardView">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:layout_margin">4dp</item>
+ <item name="android:baselineAligned">false</item>
+ <item name="cardCornerRadius">2dp</item>
+ <item name="cardBackgroundColor">@color/background_dialer_call_log_list_item</item>
+ </style>
+
+ <!-- Inherit from Theme.Material.Light.Dialog instead of Theme.Material.Light.Dialog.Alert
+ since the Alert dialog is private. They are identical anyway. -->
+ <style name="AlertDialogTheme" parent="@android:style/Theme.Material.Light.Dialog">
+ <item name="android:colorAccent">@color/dialtacts_theme_color</item>
+ </style>
+
+ <style name="TextActionStyle">
+ <item name="android:layout_width">wrap_content</item>
+ <item name="android:layout_height">@dimen/call_log_action_height</item>
+ <item name="android:gravity">end|center_vertical</item>
+ <item name="android:paddingStart">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:paddingEnd">@dimen/call_log_action_horizontal_padding</item>
+ <item name="android:textColor">@color/dialtacts_theme_color</item>
+ <item name="android:fontFamily">"sans-serif-medium"</item>
+ <item name="android:focusable">true</item>
+ <item name="android:singleLine">true</item>
+ <item name="android:textAllCaps">true</item>
+ </style>
+
+ <style name="DialerButtonTextStyle" parent="@android:style/TextAppearance.Material.Widget.Button">
+ <item name="android:textColor">#fff</item>
+ </style>
+
+ <style name="DialerActionBarBaseStyle"
+ parent="@style/Widget.AppCompat.Light.ActionBar.Solid.Inverse">
+ <item name="android:background">@color/actionbar_background_color</item>
+ <item name="background">@color/actionbar_background_color</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/theme/res/values/themes.xml b/java/com/android/dialer/theme/res/values/themes.xml
new file mode 100644
index 000000000..452b36929
--- /dev/null
+++ b/java/com/android/dialer/theme/res/values/themes.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="DialerThemeBase" parent="@style/Theme.AppCompat.Light.DarkActionBar">
+ <item name="android:textColorPrimary">@color/dialer_primary_text_color</item>
+ <item name="android:textColorSecondary">@color/dialer_secondary_text_color</item>
+ <!-- This is used for title bar color in recents -->
+ <item name="android:colorPrimary">@color/titlebar_in_recents_background_color</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="android:colorControlActivated">@color/dialer_theme_color</item>
+ <item name="android:colorButtonNormal">@color/dialer_theme_color</item>
+ <item name="android:colorAccent">@color/dialtacts_theme_color</item>
+ <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
+ <item name="android:textAppearanceButton">@style/DialerButtonTextStyle</item>
+
+ <item name="android:actionBarStyle">@style/DialerActionBarBaseStyle</item>
+ <item name="actionBarStyle">@style/DialerActionBarBaseStyle</item>
+ <item name="android:actionBarSize">@dimen/action_bar_height</item>
+ <item name="actionBarSize">@dimen/action_bar_height</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/util/AndroidManifest.xml b/java/com/android/dialer/util/AndroidManifest.xml
new file mode 100644
index 000000000..499df9b4e
--- /dev/null
+++ b/java/com/android/dialer/util/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.util">
+</manifest>
diff --git a/java/com/android/dialer/util/CallUtil.java b/java/com/android/dialer/util/CallUtil.java
new file mode 100644
index 000000000..81a4bb21e
--- /dev/null
+++ b/java/com/android/dialer/util/CallUtil.java
@@ -0,0 +1,135 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import java.util.List;
+
+/** Utilities related to calls that can be used by non system apps. */
+public class CallUtil {
+
+ /** Indicates that the video calling is not available. */
+ public static final int VIDEO_CALLING_DISABLED = 0;
+
+ /** Indicates that video calling is enabled, regardless of presence status. */
+ public static final int VIDEO_CALLING_ENABLED = 1;
+
+ /**
+ * Indicates that video calling is enabled, but the availability of video call affordances is
+ * determined by the presence status associated with contacts.
+ */
+ public static final int VIDEO_CALLING_PRESENCE = 2;
+
+ /** Return Uri with an appropriate scheme, accepting both SIP and usual phone call numbers. */
+ public static Uri getCallUri(String number) {
+ if (PhoneNumberHelper.isUriNumber(number)) {
+ return Uri.fromParts(PhoneAccount.SCHEME_SIP, number, null);
+ }
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null);
+ }
+
+ /** @return Uri that directly dials a user's voicemail inbox. */
+ public static Uri getVoicemailUri() {
+ return Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null);
+ }
+
+ /**
+ * Determines if video calling is available, and if so whether presence checking is available as
+ * well.
+ *
+ * <p>Returns a bitmask with {@link #VIDEO_CALLING_ENABLED} to indicate that video calling is
+ * available, and {@link #VIDEO_CALLING_PRESENCE} if presence indication is also available.
+ *
+ * @param context The context
+ * @return A bit-mask describing the current video capabilities.
+ */
+ public static int getVideoCallingAvailability(Context context) {
+ if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)
+ || !CompatUtils.isVideoCompatible()) {
+ return VIDEO_CALLING_DISABLED;
+ }
+ TelecomManager telecommMgr = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ if (telecommMgr == null) {
+ return VIDEO_CALLING_DISABLED;
+ }
+
+ List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle accountHandle : accountHandles) {
+ PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+ if (account != null) {
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING)) {
+ // Builds prior to N do not have presence support.
+ if (!CompatUtils.isVideoPresenceCompatible()) {
+ return VIDEO_CALLING_ENABLED;
+ }
+
+ int videoCapabilities = VIDEO_CALLING_ENABLED;
+ if (account.hasCapabilities(PhoneAccount.CAPABILITY_VIDEO_CALLING_RELIES_ON_PRESENCE)) {
+ videoCapabilities |= VIDEO_CALLING_PRESENCE;
+ }
+ return videoCapabilities;
+ }
+ }
+ }
+ return VIDEO_CALLING_DISABLED;
+ }
+
+ /**
+ * Determines if one of the call capable phone accounts defined supports video calling.
+ *
+ * @param context The context.
+ * @return {@code true} if one of the call capable phone accounts supports video calling, {@code
+ * false} otherwise.
+ */
+ public static boolean isVideoEnabled(Context context) {
+ return (getVideoCallingAvailability(context) & VIDEO_CALLING_ENABLED) != 0;
+ }
+
+ /**
+ * Determines if one of the call capable phone accounts defined supports calling with a subject
+ * specified.
+ *
+ * @param context The context.
+ * @return {@code true} if one of the call capable phone accounts supports calling with a subject
+ * specified, {@code false} otherwise.
+ */
+ public static boolean isCallWithSubjectSupported(Context context) {
+ if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)
+ || !CompatUtils.isCallSubjectCompatible()) {
+ return false;
+ }
+ TelecomManager telecommMgr = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ if (telecommMgr == null) {
+ return false;
+ }
+
+ List<PhoneAccountHandle> accountHandles = telecommMgr.getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle accountHandle : accountHandles) {
+ PhoneAccount account = telecommMgr.getPhoneAccount(accountHandle);
+ if (account != null && account.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/dialer/util/DialerUtils.java b/java/com/android/dialer/util/DialerUtils.java
new file mode 100644
index 000000000..63f870e73
--- /dev/null
+++ b/java/com/android/dialer/util/DialerUtils.java
@@ -0,0 +1,246 @@
+/*
+ * 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.util;
+
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Point;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.telecom.TelecomManager;
+import android.telephony.TelephonyManager;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import java.io.File;
+import java.util.Iterator;
+import java.util.Random;
+
+/** General purpose utility methods for the Dialer. */
+public class DialerUtils {
+
+ /**
+ * Prefix on a dialed number that indicates that the call should be placed through the Wireless
+ * Priority Service.
+ */
+ private static final String WPS_PREFIX = "*272";
+
+ public static final String FILE_PROVIDER_CACHE_DIR = "my_cache";
+
+ private static final Random RANDOM = new Random();
+
+ /**
+ * Attempts to start an activity and displays a toast with the default error message if the
+ * activity is not found, instead of throwing an exception.
+ *
+ * @param context to start the activity with.
+ * @param intent to start the activity with.
+ */
+ public static void startActivityWithErrorToast(Context context, Intent intent) {
+ startActivityWithErrorToast(context, intent, R.string.activity_not_available);
+ }
+
+ /**
+ * Attempts to start an activity and displays a toast with a provided error message if the
+ * activity is not found, instead of throwing an exception.
+ *
+ * @param context to start the activity with.
+ * @param intent to start the activity with.
+ * @param msgId Resource ID of the string to display in an error message if the activity is not
+ * found.
+ */
+ public static void startActivityWithErrorToast(
+ final Context context, final Intent intent, int msgId) {
+ try {
+ if ((Intent.ACTION_CALL.equals(intent.getAction()))) {
+ // All dialer-initiated calls should pass the touch point to the InCallUI
+ Point touchPoint = TouchPointManager.getInstance().getPoint();
+ if (touchPoint.x != 0 || touchPoint.y != 0) {
+ Bundle extras;
+ // Make sure to not accidentally clobber any existing extras
+ if (intent.hasExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS)) {
+ extras = intent.getParcelableExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+ } else {
+ extras = new Bundle();
+ }
+ extras.putParcelable(TouchPointManager.TOUCH_POINT, touchPoint);
+ intent.putExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extras);
+ }
+
+ if (shouldWarnForOutgoingWps(context, intent.getData().getSchemeSpecificPart())) {
+ LogUtil.i(
+ "DialUtils.startActivityWithErrorToast",
+ "showing outgoing WPS dialog before placing call");
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setMessage(R.string.outgoing_wps_warning);
+ builder.setPositiveButton(
+ R.string.dialog_continue,
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ placeCallOrMakeToast(context, intent);
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.create().show();
+ } else {
+ placeCallOrMakeToast(context, intent);
+ }
+ } else {
+ context.startActivity(intent);
+ }
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, msgId, Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private static void placeCallOrMakeToast(Context context, Intent intent) {
+ final boolean hasCallPermission = TelecomUtil.placeCall(context, intent);
+ if (!hasCallPermission) {
+ // TODO: Make calling activity show request permission dialog and handle
+ // callback results appropriately.
+ Toast.makeText(context, "Cannot place call without Phone permission", Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ /**
+ * Returns whether the user should be warned about an outgoing WPS call. This checks if there is a
+ * currently active call over LTE. Regardless of the country or carrier, the radio will drop an
+ * active LTE call if a WPS number is dialed, so this warning is necessary.
+ */
+ private static boolean shouldWarnForOutgoingWps(Context context, String number) {
+ if (number != null && number.startsWith(WPS_PREFIX)) {
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ boolean isOnVolte =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ && telephonyManager.getVoiceNetworkType() == TelephonyManager.NETWORK_TYPE_LTE;
+ boolean hasCurrentActiveCall =
+ telephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK;
+ return isOnVolte && hasCurrentActiveCall;
+ }
+ return false;
+ }
+
+ /**
+ * Closes an {@link AutoCloseable}, silently ignoring any checked exceptions. Does nothing if
+ * null.
+ *
+ * @param closeable to close.
+ */
+ public static void closeQuietly(AutoCloseable closeable) {
+ if (closeable != null) {
+ try {
+ closeable.close();
+ } catch (RuntimeException rethrown) {
+ throw rethrown;
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ /**
+ * Joins a list of {@link CharSequence} into a single {@link CharSequence} seperated by ", ".
+ *
+ * @param list List of char sequences to join.
+ * @return Joined char sequences.
+ */
+ public static CharSequence join(Iterable<CharSequence> list) {
+ StringBuilder sb = new StringBuilder();
+ final BidiFormatter formatter = BidiFormatter.getInstance();
+ final CharSequence separator = ", ";
+
+ Iterator<CharSequence> itr = list.iterator();
+ boolean firstTime = true;
+ while (itr.hasNext()) {
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ sb.append(separator);
+ }
+ // Unicode wrap the elements of the list to respect RTL for individual strings.
+ sb.append(
+ formatter.unicodeWrap(itr.next().toString(), TextDirectionHeuristics.FIRSTSTRONG_LTR));
+ }
+
+ // Unicode wrap the joined value, to respect locale's RTL ordering for the whole list.
+ return formatter.unicodeWrap(sb.toString());
+ }
+
+ public static void showInputMethod(View view) {
+ final InputMethodManager imm =
+ (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.showSoftInput(view, 0);
+ }
+ }
+
+ public static void hideInputMethod(View view) {
+ final InputMethodManager imm =
+ (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * Create a File in the cache directory that Dialer's FileProvider knows about so they can be
+ * shared to other apps.
+ */
+ public static File createShareableFile(Context context) {
+ long fileId = Math.abs(RANDOM.nextLong());
+ File parentDir = new File(context.getCacheDir(), FILE_PROVIDER_CACHE_DIR);
+ if (!parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ return new File(parentDir, String.valueOf(fileId));
+ }
+
+ /**
+ * Returns default preference for context accessing device protected storage. This is used when
+ * directBoot is enabled (before device unlocked after boot) since the default shared preference
+ * used normally is not available at this moment for N devices. Returns regular default shared
+ * preference for pre-N devices.
+ */
+ @NonNull
+ public static SharedPreferences getDefaultSharedPreferenceForDeviceProtectedStorageContext(
+ @NonNull Context context) {
+ Assert.isNotNull(context);
+ Context deviceProtectedContext =
+ ContextCompat.isDeviceProtectedStorage(context)
+ ? context
+ : ContextCompat.createDeviceProtectedStorageContext(context);
+ // ContextCompat.createDeviceProtectedStorageContext(context) returns null on pre-N, thus fall
+ // back to regular default shared preference for pre-N devices since devices protected context
+ // is not available.
+ return PreferenceManager.getDefaultSharedPreferences(
+ deviceProtectedContext != null ? deviceProtectedContext : context);
+ }
+}
diff --git a/java/com/android/dialer/util/DrawableConverter.java b/java/com/android/dialer/util/DrawableConverter.java
new file mode 100644
index 000000000..5670315c9
--- /dev/null
+++ b/java/com/android/dialer/util/DrawableConverter.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.util;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawable;
+import android.support.v4.graphics.drawable.RoundedBitmapDrawableFactory;
+import com.android.dialer.common.LogUtil;
+
+/** Provides utilities for bitmaps and drawables. */
+public class DrawableConverter {
+
+ private DrawableConverter() {}
+
+ /** Converts the provided drawable to a bitmap using the drawable's intrinsic width and height. */
+ @Nullable
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable) {
+ return drawableToBitmap(drawable, 0, 0);
+ }
+
+ /**
+ * Converts the provided drawable to a bitmap with the specified width and height.
+ *
+ * <p>If both width and height are 0, the drawable's intrinsic width and height are used (but in
+ * that case {@link #drawableToBitmap(Drawable)} should be used).
+ */
+ @Nullable
+ public static Bitmap drawableToBitmap(@Nullable Drawable drawable, int width, int height) {
+ if (drawable == null) {
+ return null;
+ }
+
+ Bitmap bitmap;
+ if (drawable instanceof BitmapDrawable) {
+ bitmap = ((BitmapDrawable) drawable).getBitmap();
+ } else {
+ if (width > 0 || height > 0) {
+ bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ } else if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+ // Needed for drawables that are just a colour.
+ bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
+ } else {
+ bitmap =
+ Bitmap.createBitmap(
+ drawable.getIntrinsicWidth(),
+ drawable.getIntrinsicHeight(),
+ Bitmap.Config.ARGB_8888);
+ }
+
+ LogUtil.i(
+ "DrawableConverter.drawableToBitmap",
+ "created bitmap with width: %d, height: %d",
+ bitmap.getWidth(),
+ bitmap.getHeight());
+
+ Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ }
+ return bitmap;
+ }
+
+ @Nullable
+ public static Drawable getRoundedDrawable(
+ @NonNull Context context, @Nullable Drawable photo, int width, int height) {
+ Bitmap bitmap = drawableToBitmap(photo);
+ if (bitmap != null) {
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, false);
+ RoundedBitmapDrawable drawable =
+ RoundedBitmapDrawableFactory.create(context.getResources(), scaledBitmap);
+ drawable.setAntiAlias(true);
+ drawable.setCornerRadius(drawable.getIntrinsicHeight() / 2);
+ return drawable;
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/util/ExpirableCache.java b/java/com/android/dialer/util/ExpirableCache.java
new file mode 100644
index 000000000..2778a572c
--- /dev/null
+++ b/java/com/android/dialer/util/ExpirableCache.java
@@ -0,0 +1,269 @@
+/*
+ * 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.util;
+
+import android.util.LruCache;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.annotation.concurrent.Immutable;
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * An LRU cache in which all items can be marked as expired at a given time and it is possible to
+ * query whether a particular cached value is expired or not.
+ *
+ * <p>A typical use case for this is caching of values which are expensive to compute but which are
+ * still useful when out of date.
+ *
+ * <p>Consider a cache for contact information:
+ *
+ * <pre>{@code
+ * private ExpirableCache<String, Contact> mContactCache;
+ * }</pre>
+ *
+ * which stores the contact information for a given phone number.
+ *
+ * <p>When we need to store contact information for a given phone number, we can look up the info in
+ * the cache:
+ *
+ * <pre>{@code
+ * CachedValue<Contact> cachedContact = mContactCache.getCachedValue(phoneNumber);
+ * }</pre>
+ *
+ * We might also want to fetch the contact information again if the item is expired.
+ *
+ * <pre>
+ * if (cachedContact.isExpired()) {
+ * fetchContactForNumber(phoneNumber,
+ * new FetchListener() {
+ * &#64;Override
+ * public void onFetched(Contact contact) {
+ * mContactCache.put(phoneNumber, contact);
+ * }
+ * });
+ * }</pre>
+ *
+ * and insert it back into the cache when the fetch completes.
+ *
+ * <p>At a certain point we want to expire the content of the cache because we know the content may
+ * no longer be up-to-date, for instance, when resuming the activity this is shown into:
+ *
+ * <pre>
+ * &#64;Override
+ * protected onResume() {
+ * // We were paused for some time, the cached value might no longer be up to date.
+ * mContactCache.expireAll();
+ * super.onResume();
+ * }
+ * </pre>
+ *
+ * The values will be still available from the cache, but they will be expired.
+ *
+ * <p>If interested only in the value itself, not whether it is expired or not, one should use the
+ * {@link #getPossiblyExpired(Object)} method. If interested only in non-expired values, one should
+ * use the {@link #get(Object)} method instead.
+ *
+ * <p>This class wraps around an {@link LruCache} instance: it follows the {@link LruCache} behavior
+ * for evicting items when the cache is full. It is possible to supply your own subclass of LruCache
+ * by using the {@link #create(LruCache)} method, which can define a custom expiration policy. Since
+ * the underlying cache maps keys to cached values it can determine which items are expired and
+ * which are not, allowing for an implementation that evicts expired items before non expired ones.
+ *
+ * <p>This class is thread-safe.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ */
+@ThreadSafe
+public class ExpirableCache<K, V> {
+
+ /**
+ * The current generation of items added to the cache.
+ *
+ * <p>Items in the cache can belong to a previous generation, but in that case they would be
+ * expired.
+ *
+ * @see ExpirableCache.CachedValue#isExpired()
+ */
+ private final AtomicInteger mGeneration;
+ /** The underlying cache used to stored the cached values. */
+ private LruCache<K, CachedValue<V>> mCache;
+
+ private ExpirableCache(LruCache<K, CachedValue<V>> cache) {
+ mCache = cache;
+ mGeneration = new AtomicInteger(0);
+ }
+
+ /**
+ * Creates a new {@link ExpirableCache} that wraps the given {@link LruCache}.
+ *
+ * <p>The created cache takes ownership of the cache passed in as an argument.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ * @param cache the cache to store the value in
+ * @return the newly created expirable cache
+ * @throws IllegalArgumentException if the cache is not empty
+ */
+ public static <K, V> ExpirableCache<K, V> create(LruCache<K, CachedValue<V>> cache) {
+ return new ExpirableCache<K, V>(cache);
+ }
+
+ /**
+ * Creates a new {@link ExpirableCache} with the given maximum size.
+ *
+ * @param <K> the type of the keys
+ * @param <V> the type of the values
+ * @return the newly created expirable cache
+ */
+ public static <K, V> ExpirableCache<K, V> create(int maxSize) {
+ return create(new LruCache<K, CachedValue<V>>(maxSize));
+ }
+
+ /**
+ * Returns the cached value for the given key, or null if no value exists.
+ *
+ * <p>The cached value gives access both to the value associated with the key and whether it is
+ * expired or not.
+ *
+ * <p>If not interested in whether the value is expired, use {@link #getPossiblyExpired(Object)}
+ * instead.
+ *
+ * <p>If only wants values that are not expired, use {@link #get(Object)} instead.
+ *
+ * @param key the key to look up
+ */
+ public CachedValue<V> getCachedValue(K key) {
+ return mCache.get(key);
+ }
+
+ /**
+ * Returns the value for the given key, or null if no value exists.
+ *
+ * <p>When using this method, it is not possible to determine whether the value is expired or not.
+ * Use {@link #getCachedValue(Object)} to achieve that instead. However, if using {@link
+ * #getCachedValue(Object)} to determine if an item is expired, one should use the item within the
+ * {@link CachedValue} and not call {@link #getPossiblyExpired(Object)} to get the value
+ * afterwards, since that is not guaranteed to return the same value or that the newly returned
+ * value is in the same state.
+ *
+ * @param key the key to look up
+ */
+ public V getPossiblyExpired(K key) {
+ CachedValue<V> cachedValue = getCachedValue(key);
+ return cachedValue == null ? null : cachedValue.getValue();
+ }
+
+ /**
+ * Returns the value for the given key only if it is not expired, or null if no value exists or is
+ * expired.
+ *
+ * <p>This method will return null if either there is no value associated with this key or if the
+ * associated value is expired.
+ *
+ * @param key the key to look up
+ */
+ public V get(K key) {
+ CachedValue<V> cachedValue = getCachedValue(key);
+ return cachedValue == null || cachedValue.isExpired() ? null : cachedValue.getValue();
+ }
+
+ /**
+ * Puts an item in the cache.
+ *
+ * <p>Newly added item will not be expired until {@link #expireAll()} is next called.
+ *
+ * @param key the key to look up
+ * @param value the value to associate with the key
+ */
+ public void put(K key, V value) {
+ mCache.put(key, newCachedValue(value));
+ }
+
+ /**
+ * Mark all items currently in the cache as expired.
+ *
+ * <p>Newly added items after this call will be marked as not expired.
+ *
+ * <p>Expiring the items in the cache does not imply they will be evicted.
+ */
+ public void expireAll() {
+ mGeneration.incrementAndGet();
+ }
+
+ /**
+ * Creates a new {@link CachedValue} instance to be stored in this cache.
+ *
+ * <p>Implementation of {@link LruCache#create(K)} can use this method to create a new entry.
+ */
+ public CachedValue<V> newCachedValue(V value) {
+ return new GenerationalCachedValue<V>(value, mGeneration);
+ }
+
+ /**
+ * A cached value stored inside the cache.
+ *
+ * <p>It provides access to the value stored in the cache but also allows to check whether the
+ * value is expired.
+ *
+ * @param <V> the type of value stored in the cache
+ */
+ public interface CachedValue<V> {
+
+ /** Returns the value stored in the cache for a given key. */
+ V getValue();
+
+ /**
+ * Checks whether the value, while still being present in the cache, is expired.
+ *
+ * @return true if the value is expired
+ */
+ boolean isExpired();
+ }
+
+ /** Cached values storing the generation at which they were added. */
+ @Immutable
+ private static class GenerationalCachedValue<V> implements ExpirableCache.CachedValue<V> {
+
+ /** The value stored in the cache. */
+ public final V mValue;
+ /** The generation at which the value was added to the cache. */
+ private final int mGeneration;
+ /** The atomic integer storing the current generation of the cache it belongs to. */
+ private final AtomicInteger mCacheGeneration;
+
+ /**
+ * @param cacheGeneration the atomic integer storing the generation of the cache in which this
+ * value will be stored
+ */
+ public GenerationalCachedValue(V value, AtomicInteger cacheGeneration) {
+ mValue = value;
+ mCacheGeneration = cacheGeneration;
+ // Snapshot the current generation.
+ mGeneration = mCacheGeneration.get();
+ }
+
+ @Override
+ public V getValue() {
+ return mValue;
+ }
+
+ @Override
+ public boolean isExpired() {
+ return mGeneration != mCacheGeneration.get();
+ }
+ }
+}
diff --git a/java/com/android/dialer/util/IntentUtil.java b/java/com/android/dialer/util/IntentUtil.java
new file mode 100644
index 000000000..2f265b5a7
--- /dev/null
+++ b/java/com/android/dialer/util/IntentUtil.java
@@ -0,0 +1,78 @@
+/*
+ * 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.util;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+/** Utilities for creation of intents in Dialer. */
+public class IntentUtil {
+
+ private static final String SMS_URI_PREFIX = "sms:";
+ private static final int NO_PHONE_TYPE = -1;
+
+ public static Intent getSendSmsIntent(CharSequence phoneNumber) {
+ return new Intent(Intent.ACTION_SENDTO, Uri.parse(SMS_URI_PREFIX + phoneNumber));
+ }
+
+ public static Intent getNewContactIntent() {
+ return new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
+ }
+
+ public static Intent getNewContactIntent(CharSequence phoneNumber) {
+ return getNewContactIntent(null /* name */, phoneNumber /* phoneNumber */, NO_PHONE_TYPE);
+ }
+
+ public static Intent getNewContactIntent(
+ CharSequence name, CharSequence phoneNumber, int phoneNumberType) {
+ Intent intent = getNewContactIntent();
+ populateContactIntent(intent, name, phoneNumber, phoneNumberType);
+ return intent;
+ }
+
+ public static Intent getAddToExistingContactIntent() {
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ intent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
+ return intent;
+ }
+
+ public static Intent getAddToExistingContactIntent(CharSequence phoneNumber) {
+ return getAddToExistingContactIntent(
+ null /* name */, phoneNumber /* phoneNumber */, NO_PHONE_TYPE);
+ }
+
+ public static Intent getAddToExistingContactIntent(
+ CharSequence name, CharSequence phoneNumber, int phoneNumberType) {
+ Intent intent = getAddToExistingContactIntent();
+ populateContactIntent(intent, name, phoneNumber, phoneNumberType);
+ return intent;
+ }
+
+ private static void populateContactIntent(
+ Intent intent, CharSequence name, CharSequence phoneNumber, int phoneNumberType) {
+ if (phoneNumber != null) {
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE, phoneNumber);
+ }
+ if (name != null) {
+ intent.putExtra(ContactsContract.Intents.Insert.NAME, name);
+ }
+ if (phoneNumberType != NO_PHONE_TYPE) {
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE_TYPE, phoneNumberType);
+ }
+ }
+}
diff --git a/java/com/android/dialer/util/MoreStrings.java b/java/com/android/dialer/util/MoreStrings.java
new file mode 100644
index 000000000..5a43b1d10
--- /dev/null
+++ b/java/com/android/dialer/util/MoreStrings.java
@@ -0,0 +1,64 @@
+/*
+ * 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.util;
+
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+
+/** Static utility methods for Strings. */
+public class MoreStrings {
+
+ /**
+ * Returns the given string if it is non-null; the empty string otherwise.
+ *
+ * @param string the string to test and possibly return
+ * @return {@code string} itself if it is non-null; {@code ""} if it is null
+ */
+ public static String nullToEmpty(@Nullable String string) {
+ return (string == null) ? "" : string;
+ }
+
+ /**
+ * Returns the given string if it is nonempty; {@code null} otherwise.
+ *
+ * @param string the string to test and possibly return
+ * @return {@code string} itself if it is nonempty; {@code null} if it is empty or null
+ */
+ @Nullable
+ public static String emptyToNull(@Nullable String string) {
+ return TextUtils.isEmpty(string) ? null : string;
+ }
+
+ public static String toSafeString(String value) {
+ if (value == null) {
+ return null;
+ }
+
+ // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+ // sanitized phone numbers.
+ final StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < value.length(); i++) {
+ final char c = value.charAt(i);
+ if (c == '-' || c == '@' || c == '.') {
+ builder.append(c);
+ } else {
+ builder.append('x');
+ }
+ }
+ return builder.toString();
+ }
+}
diff --git a/java/com/android/dialer/util/OrientationUtil.java b/java/com/android/dialer/util/OrientationUtil.java
new file mode 100644
index 000000000..5a8d1ae0f
--- /dev/null
+++ b/java/com/android/dialer/util/OrientationUtil.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.dialer.util;
+
+import android.content.Context;
+import android.content.res.Configuration;
+
+/** Static methods related to device orientation. */
+public class OrientationUtil {
+
+ /** @return if the context is in landscape orientation. */
+ public static boolean isLandscape(Context context) {
+ return context.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+}
diff --git a/java/com/android/dialer/util/PermissionsUtil.java b/java/com/android/dialer/util/PermissionsUtil.java
new file mode 100644
index 000000000..70b96dfe1
--- /dev/null
+++ b/java/com/android/dialer/util/PermissionsUtil.java
@@ -0,0 +1,121 @@
+/*
+ * 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.util;
+
+import android.Manifest.permission;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.LocalBroadcastManager;
+import com.android.dialer.common.LogUtil;
+
+/** Utility class to help with runtime permissions. */
+public class PermissionsUtil {
+
+ private static final String PERMISSION_PREFERENCE = "dialer_permissions";
+
+ public static boolean hasPhonePermissions(Context context) {
+ return hasPermission(context, permission.CALL_PHONE);
+ }
+
+ public static boolean hasContactsPermissions(Context context) {
+ return hasPermission(context, permission.READ_CONTACTS);
+ }
+
+ public static boolean hasLocationPermissions(Context context) {
+ return hasPermission(context, permission.ACCESS_FINE_LOCATION);
+ }
+
+ public static boolean hasCameraPermissions(Context context) {
+ return hasPermission(context, permission.CAMERA);
+ }
+
+ public static boolean hasPermission(Context context, String permission) {
+ return ContextCompat.checkSelfPermission(context, permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ /**
+ * Checks {@link android.content.SharedPreferences} if a permission has been requested before.
+ *
+ * <p>It is important to note that this method only works if you call {@link
+ * PermissionsUtil#permissionRequested(Context, String)} in {@link
+ * android.app.Activity#onRequestPermissionsResult(int, String[], int[])}.
+ */
+ public static boolean isFirstRequest(Context context, String permission) {
+ return context
+ .getSharedPreferences(PERMISSION_PREFERENCE, Context.MODE_PRIVATE)
+ .getBoolean(permission, true);
+ }
+
+ /**
+ * Records in {@link android.content.SharedPreferences} that the specified permission has been
+ * requested at least once.
+ *
+ * <p>This method should be called in {@link android.app.Activity#onRequestPermissionsResult(int,
+ * String[], int[])}.
+ */
+ public static void permissionRequested(Context context, String permission) {
+ context
+ .getSharedPreferences(PERMISSION_PREFERENCE, Context.MODE_PRIVATE)
+ .edit()
+ .putBoolean(permission, false)
+ .apply();
+ }
+
+ /**
+ * Rudimentary methods wrapping the use of a LocalBroadcastManager to simplify the process of
+ * notifying other classes when a particular fragment is notified that a permission is granted.
+ *
+ * <p>To be notified when a permission has been granted, create a new broadcast receiver and
+ * register it using {@link #registerPermissionReceiver(Context, BroadcastReceiver, String)}
+ *
+ * <p>E.g.
+ *
+ * <p>final BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void
+ * onReceive(Context context, Intent intent) { refreshContactsView(); } }
+ *
+ * <p>PermissionsUtil.registerPermissionReceiver(getActivity(), receiver, READ_CONTACTS);
+ *
+ * <p>If you register to listen for multiple permissions, you can identify which permission was
+ * granted by inspecting {@link Intent#getAction()}.
+ *
+ * <p>In the fragment that requests for the permission, be sure to call {@link
+ * #notifyPermissionGranted(Context, String)} when the permission is granted so that any
+ * interested listeners are notified of the change.
+ */
+ public static void registerPermissionReceiver(
+ Context context, BroadcastReceiver receiver, String permission) {
+ LogUtil.i("PermissionsUtil.registerPermissionReceiver", permission);
+ final IntentFilter filter = new IntentFilter(permission);
+ LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
+ }
+
+ public static void unregisterPermissionReceiver(Context context, BroadcastReceiver receiver) {
+ LogUtil.i("PermissionsUtil.unregisterPermissionReceiver", null);
+ LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
+ }
+
+ public static void notifyPermissionGranted(Context context, String permission) {
+ LogUtil.i("PermissionsUtil.notifyPermissionGranted", permission);
+ final Intent intent = new Intent(permission);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+}
diff --git a/java/com/android/dialer/util/SettingsUtil.java b/java/com/android/dialer/util/SettingsUtil.java
new file mode 100644
index 000000000..c61c09b6c
--- /dev/null
+++ b/java/com/android/dialer/util/SettingsUtil.java
@@ -0,0 +1,95 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.sqlite.SQLiteException;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+public class SettingsUtil {
+
+ private static final String DEFAULT_NOTIFICATION_URI_STRING =
+ Settings.System.DEFAULT_NOTIFICATION_URI.toString();
+
+ /**
+ * Queries for a ringtone name, and sets the name using a handler. This is a method was originally
+ * copied from com.android.settings.SoundSettings.
+ *
+ * @param context The application context.
+ * @param handler The handler, which takes the name of the ringtone as a String as a parameter.
+ * @param type The type of sound.
+ * @param key The key to the shared preferences entry being updated.
+ * @param msg An integer identifying the message sent to the handler.
+ */
+ public static void updateRingtoneName(
+ Context context, Handler handler, int type, String key, int msg) {
+ final Uri ringtoneUri;
+ boolean defaultRingtone = false;
+ if (type == RingtoneManager.TYPE_RINGTONE) {
+ // For ringtones, we can just lookup the system default because changing the settings
+ // in Call Settings changes the system default.
+ ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ } else {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ // For voicemail notifications, we use the value saved in Phone's shared preferences.
+ String uriString = prefs.getString(key, DEFAULT_NOTIFICATION_URI_STRING);
+ if (TextUtils.isEmpty(uriString)) {
+ // silent ringtone
+ ringtoneUri = null;
+ } else {
+ if (uriString.equals(DEFAULT_NOTIFICATION_URI_STRING)) {
+ // If it turns out that the voicemail notification is set to the system
+ // default notification, we retrieve the actual URI to prevent it from showing
+ // up as "Unknown Ringtone".
+ defaultRingtone = true;
+ ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ } else {
+ ringtoneUri = Uri.parse(uriString);
+ }
+ }
+ }
+ CharSequence summary = context.getString(R.string.ringtone_unknown);
+ // Is it a silent ringtone?
+ if (ringtoneUri == null) {
+ summary = context.getString(R.string.ringtone_silent);
+ } else {
+ // Fetch the ringtone title from the media provider
+ final Ringtone ringtone = RingtoneManager.getRingtone(context, ringtoneUri);
+ if (ringtone != null) {
+ try {
+ final String title = ringtone.getTitle(context);
+ if (!TextUtils.isEmpty(title)) {
+ summary = title;
+ }
+ } catch (SQLiteException sqle) {
+ // Unknown title for the ringtone
+ }
+ }
+ }
+ if (defaultRingtone) {
+ summary = context.getString(R.string.default_notification_description, summary);
+ }
+ handler.sendMessage(handler.obtainMessage(msg, summary));
+ }
+}
diff --git a/java/com/android/dialer/util/TouchPointManager.java b/java/com/android/dialer/util/TouchPointManager.java
new file mode 100644
index 000000000..74f87c477
--- /dev/null
+++ b/java/com/android/dialer/util/TouchPointManager.java
@@ -0,0 +1,60 @@
+/*
+ * 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.util;
+
+import android.graphics.Point;
+
+/**
+ * Singleton class to keep track of where the user last touched the screen.
+ *
+ * <p>Used to pass on to the InCallUI for animation.
+ */
+public class TouchPointManager {
+
+ public static final String TOUCH_POINT = "touchPoint";
+
+ private static TouchPointManager sInstance = new TouchPointManager();
+
+ private Point mPoint = new Point();
+
+ /** Private constructor. Instance should only be acquired through getInstance(). */
+ private TouchPointManager() {}
+
+ public static TouchPointManager getInstance() {
+ return sInstance;
+ }
+
+ public Point getPoint() {
+ return mPoint;
+ }
+
+ public void setPoint(int x, int y) {
+ mPoint.set(x, y);
+ }
+
+ /**
+ * When a point is initialized, its value is (0,0). Since it is highly unlikely a user will touch
+ * at that exact point, if the point in TouchPointManager is (0,0), it is safe to assume that the
+ * TouchPointManager has not yet collected a touch.
+ *
+ * @return True if there is a valid point saved. Define a valid point as any point that is not
+ * (0,0).
+ */
+ public boolean hasValidPoint() {
+ return mPoint.x != 0 || mPoint.y != 0;
+ }
+}
diff --git a/java/com/android/dialer/util/TransactionSafeActivity.java b/java/com/android/dialer/util/TransactionSafeActivity.java
new file mode 100644
index 000000000..9b5e92ba8
--- /dev/null
+++ b/java/com/android/dialer/util/TransactionSafeActivity.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.dialer.util;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+/**
+ * A common superclass that keeps track of whether an {@link Activity} has saved its state yet or
+ * not.
+ */
+public abstract class TransactionSafeActivity extends AppCompatActivity {
+
+ private boolean mIsSafeToCommitTransactions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mIsSafeToCommitTransactions = false;
+ }
+
+ /**
+ * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+ * whether {@link Activity#onSaveInstanceState} has been called or not.
+ *
+ * <p>Make sure that the current activity calls into {@link super.onSaveInstanceState(Bundle
+ * outState)} (if that method is overridden), so the flag is properly set.
+ */
+ public boolean isSafeToCommitTransactions() {
+ return mIsSafeToCommitTransactions;
+ }
+}
diff --git a/java/com/android/dialer/util/ViewUtil.java b/java/com/android/dialer/util/ViewUtil.java
new file mode 100644
index 000000000..de08e41a7
--- /dev/null
+++ b/java/com/android/dialer/util/ViewUtil.java
@@ -0,0 +1,129 @@
+/*
+ * 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.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Paint;
+import android.os.PowerManager;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnPreDrawListener;
+import android.widget.TextView;
+import java.util.Locale;
+
+/** Provides static functions to work with views */
+public class ViewUtil {
+
+ private ViewUtil() {}
+
+ /** Similar to {@link Runnable} but takes a View parameter to operate on */
+ public interface ViewRunnable {
+ void run(@NonNull View view);
+ }
+
+ /**
+ * Returns the width as specified in the LayoutParams
+ *
+ * @throws IllegalStateException Thrown if the view's width is unknown before a layout pass s
+ */
+ public static int getConstantPreLayoutWidth(View view) {
+ // We haven't been layed out yet, so get the size from the LayoutParams
+ final ViewGroup.LayoutParams p = view.getLayoutParams();
+ if (p.width < 0) {
+ throw new IllegalStateException(
+ "Expecting view's width to be a constant rather " + "than a result of the layout pass");
+ }
+ return p.width;
+ }
+
+ /**
+ * Returns a boolean indicating whether or not the view's layout direction is RTL
+ *
+ * @param view - A valid view
+ * @return True if the view's layout direction is RTL
+ */
+ public static boolean isViewLayoutRtl(View view) {
+ return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ public static boolean isRtl() {
+ return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
+ }
+
+ public static void resizeText(TextView textView, int originalTextSize, int minTextSize) {
+ final Paint paint = textView.getPaint();
+ final int width = textView.getWidth();
+ if (width == 0) {
+ return;
+ }
+ textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize);
+ float ratio = width / paint.measureText(textView.getText().toString());
+ if (ratio <= 1.0f) {
+ textView.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX, Math.max(minTextSize, originalTextSize * ratio));
+ }
+ }
+
+ /** Runs a piece of code just before the next draw, after layout and measurement */
+ public static void doOnPreDraw(
+ @NonNull final View view, final boolean drawNextFrame, final Runnable runnable) {
+ view.getViewTreeObserver()
+ .addOnPreDrawListener(
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ view.getViewTreeObserver().removeOnPreDrawListener(this);
+ runnable.run();
+ return drawNextFrame;
+ }
+ });
+ }
+
+ public static void doOnPreDraw(
+ @NonNull final View view, final boolean drawNextFrame, final ViewRunnable runnable) {
+ view.getViewTreeObserver()
+ .addOnPreDrawListener(
+ new OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ view.getViewTreeObserver().removeOnPreDrawListener(this);
+ runnable.run(view);
+ return drawNextFrame;
+ }
+ });
+ }
+
+ /**
+ * Returns {@code true} if animations should be disabled.
+ *
+ * <p>Animations should be disabled if {@link
+ * android.provider.Settings.Global#ANIMATOR_DURATION_SCALE} is set to 0 through system settings
+ * or the device is in power save mode.
+ */
+ public static boolean areAnimationsDisabled(Context context) {
+ ContentResolver contentResolver = context.getContentResolver();
+ PowerManager powerManager = context.getSystemService(PowerManager.class);
+ return Settings.Global.getFloat(contentResolver, Global.ANIMATOR_DURATION_SCALE, 1.0f) == 0
+ || powerManager.isPowerSaveMode();
+ }
+}
diff --git a/java/com/android/dialer/util/res/values/strings.xml b/java/com/android/dialer/util/res/values/strings.xml
new file mode 100644
index 000000000..43ea6e31a
--- /dev/null
+++ b/java/com/android/dialer/util/res/values/strings.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- The string used to describe a notification if it is the default one in the system. For
+ example, if the user selects the default notification, it will appear as something like
+ Default sound(Capella) in the notification summary.
+ [CHAR LIMIT=40] -->
+ <string name="default_notification_description">Default sound (<xliff:g id="default_sound_title">%1$s</xliff:g>)</string>
+
+ <!-- Choice in the ringtone picker. If chosen, there will be silence instead of a ringtone played. -->
+ <string name="ringtone_silent">None</string>
+
+ <!-- If there is ever a ringtone set for some setting, but that ringtone can no longer be resolved, this is shown instead. For example, if the ringtone was on a SD card and it had been removed, this would be shown for ringtones on that SD card. -->
+ <string name="ringtone_unknown">Unknown ringtone</string>
+
+ <!-- Message displayed when there is no application available to handle a particular action.
+ [CHAR LIMIT=NONE] -->
+ <string name="activity_not_available">No app for that on this device</string>
+
+ <!-- Text of warning to be shown when the user attempts to make an outgoing Wireless
+ Preferred Service call when there is an VoLTE call in progress -->
+ <string name="outgoing_wps_warning">Placing a WPS call will disconnect your existing call.</string>
+
+ <!-- Text for button which indicates that the user wants to proceed with an action. -->
+ <string name="dialog_continue">Continue</string>
+
+</resources>
diff --git a/java/com/android/dialer/voicemailstatus/AndroidManifest.xml b/java/com/android/dialer/voicemailstatus/AndroidManifest.xml
new file mode 100644
index 000000000..a39894c88
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.voicemailstatus">
+</manifest>
diff --git a/java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java b/java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java
new file mode 100644
index 000000000..142bb63ed
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java
@@ -0,0 +1,111 @@
+/*
+ * 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.voicemailstatus;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import com.android.dialer.database.CallLogQueryHandler;
+
+/**
+ * Helper class to check whether visual voicemail is enabled.
+ *
+ * <p>Call isVisualVoicemailEnabled() to retrieve the result.
+ *
+ * <p>The result is cached and saved in a SharedPreferences, stored as a boolean in
+ * PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER. Every time a new instance is created, it will try to
+ * restore the cached result from the SharedPreferences.
+ *
+ * <p>Call asyncUpdate() to make a CallLogQuery to check the actual status. This is a async call so
+ * isVisualVoicemailEnabled() will not be affected immediately.
+ *
+ * <p>If the status has changed as a result of asyncUpdate(),
+ * Callback.onVisualVoicemailEnabledStatusChanged() will be called with the new value.
+ */
+public class VisualVoicemailEnabledChecker implements CallLogQueryHandler.Listener {
+
+ public static final String PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER =
+ "has_active_voicemail_provider";
+ private SharedPreferences mPrefs;
+ private boolean mHasActiveVoicemailProvider;
+ private CallLogQueryHandler mCallLogQueryHandler;
+ private VoicemailStatusHelper mVoicemailStatusHelper;
+ private Context mContext;
+ private Callback mCallback;
+
+ public VisualVoicemailEnabledChecker(Context context, @Nullable Callback callback) {
+ mContext = context;
+ mCallback = callback;
+ mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
+ mHasActiveVoicemailProvider = mPrefs.getBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, false);
+ }
+
+ /**
+ * @return whether visual voicemail is enabled. Result is cached, call asyncUpdate() to update the
+ * result.
+ */
+ public boolean isVisualVoicemailEnabled() {
+ return mHasActiveVoicemailProvider;
+ }
+
+ /**
+ * Perform an async query into the system to check the status of visual voicemail. If the status
+ * has changed, Callback.onVisualVoicemailEnabledStatusChanged() will be called.
+ */
+ public void asyncUpdate() {
+ mCallLogQueryHandler = new CallLogQueryHandler(mContext, mContext.getContentResolver(), this);
+ mCallLogQueryHandler.fetchVoicemailStatus();
+ }
+
+ @Override
+ public void onVoicemailStatusFetched(Cursor statusCursor) {
+ boolean hasActiveVoicemailProvider =
+ mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) > 0;
+ if (hasActiveVoicemailProvider != mHasActiveVoicemailProvider) {
+ mHasActiveVoicemailProvider = hasActiveVoicemailProvider;
+ mPrefs.edit().putBoolean(PREF_KEY_HAS_ACTIVE_VOICEMAIL_PROVIDER, mHasActiveVoicemailProvider);
+ if (mCallback != null) {
+ mCallback.onVisualVoicemailEnabledStatusChanged(mHasActiveVoicemailProvider);
+ }
+ }
+ }
+
+ @Override
+ public void onVoicemailUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public void onMissedCallsUnreadCountFetched(Cursor cursor) {
+ // Do nothing
+ }
+
+ @Override
+ public boolean onCallsFetched(Cursor combinedCursor) {
+ // Do nothing
+ return false;
+ }
+
+ public interface Callback {
+
+ /** Callback to notify enabled status has changed to the @param newValue */
+ void onVisualVoicemailEnabledStatusChanged(boolean newValue);
+ }
+}
diff --git a/java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java
new file mode 100644
index 000000000..16bfe704d
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java
@@ -0,0 +1,96 @@
+/*
+ * 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.voicemailstatus;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.VisibleForTesting;
+import java.util.List;
+
+/**
+ * Interface used by the call log UI to determine what user message, if any, related to voicemail
+ * source status needs to be shown. The messages are returned in the order of importance.
+ *
+ * <p>The implementation of this interface interacts with the voicemail content provider to fetch
+ * statuses of all the registered voicemail sources and determines if any status message needs to be
+ * shown. The user of this interface must observe/listen to provider changes and invoke this class
+ * to check if any message needs to be shown.
+ */
+public interface VoicemailStatusHelper {
+
+ /**
+ * Returns a list of messages, in the order or priority that should be shown to the user. An empty
+ * list is returned if no message needs to be shown.
+ *
+ * @param cursor The cursor pointing to the query on {@link Status#CONTENT_URI}. The projection to
+ * be used is defined by the implementation class of this interface.
+ */
+ @VisibleForTesting
+ List<StatusMessage> getStatusMessages(Cursor cursor);
+
+ /**
+ * Returns the number of active voicemail sources installed.
+ *
+ * <p>The number of sources is counted by querying the voicemail status table.
+ */
+ int getNumberActivityVoicemailSources(Cursor cursor);
+
+ @VisibleForTesting
+ class StatusMessage {
+
+ /** Package of the source on behalf of which this message has to be shown. */
+ public final String sourcePackage;
+ /**
+ * The string resource id of the status message that should be shown in the call log page. Set
+ * to -1, if this message is not to be shown in call log.
+ */
+ public final int callLogMessageId;
+ /**
+ * The string resource id of the status message that should be shown in the call details page.
+ * Set to -1, if this message is not to be shown in call details page.
+ */
+ public final int callDetailsMessageId;
+ /** The string resource id of the action message that should be shown. */
+ public final int actionMessageId;
+ /** URI for the corrective action, where applicable. Null if no action URI is available. */
+ public final Uri actionUri;
+
+ public StatusMessage(
+ String sourcePackage,
+ int callLogMessageId,
+ int callDetailsMessageId,
+ int actionMessageId,
+ Uri actionUri) {
+ this.sourcePackage = sourcePackage;
+ this.callLogMessageId = callLogMessageId;
+ this.callDetailsMessageId = callDetailsMessageId;
+ this.actionMessageId = actionMessageId;
+ this.actionUri = actionUri;
+ }
+
+ /** Whether this message should be shown in the call log page. */
+ public boolean showInCallLog() {
+ return callLogMessageId != -1;
+ }
+
+ /** Whether this message should be shown in the call details page. */
+ public boolean showInCallDetails() {
+ return callDetailsMessageId != -1;
+ }
+ }
+}
diff --git a/java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java
new file mode 100644
index 000000000..404897fde
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java
@@ -0,0 +1,278 @@
+/*
+ * 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.voicemailstatus;
+
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_CAN_BE_CONFIGURED;
+import static android.provider.VoicemailContract.Status.CONFIGURATION_STATE_OK;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.DATA_CHANNEL_STATE_OK;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION;
+import static android.provider.VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE_OK;
+
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract.Status;
+import com.android.contacts.common.util.UriUtils;
+import com.android.dialer.database.VoicemailStatusQuery;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/** Implementation of {@link VoicemailStatusHelper}. */
+public class VoicemailStatusHelperImpl implements VoicemailStatusHelper {
+
+ @Override
+ public List<StatusMessage> getStatusMessages(Cursor cursor) {
+ List<MessageStatusWithPriority> messages =
+ new ArrayList<VoicemailStatusHelperImpl.MessageStatusWithPriority>();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ MessageStatusWithPriority message = getMessageForStatusEntry(cursor);
+ if (message != null) {
+ messages.add(message);
+ }
+ }
+ // Finally reorder the messages by their priority.
+ return reorderMessages(messages);
+ }
+
+ @Override
+ public int getNumberActivityVoicemailSources(Cursor cursor) {
+ int count = 0;
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ if (isVoicemailSourceActive(cursor)) {
+ ++count;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * Returns whether the source status in the cursor corresponds to an active source. A source is
+ * active if its' configuration state is not NOT_CONFIGURED. For most voicemail sources, only OK
+ * and NOT_CONFIGURED are used. The OMTP visual voicemail client has the same behavior pre-NMR1.
+ * NMR1 visual voicemail will only set it to NOT_CONFIGURED when it is deactivated. As soon as
+ * activation is attempted, it will transition into CONFIGURING then into OK or other error state,
+ * NOT_CONFIGURED is never set through an error.
+ */
+ private boolean isVoicemailSourceActive(Cursor cursor) {
+ return cursor.getString(VoicemailStatusQuery.SOURCE_PACKAGE_INDEX) != null
+ && cursor.getInt(VoicemailStatusQuery.CONFIGURATION_STATE_INDEX)
+ != Status.CONFIGURATION_STATE_NOT_CONFIGURED;
+ }
+
+ private List<StatusMessage> reorderMessages(List<MessageStatusWithPriority> messageWrappers) {
+ Collections.sort(
+ messageWrappers,
+ new Comparator<MessageStatusWithPriority>() {
+ @Override
+ public int compare(MessageStatusWithPriority msg1, MessageStatusWithPriority msg2) {
+ return msg1.mPriority - msg2.mPriority;
+ }
+ });
+ List<StatusMessage> reorderMessages = new ArrayList<VoicemailStatusHelper.StatusMessage>();
+ // Copy the ordered message objects into the final list.
+ for (MessageStatusWithPriority messageWrapper : messageWrappers) {
+ reorderMessages.add(messageWrapper.mMessage);
+ }
+ return reorderMessages;
+ }
+
+ /** Returns the message for the status entry pointed to by the cursor. */
+ private MessageStatusWithPriority getMessageForStatusEntry(Cursor cursor) {
+ final String sourcePackage = cursor.getString(VoicemailStatusQuery.SOURCE_PACKAGE_INDEX);
+ if (sourcePackage == null) {
+ return null;
+ }
+ final OverallState overallState =
+ getOverallState(
+ cursor.getInt(VoicemailStatusQuery.CONFIGURATION_STATE_INDEX),
+ cursor.getInt(VoicemailStatusQuery.DATA_CHANNEL_STATE_INDEX),
+ cursor.getInt(VoicemailStatusQuery.NOTIFICATION_CHANNEL_STATE_INDEX));
+ final Action action = overallState.getAction();
+
+ // No source package or no action, means no message shown.
+ if (action == Action.NONE) {
+ return null;
+ }
+
+ Uri actionUri = null;
+ if (action == Action.CALL_VOICEMAIL) {
+ actionUri =
+ UriUtils.parseUriOrNull(
+ cursor.getString(VoicemailStatusQuery.VOICEMAIL_ACCESS_URI_INDEX));
+ // Even if actionUri is null, it is still be useful to show the notification.
+ } else if (action == Action.CONFIGURE_VOICEMAIL) {
+ actionUri =
+ UriUtils.parseUriOrNull(cursor.getString(VoicemailStatusQuery.SETTINGS_URI_INDEX));
+ // If there is no settings URI, there is no point in showing the notification.
+ if (actionUri == null) {
+ return null;
+ }
+ }
+ return new MessageStatusWithPriority(
+ new StatusMessage(
+ sourcePackage,
+ overallState.getCallLogMessageId(),
+ overallState.getCallDetailsMessageId(),
+ action.getMessageId(),
+ actionUri),
+ overallState.getPriority());
+ }
+
+ private OverallState getOverallState(
+ int configurationState, int dataChannelState, int notificationChannelState) {
+ if (configurationState == CONFIGURATION_STATE_OK) {
+ // Voicemail is configured. Let's see how is the data channel.
+ if (dataChannelState == DATA_CHANNEL_STATE_OK) {
+ // Data channel is fine. What about notification channel?
+ if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+ return OverallState.OK;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+ return OverallState.NO_DETAILED_NOTIFICATION;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+ return OverallState.NO_NOTIFICATIONS;
+ }
+ } else if (dataChannelState == DATA_CHANNEL_STATE_NO_CONNECTION) {
+ // Data channel is not working. What about notification channel?
+ if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_OK) {
+ return OverallState.NO_DATA;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_MESSAGE_WAITING) {
+ return OverallState.MESSAGE_WAITING;
+ } else if (notificationChannelState == NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) {
+ return OverallState.NO_CONNECTION;
+ }
+ }
+ } else if (configurationState == CONFIGURATION_STATE_CAN_BE_CONFIGURED) {
+ // Voicemail not configured. data/notification channel states are irrelevant.
+ return OverallState.INVITE_FOR_CONFIGURATION;
+ } else if (configurationState == Status.CONFIGURATION_STATE_NOT_CONFIGURED) {
+ // Voicemail not configured. data/notification channel states are irrelevant.
+ return OverallState.NOT_CONFIGURED;
+ }
+ // Will reach here only if the source has set an invalid value.
+ return OverallState.INVALID;
+ }
+
+ /** Possible user actions. */
+ public enum Action {
+ NONE(-1),
+ CALL_VOICEMAIL(R.string.voicemail_status_action_call_server),
+ CONFIGURE_VOICEMAIL(R.string.voicemail_status_action_configure);
+
+ private final int mMessageId;
+
+ Action(int messageId) {
+ mMessageId = messageId;
+ }
+
+ public int getMessageId() {
+ return mMessageId;
+ }
+ }
+
+ /**
+ * Overall state of the source status. Each state is associated with the corresponding display
+ * string and the corrective action. The states are also assigned a relative priority which is
+ * used to order the messages from different sources.
+ */
+ private enum OverallState {
+ // TODO: Add separate string for call details and call log pages for the states that needs
+ // to be shown in both.
+ /** Both notification and data channel are not working. */
+ NO_CONNECTION(
+ 0,
+ Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_voicemail_not_available,
+ R.string.voicemail_status_audio_not_available),
+ /** Notifications working, but data channel is not working. Audio cannot be downloaded. */
+ NO_DATA(
+ 1,
+ Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_voicemail_not_available,
+ R.string.voicemail_status_audio_not_available),
+ /** Messages are known to be waiting but data channel is not working. */
+ MESSAGE_WAITING(
+ 2,
+ Action.CALL_VOICEMAIL,
+ R.string.voicemail_status_messages_waiting,
+ R.string.voicemail_status_audio_not_available),
+ /** Notification channel not working, but data channel is. */
+ NO_NOTIFICATIONS(3, Action.CALL_VOICEMAIL, R.string.voicemail_status_voicemail_not_available),
+ /** Invite user to set up voicemail. */
+ INVITE_FOR_CONFIGURATION(
+ 4, Action.CONFIGURE_VOICEMAIL, R.string.voicemail_status_configure_voicemail),
+ /**
+ * No detailed notifications, but data channel is working. This is normal mode of operation for
+ * certain sources. No action needed.
+ */
+ NO_DETAILED_NOTIFICATION(5, Action.NONE, -1),
+ /** Visual voicemail not yet set up. No local action needed. */
+ NOT_CONFIGURED(6, Action.NONE, -1),
+ /** Everything is OK. */
+ OK(7, Action.NONE, -1),
+ /** If one or more state value set by the source is not valid. */
+ INVALID(8, Action.NONE, -1);
+
+ private final int mPriority;
+ private final Action mAction;
+ private final int mCallLogMessageId;
+ private final int mCallDetailsMessageId;
+
+ OverallState(int priority, Action action, int callLogMessageId) {
+ this(priority, action, callLogMessageId, -1);
+ }
+
+ OverallState(int priority, Action action, int callLogMessageId, int callDetailsMessageId) {
+ mPriority = priority;
+ mAction = action;
+ mCallLogMessageId = callLogMessageId;
+ mCallDetailsMessageId = callDetailsMessageId;
+ }
+
+ public Action getAction() {
+ return mAction;
+ }
+
+ public int getPriority() {
+ return mPriority;
+ }
+
+ public int getCallLogMessageId() {
+ return mCallLogMessageId;
+ }
+
+ public int getCallDetailsMessageId() {
+ return mCallDetailsMessageId;
+ }
+ }
+
+ /** A wrapper on {@link StatusMessage} which additionally stores the priority of the message. */
+ private static class MessageStatusWithPriority {
+
+ private final StatusMessage mMessage;
+ private final int mPriority;
+
+ public MessageStatusWithPriority(StatusMessage message, int priority) {
+ mMessage = message;
+ mPriority = priority;
+ }
+ }
+}
diff --git a/java/com/android/dialer/voicemailstatus/res/values/strings.xml b/java/com/android/dialer/voicemailstatus/res/values/strings.xml
new file mode 100644
index 000000000..495ddf2e2
--- /dev/null
+++ b/java/com/android/dialer/voicemailstatus/res/values/strings.xml
@@ -0,0 +1,41 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+ <!-- Voicemail status message shown at the top of call log to notify the user that no new
+ voicemails are currently available. This can happen when both notification as well as data
+ connection to the voicemail server is lost. [CHAR LIMIT=64] -->
+ <string name="voicemail_status_voicemail_not_available">Voicemail updates not available</string>
+ <!-- Voicemail status message shown at the top of call log to notify the user that there is no
+ data connection to the voicemail server, but there are new voicemails waiting on the server.
+ [CHAR LIMIT=64] -->
+ <string name="voicemail_status_messages_waiting">New voicemail waiting. Can\'t load right now.</string>
+ <!-- Voicemail status message shown at the top of call log to invite the user to configure
+ visual voicemail. [CHAR LIMIT=64] -->
+ <string name="voicemail_status_configure_voicemail">Set up your voicemail</string>
+ <!-- Voicemail status message shown at the top of call details screen to notify the user that
+ the audio of this voicemail is not available. [CHAR LIMIT=64] -->
+ <string name="voicemail_status_audio_not_available">Audio not available</string>
+
+ <!-- User action prompt shown next to a voicemail status message to let the user configure
+ visual voicemail. [CHAR LIMIT=20] -->
+ <string name="voicemail_status_action_configure">Set up</string>
+ <!-- User action prompt shown next to a voicemail status message to let the user call voicemail
+ server directly to listen to the voicemails. [CHAR LIMIT=20] -->
+ <string name="voicemail_status_action_call_server">Call voicemail</string>
+
+</resources>
diff --git a/java/com/android/dialer/widget/AndroidManifest.xml b/java/com/android/dialer/widget/AndroidManifest.xml
new file mode 100644
index 000000000..f104cc146
--- /dev/null
+++ b/java/com/android/dialer/widget/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.dialer.widget">
+</manifest>
diff --git a/java/com/android/dialer/widget/ResizingTextEditText.java b/java/com/android/dialer/widget/ResizingTextEditText.java
new file mode 100644
index 000000000..fb894bd14
--- /dev/null
+++ b/java/com/android/dialer/widget/ResizingTextEditText.java
@@ -0,0 +1,51 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.EditText;
+import com.android.dialer.util.ViewUtil;
+
+/** EditText which resizes dynamically with respect to text length. */
+public class ResizingTextEditText extends EditText {
+
+ private final int mOriginalTextSize;
+ private final int mMinTextSize;
+
+ public ResizingTextEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mOriginalTextSize = (int) getTextSize();
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResizingText);
+ mMinTextSize =
+ (int) a.getDimension(R.styleable.ResizingText_resizing_text_min_size, mOriginalTextSize);
+ a.recycle();
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+}
diff --git a/java/com/android/dialer/widget/ResizingTextTextView.java b/java/com/android/dialer/widget/ResizingTextTextView.java
new file mode 100644
index 000000000..9b624414d
--- /dev/null
+++ b/java/com/android/dialer/widget/ResizingTextTextView.java
@@ -0,0 +1,51 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.widget.TextView;
+import com.android.dialer.util.ViewUtil;
+
+/** TextView which resizes dynamically with respect to text length. */
+public class ResizingTextTextView extends TextView {
+
+ private final int mOriginalTextSize;
+ private final int mMinTextSize;
+
+ public ResizingTextTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mOriginalTextSize = (int) getTextSize();
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResizingText);
+ mMinTextSize =
+ (int) a.getDimension(R.styleable.ResizingText_resizing_text_min_size, mOriginalTextSize);
+ a.recycle();
+ }
+
+ @Override
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+ super.onTextChanged(text, start, lengthBefore, lengthAfter);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ ViewUtil.resizeText(this, mOriginalTextSize, mMinTextSize);
+ }
+}
diff --git a/java/com/android/dialer/widget/res/values/attrs.xml b/java/com/android/dialer/widget/res/values/attrs.xml
new file mode 100644
index 000000000..bd5c3a4fb
--- /dev/null
+++ b/java/com/android/dialer/widget/res/values/attrs.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ Copyright (C) 2012 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+
+ <declare-styleable name="ResizingText">
+ <attr format="dimension" name="resizing_text_min_size"/>
+ </declare-styleable>
+
+</resources>
diff --git a/java/com/android/incallui/AccelerometerListener.java b/java/com/android/incallui/AccelerometerListener.java
new file mode 100644
index 000000000..01f884354
--- /dev/null
+++ b/java/com/android/incallui/AccelerometerListener.java
@@ -0,0 +1,173 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * This class is used to listen to the accelerometer to monitor the orientation of the phone. The
+ * client of this class is notified when the orientation changes between horizontal and vertical.
+ */
+public class AccelerometerListener {
+
+ // Device orientation
+ public static final int ORIENTATION_UNKNOWN = 0;
+ public static final int ORIENTATION_VERTICAL = 1;
+ public static final int ORIENTATION_HORIZONTAL = 2;
+ private static final String TAG = "AccelerometerListener";
+ private static final boolean DEBUG = true;
+ private static final boolean VDEBUG = false;
+ private static final int ORIENTATION_CHANGED = 1234;
+ private static final int VERTICAL_DEBOUNCE = 100;
+ private static final int HORIZONTAL_DEBOUNCE = 500;
+ private static final double VERTICAL_ANGLE = 50.0;
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+ // mOrientation is the orientation value most recently reported to the client.
+ private int mOrientation;
+ // mPendingOrientation is the latest orientation computed based on the sensor value.
+ // This is sent to the client after a rebounce delay, at which point it is copied to
+ // mOrientation.
+ private int mPendingOrientation;
+ private OrientationListener mListener;
+ Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ORIENTATION_CHANGED:
+ synchronized (this) {
+ mOrientation = mPendingOrientation;
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "orientation: "
+ + (mOrientation == ORIENTATION_HORIZONTAL
+ ? "horizontal"
+ : (mOrientation == ORIENTATION_VERTICAL ? "vertical" : "unknown")));
+ }
+ if (mListener != null) {
+ mListener.orientationChanged(mOrientation);
+ }
+ }
+ break;
+ }
+ }
+ };
+ SensorEventListener mSensorListener =
+ new SensorEventListener() {
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ onSensorEvent(event.values[0], event.values[1], event.values[2]);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // ignore
+ }
+ };
+
+ public AccelerometerListener(Context context) {
+ mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ public void setListener(OrientationListener listener) {
+ mListener = listener;
+ }
+
+ public void enable(boolean enable) {
+ if (DEBUG) {
+ Log.d(TAG, "enable(" + enable + ")");
+ }
+ synchronized (this) {
+ if (enable) {
+ mOrientation = ORIENTATION_UNKNOWN;
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ mSensorManager.registerListener(
+ mSensorListener, mSensor, SensorManager.SENSOR_DELAY_NORMAL);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener);
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+ }
+ }
+ }
+
+ private void setOrientation(int orientation) {
+ synchronized (this) {
+ if (mPendingOrientation == orientation) {
+ // Pending orientation has not changed, so do nothing.
+ return;
+ }
+
+ // Cancel any pending messages.
+ // We will either start a new timer or cancel alltogether
+ // if the orientation has not changed.
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+
+ if (mOrientation != orientation) {
+ // Set timer to send an event if the orientation has changed since its
+ // previously reported value.
+ mPendingOrientation = orientation;
+ final Message m = mHandler.obtainMessage(ORIENTATION_CHANGED);
+ // set delay to our debounce timeout
+ int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE : HORIZONTAL_DEBOUNCE);
+ mHandler.sendMessageDelayed(m, delay);
+ } else {
+ // no message is pending
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ }
+ }
+ }
+
+ private void onSensorEvent(double x, double y, double z) {
+ if (VDEBUG) {
+ Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")");
+ }
+
+ // If some values are exactly zero, then likely the sensor is not powered up yet.
+ // ignore these events to avoid false horizontal positives.
+ if (x == 0.0 || y == 0.0 || z == 0.0) {
+ return;
+ }
+
+ // magnitude of the acceleration vector projected onto XY plane
+ final double xy = Math.hypot(x, y);
+ // compute the vertical angle
+ double angle = Math.atan2(xy, z);
+ // convert to degrees
+ angle = angle * 180.0 / Math.PI;
+ final int orientation =
+ (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL);
+ if (VDEBUG) {
+ Log.d(TAG, "angle: " + angle + " orientation: " + orientation);
+ }
+ setOrientation(orientation);
+ }
+
+ public interface OrientationListener {
+
+ void orientationChanged(int orientation);
+ }
+}
diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml
new file mode 100644
index 000000000..276b47a5e
--- /dev/null
+++ b/java/com/android/incallui/AndroidManifest.xml
@@ -0,0 +1,121 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <uses-permission android:name="android.permission.CONTROL_INCALL_EXPERIENCE"/>
+ <!-- We use this to disable the status bar buttons of home, back and recent
+ during an incoming call. By doing so this allows us to not show the user
+ is viewing the activity in full screen alert, on a fresh system/factory
+ reset state of the app. -->
+ <uses-permission android:name="android.permission.STATUS_BAR"/>
+ <uses-permission android:name="android.permission.CAMERA"/>
+ <!-- Warning: setting the required boolean to true would prevent installation of Dialer on
+ devices which do not support a camera. -->
+ <uses-feature
+ android:name="android.hardware.camera.any"
+ android:required="false"/>
+
+ <!-- Testing location -->
+ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+
+ <!-- Set android:taskAffinity="com.android.incallui" for all activities to ensure proper
+ navigation. Otherwise system could bring up DialtactsActivity instead, e.g. when user unmerge a
+ call.
+ Set taskAffinity for application is not working because it will be merged and the result is
+ that all activities here still have same taskAffinity as activities under dialer. -->
+ <application>
+ <meta-data android:name="android.telephony.hide_voicemail_settings_menu"
+ android:value="true"/>
+ <activity
+ android:directBootAware="true"
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:label="@string/phoneAppLabel"
+ android:taskAffinity="com.android.incallui"
+ android:launchMode="singleInstance"
+ android:name="com.android.incallui.InCallActivity"
+ android:resizeableActivity="true"
+ android:screenOrientation="nosensor"
+ android:theme="@style/Theme.InCallScreen">
+ </activity>
+
+ <activity
+ android:directBootAware="true"
+ android:excludeFromRecents="true"
+ android:noHistory="true"
+ android:exported="false"
+ android:label="@string/manageConferenceLabel"
+ android:taskAffinity="com.android.incallui"
+ android:launchMode="singleTask"
+ android:name="com.android.incallui.ManageConferenceActivity"
+ android:resizeableActivity="true"
+ android:theme="@style/Theme.InCallScreen.ManageConference"/>
+
+ <service
+ android:directBootAware="true"
+ android:exported="true"
+ android:name="com.android.incallui.InCallServiceImpl"
+ android:permission="android.permission.BIND_INCALL_SERVICE">
+ <meta-data
+ android:name="android.telecom.IN_CALL_SERVICE_UI"
+ android:value="true"/>
+ <meta-data
+ android:name="android.telecom.IN_CALL_SERVICE_RINGING"
+ android:value="false"/>
+ <meta-data
+ android:name="android.telecom.INCLUDE_EXTERNAL_CALLS"
+ android:value="true"/>
+
+ <intent-filter>
+ <action android:name="android.telecom.InCallService"/>
+ </intent-filter>
+ </service>
+
+ <!--
+ Comments for attributes in SpamNotificationActivity:
+ taskAffinity="" -> Open the dialog without opening the dialer app behind it
+ noHistory="true" -> Navigating away finishes activity
+ excludeFromRecents="true" -> Don't show in "recent apps" screen
+ -->
+ <activity
+ android:excludeFromRecents="true"
+ android:exported="false"
+ android:name="com.android.incallui.spam.SpamNotificationActivity"
+ android:noHistory="true"
+ android:taskAffinity=""
+ android:theme="@style/AfterCallNotificationTheme">
+ </activity>
+
+ <service
+ android:exported="false"
+ android:name="com.android.incallui.spam.SpamNotificationService"/>
+
+ <!-- BroadcastReceiver for receiving Intents from Notification mechanism. -->
+ <receiver
+ android:directBootAware="true"
+ android:exported="false"
+ android:name="com.android.incallui.NotificationBroadcastReceiver"/>
+
+ </application>
+
+</manifest>
+
diff --git a/java/com/android/incallui/AnswerScreenPresenter.java b/java/com/android/incallui/AnswerScreenPresenter.java
new file mode 100644
index 000000000..a21876b2b
--- /dev/null
+++ b/java/com/android/incallui/AnswerScreenPresenter.java
@@ -0,0 +1,110 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.v4.os.UserManagerCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.answer.protocol.AnswerScreen;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+import com.android.incallui.answerproximitysensor.AnswerProximitySensor;
+import com.android.incallui.answerproximitysensor.PseudoScreenState;
+import com.android.incallui.call.DialerCall;
+
+/** Manages changes for an incoming call screen. */
+public class AnswerScreenPresenter
+ implements AnswerScreenDelegate, DialerCall.CannedTextResponsesLoadedListener {
+ @NonNull private final Context context;
+ @NonNull private final AnswerScreen answerScreen;
+ @NonNull private final DialerCall call;
+
+ public AnswerScreenPresenter(
+ @NonNull Context context, @NonNull AnswerScreen answerScreen, @NonNull DialerCall call) {
+ LogUtil.i("AnswerScreenPresenter.constructor", null);
+ this.context = Assert.isNotNull(context);
+ this.answerScreen = Assert.isNotNull(answerScreen);
+ this.call = Assert.isNotNull(call);
+ if (isSmsResponseAllowed(call)) {
+ answerScreen.setTextResponses(call.getCannedSmsResponses());
+ }
+ call.addCannedTextResponsesLoadedListener(this);
+
+ PseudoScreenState pseudoScreenState = InCallPresenter.getInstance().getPseudoScreenState();
+ if (AnswerProximitySensor.shouldUse(context, call)) {
+ new AnswerProximitySensor(context, call, pseudoScreenState);
+ } else {
+ pseudoScreenState.setOn(true);
+ }
+ }
+
+ @Override
+ public void onAnswerScreenUnready() {
+ call.removeCannedTextResponsesLoadedListener(this);
+ }
+
+ @Override
+ public void onDismissDialog() {
+ InCallPresenter.getInstance().onDismissDialog();
+ }
+
+ @Override
+ public void onRejectCallWithMessage(String message) {
+ call.reject(true /* rejectWithMessage */, message);
+ onDismissDialog();
+ }
+
+ @Override
+ public void onAnswer(int videoState) {
+ if (answerScreen.isVideoUpgradeRequest()) {
+ call.acceptUpgradeRequest(videoState);
+ } else {
+ call.answer(videoState);
+ }
+ }
+
+ @Override
+ public void onReject() {
+ if (answerScreen.isVideoUpgradeRequest()) {
+ call.declineUpgradeRequest();
+ } else {
+ call.reject(false /* rejectWithMessage */, null);
+ }
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(DialerCall call) {
+ if (isSmsResponseAllowed(call)) {
+ answerScreen.setTextResponses(call.getCannedSmsResponses());
+ }
+ }
+
+ @Override
+ public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {
+ InCallActivity activity = (InCallActivity) answerScreen.getAnswerScreenFragment().getActivity();
+ if (activity != null) {
+ activity.updateWindowBackgroundColor(progress);
+ }
+ }
+
+ private boolean isSmsResponseAllowed(DialerCall call) {
+ return UserManagerCompat.isUserUnlocked(context)
+ && call.can(android.telecom.Call.Details.CAPABILITY_RESPOND_VIA_TEXT);
+ }
+}
diff --git a/java/com/android/incallui/AnswerScreenPresenterStub.java b/java/com/android/incallui/AnswerScreenPresenterStub.java
new file mode 100644
index 000000000..fc47bf5b0
--- /dev/null
+++ b/java/com/android/incallui/AnswerScreenPresenterStub.java
@@ -0,0 +1,44 @@
+/*
+ * 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.incallui;
+
+import android.support.annotation.FloatRange;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+
+/**
+ * Stub implementation of the answer screen delegate. Used to keep the answer fragment visible when
+ * no call exists.
+ */
+public class AnswerScreenPresenterStub implements AnswerScreenDelegate {
+ @Override
+ public void onAnswerScreenUnready() {}
+
+ @Override
+ public void onDismissDialog() {}
+
+ @Override
+ public void onRejectCallWithMessage(String message) {}
+
+ @Override
+ public void onAnswer(int videoState) {}
+
+ @Override
+ public void onReject() {}
+
+ @Override
+ public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {}
+}
diff --git a/java/com/android/incallui/AudioModeProvider.java b/java/com/android/incallui/AudioModeProvider.java
new file mode 100644
index 000000000..698db0ab9
--- /dev/null
+++ b/java/com/android/incallui/AudioModeProvider.java
@@ -0,0 +1,69 @@
+/*
+ * 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.incallui;
+
+import android.telecom.CallAudioState;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Proxy class for getting and setting the audio mode. */
+public class AudioModeProvider {
+ private static final int SUPPORTED_AUDIO_ROUTE_ALL =
+ CallAudioState.ROUTE_EARPIECE
+ | CallAudioState.ROUTE_BLUETOOTH
+ | CallAudioState.ROUTE_WIRED_HEADSET
+ | CallAudioState.ROUTE_SPEAKER;
+
+ private static final AudioModeProvider instance = new AudioModeProvider();
+ private final List<AudioModeListener> listeners = new ArrayList<>();
+ private CallAudioState audioState =
+ new CallAudioState(false, CallAudioState.ROUTE_EARPIECE, SUPPORTED_AUDIO_ROUTE_ALL);
+
+ public static AudioModeProvider getInstance() {
+ return instance;
+ }
+
+ public void onAudioStateChanged(CallAudioState audioState) {
+ if (!this.audioState.equals(audioState)) {
+ this.audioState = audioState;
+ for (AudioModeListener listener : listeners) {
+ listener.onAudioStateChanged(audioState);
+ }
+ }
+ }
+
+ public void addListener(AudioModeListener listener) {
+ if (!listeners.contains(listener)) {
+ listeners.add(listener);
+ listener.onAudioStateChanged(audioState);
+ }
+ }
+
+ public void removeListener(AudioModeListener listener) {
+ listeners.remove(listener);
+ }
+
+ public CallAudioState getAudioState() {
+ return audioState;
+ }
+
+ /** Notified on changes to audio mode. */
+ public interface AudioModeListener {
+
+ void onAudioStateChanged(CallAudioState audioState);
+ }
+}
diff --git a/java/com/android/incallui/Bindings.java b/java/com/android/incallui/Bindings.java
new file mode 100644
index 000000000..4f142ff96
--- /dev/null
+++ b/java/com/android/incallui/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.incallui;
+
+import android.content.Context;
+import com.android.incallui.bindings.InCallUiBindings;
+import com.android.incallui.bindings.InCallUiBindingsFactory;
+import com.android.incallui.bindings.InCallUiBindingsStub;
+import java.util.Objects;
+
+/** Accessor for the in call UI bindings. */
+public class Bindings {
+
+ private static InCallUiBindings instance;
+
+ private Bindings() {}
+
+ public static InCallUiBindings get(Context context) {
+ Objects.requireNonNull(context);
+ if (instance != null) {
+ return instance;
+ }
+
+ Context application = context.getApplicationContext();
+ if (application instanceof InCallUiBindingsFactory) {
+ instance = ((InCallUiBindingsFactory) application).newInCallUiBindings();
+ }
+
+ if (instance == null) {
+ instance = new InCallUiBindingsStub();
+ }
+ return instance;
+ }
+
+ public static void setForTesting(InCallUiBindings testInstance) {
+ instance = testInstance;
+ }
+}
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
new file mode 100644
index 000000000..d6f4cddc9
--- /dev/null
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -0,0 +1,515 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.CallAudioState;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.SdkVersionOverride;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.incallui.AudioModeProvider.AudioModeListener;
+import com.android.incallui.InCallCameraManager.Listener;
+import com.android.incallui.InCallPresenter.CanAddCallListener;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+
+/** Logic for call buttons. */
+public class CallButtonPresenter
+ implements InCallStateListener,
+ AudioModeListener,
+ IncomingCallListener,
+ InCallDetailsListener,
+ CanAddCallListener,
+ Listener,
+ InCallButtonUiDelegate {
+
+ private static final String KEY_AUTOMATICALLY_MUTED = "incall_key_automatically_muted";
+ private static final String KEY_PREVIOUS_MUTE_STATE = "incall_key_previous_mute_state";
+
+ private final Context mContext;
+ private InCallButtonUi mInCallButtonUi;
+ private DialerCall mCall;
+ private boolean mAutomaticallyMuted = false;
+ private boolean mPreviousMuteState = false;
+ private boolean isInCallButtonUiReady;
+
+ public CallButtonPresenter(Context context) {
+ mContext = context.getApplicationContext();
+ }
+
+ @Override
+ public void onInCallButtonUiReady(InCallButtonUi ui) {
+ Assert.checkState(!isInCallButtonUiReady);
+ mInCallButtonUi = ui;
+ AudioModeProvider.getInstance().addListener(this);
+
+ // register for call state changes last
+ final InCallPresenter inCallPresenter = InCallPresenter.getInstance();
+ inCallPresenter.addListener(this);
+ inCallPresenter.addIncomingCallListener(this);
+ inCallPresenter.addDetailsListener(this);
+ inCallPresenter.addCanAddCallListener(this);
+ inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
+
+ // Update the buttons state immediately for the current call
+ onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
+ isInCallButtonUiReady = true;
+ }
+
+ @Override
+ public void onInCallButtonUiUnready() {
+ Assert.checkState(isInCallButtonUiReady);
+ mInCallButtonUi = null;
+ InCallPresenter.getInstance().removeListener(this);
+ AudioModeProvider.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
+ InCallPresenter.getInstance().removeCanAddCallListener(this);
+ isInCallButtonUiReady = false;
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ if (newState == InCallState.OUTGOING) {
+ mCall = callList.getOutgoingCall();
+ } else if (newState == InCallState.INCALL) {
+ mCall = callList.getActiveOrBackgroundCall();
+
+ // When connected to voice mail, automatically shows the dialpad.
+ // (On previous releases we showed it when in-call shows up, before waiting for
+ // OUTGOING. We may want to do that once we start showing "Voice mail" label on
+ // the dialpad too.)
+ if (oldState == InCallState.OUTGOING && mCall != null) {
+ if (CallerInfoUtils.isVoiceMailNumber(mContext, mCall) && getActivity() != null) {
+ getActivity().showDialpadFragment(true /* show */, true /* animate */);
+ }
+ }
+ } else if (newState == InCallState.INCOMING) {
+ if (getActivity() != null) {
+ getActivity().showDialpadFragment(false /* show */, true /* animate */);
+ }
+ mCall = callList.getIncomingCall();
+ } else {
+ mCall = null;
+ }
+ updateUi(newState, mCall);
+ }
+
+ /**
+ * Updates the user interface in response to a change in the details of a call. Currently handles
+ * changes to the call buttons in response to a change in the details for a call. This is
+ * important to ensure changes to the active call are reflected in the available buttons.
+ *
+ * @param call The active call.
+ * @param details The call details.
+ */
+ @Override
+ public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
+ // Only update if the changes are for the currently active call
+ if (mInCallButtonUi != null && call != null && call.equals(mCall)) {
+ updateButtonsState(call);
+ }
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ @Override
+ public void onCanAddCallChanged(boolean canAddCall) {
+ if (mInCallButtonUi != null && mCall != null) {
+ updateButtonsState(mCall);
+ }
+ }
+
+ @Override
+ public void onAudioStateChanged(CallAudioState audioState) {
+ if (mInCallButtonUi != null) {
+ mInCallButtonUi.setAudioState(audioState);
+ }
+ }
+
+ @Override
+ public CallAudioState getCurrentAudioState() {
+ return AudioModeProvider.getInstance().getAudioState();
+ }
+
+ @Override
+ public void setAudioRoute(int route) {
+ LogUtil.i(
+ "CallButtonPresenter.setAudioRoute",
+ "sending new audio route: " + CallAudioState.audioRouteToString(route));
+ TelecomAdapter.getInstance().setAudioRoute(route);
+ }
+
+ /** Function assumes that bluetooth is not supported. */
+ @Override
+ public void toggleSpeakerphone() {
+ // This function should not be called if bluetooth is available.
+ CallAudioState audioState = getCurrentAudioState();
+ if (0 != (CallAudioState.ROUTE_BLUETOOTH & audioState.getSupportedRouteMask())) {
+ // It's clear the UI is wrong, so update the supported mode once again.
+ LogUtil.e(
+ "CallButtonPresenter", "toggling speakerphone not allowed when bluetooth supported.");
+ mInCallButtonUi.setAudioState(audioState);
+ return;
+ }
+
+ int newRoute;
+ if (audioState.getRoute() == CallAudioState.ROUTE_SPEAKER) {
+ newRoute = CallAudioState.ROUTE_WIRED_OR_EARPIECE;
+ Logger.get(mContext)
+ .logCallImpression(
+ DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_WIRED_OR_EARPIECE,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
+ } else {
+ newRoute = CallAudioState.ROUTE_SPEAKER;
+ Logger.get(mContext)
+ .logCallImpression(
+ DialerImpression.Type.IN_CALL_SCREEN_TURN_ON_SPEAKERPHONE,
+ mCall.getUniqueCallId(),
+ mCall.getTimeAddedMs());
+ }
+
+ setAudioRoute(newRoute);
+ }
+
+ @Override
+ public void muteClicked(boolean checked) {
+ LogUtil.v("CallButtonPresenter", "turning on mute: " + checked);
+ TelecomAdapter.getInstance().mute(checked);
+ }
+
+ @Override
+ public void holdClicked(boolean checked) {
+ if (mCall == null) {
+ return;
+ }
+ if (checked) {
+ LogUtil.i("CallButtonPresenter", "putting the call on hold: " + mCall);
+ mCall.hold();
+ } else {
+ LogUtil.i("CallButtonPresenter", "removing the call from hold: " + mCall);
+ mCall.unhold();
+ }
+ }
+
+ @Override
+ public void swapClicked() {
+ if (mCall == null) {
+ return;
+ }
+
+ LogUtil.i("CallButtonPresenter", "swapping the call: " + mCall);
+ TelecomAdapter.getInstance().swap(mCall.getId());
+ }
+
+ @Override
+ public void mergeClicked() {
+ TelecomAdapter.getInstance().merge(mCall.getId());
+ }
+
+ @Override
+ public void addCallClicked() {
+ // Automatically mute the current call
+ mAutomaticallyMuted = true;
+ mPreviousMuteState = AudioModeProvider.getInstance().getAudioState().isMuted();
+ // Simulate a click on the mute button
+ muteClicked(true);
+ TelecomAdapter.getInstance().addCall();
+ }
+
+ @Override
+ public void showDialpadClicked(boolean checked) {
+ LogUtil.v("CallButtonPresenter", "show dialpad " + String.valueOf(checked));
+ getActivity().showDialpadFragment(checked /* show */, true /* animate */);
+ }
+
+ @Override
+ public void changeToVideoClicked() {
+ VideoCall videoCall = mCall.getVideoCall();
+ if (videoCall == null) {
+ return;
+ }
+ int currVideoState = mCall.getVideoState();
+ int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(currVideoState);
+ currUnpausedVideoState |= VideoProfile.STATE_BIDIRECTIONAL;
+
+ VideoProfile videoProfile = new VideoProfile(currUnpausedVideoState);
+ videoCall.sendSessionModifyRequest(videoProfile);
+ mCall.setSessionModificationState(
+ DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE);
+ }
+
+ @Override
+ public void onEndCallClicked() {
+ LogUtil.i("CallButtonPresenter.onEndCallClicked", "call: " + mCall);
+ if (mCall != null) {
+ mCall.disconnect();
+ }
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ mInCallButtonUi.showAudioRouteSelector();
+ }
+
+ /**
+ * Switches the camera between the front-facing and back-facing camera.
+ *
+ * @param useFrontFacingCamera True if we should switch to using the front-facing camera, or false
+ * if we should switch to using the back-facing camera.
+ */
+ @Override
+ public void switchCameraClicked(boolean useFrontFacingCamera) {
+ InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
+ cameraManager.setUseFrontFacingCamera(useFrontFacingCamera);
+
+ VideoCall videoCall = mCall.getVideoCall();
+ if (videoCall == null) {
+ return;
+ }
+
+ String cameraId = cameraManager.getActiveCameraId();
+ if (cameraId != null) {
+ final int cameraDir =
+ cameraManager.isUsingFrontFacingCamera()
+ ? DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING
+ : DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING;
+ mCall.getVideoSettings().setCameraDir(cameraDir);
+ videoCall.setCamera(cameraId);
+ videoCall.requestCameraCapabilities();
+ }
+ }
+
+ @Override
+ public void toggleCameraClicked() {
+ LogUtil.i("CallButtonPresenter.toggleCameraClicked", "");
+ switchCameraClicked(
+ !InCallPresenter.getInstance().getInCallCameraManager().isUsingFrontFacingCamera());
+ }
+
+ /**
+ * Stop or start client's video transmission.
+ *
+ * @param pause True if pausing the local user's video, or false if starting the local user's
+ * video.
+ */
+ @Override
+ public void pauseVideoClicked(boolean pause) {
+ LogUtil.i("CallButtonPresenter.pauseVideoClicked", "%s", pause ? "pause" : "unpause");
+ VideoCall videoCall = mCall.getVideoCall();
+ if (videoCall == null) {
+ return;
+ }
+
+ int currUnpausedVideoState = VideoUtils.getUnPausedVideoState(mCall.getVideoState());
+ if (pause) {
+ videoCall.setCamera(null);
+ VideoProfile videoProfile =
+ new VideoProfile(currUnpausedVideoState & ~VideoProfile.STATE_TX_ENABLED);
+ videoCall.sendSessionModifyRequest(videoProfile);
+ } else {
+ InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
+ videoCall.setCamera(cameraManager.getActiveCameraId());
+ VideoProfile videoProfile =
+ new VideoProfile(currUnpausedVideoState | VideoProfile.STATE_TX_ENABLED);
+ videoCall.sendSessionModifyRequest(videoProfile);
+ mCall.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE);
+ }
+
+ mInCallButtonUi.setVideoPaused(pause);
+ mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, false);
+ }
+
+ private void updateUi(InCallState state, DialerCall call) {
+ LogUtil.v("CallButtonPresenter", "updating call UI for call: ", call);
+
+ if (mInCallButtonUi == null) {
+ return;
+ }
+
+ if (call != null) {
+ mInCallButtonUi.updateInCallButtonUiColors();
+ }
+
+ final boolean isEnabled =
+ state.isConnectingOrConnected() && !state.isIncoming() && call != null;
+ mInCallButtonUi.setEnabled(isEnabled);
+
+ if (call == null) {
+ return;
+ }
+
+ updateButtonsState(call);
+ }
+
+ /**
+ * Updates the buttons applicable for the UI.
+ *
+ * @param call The active call.
+ */
+ private void updateButtonsState(DialerCall call) {
+ LogUtil.v("CallButtonPresenter.updateButtonsState", "");
+ final boolean isVideo = VideoUtils.isVideoCall(call);
+
+ // Common functionality (audio, hold, etc).
+ // Show either HOLD or SWAP, but not both. If neither HOLD or SWAP is available:
+ // (1) If the device normally can hold, show HOLD in a disabled state.
+ // (2) If the device doesn't have the concept of hold/swap, remove the button.
+ final boolean showSwap = call.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
+ final boolean showHold =
+ !showSwap
+ && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORT_HOLD)
+ && call.can(android.telecom.Call.Details.CAPABILITY_HOLD);
+ final boolean isCallOnHold = call.getState() == DialerCall.State.ONHOLD;
+
+ final boolean showAddCall =
+ TelecomAdapter.getInstance().canAddCall() && UserManagerCompat.isUserUnlocked(mContext);
+ final boolean showMerge = call.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
+ final boolean showUpgradeToVideo = !isVideo && hasVideoCallCapabilities(call);
+ final boolean showDowngradeToAudio = isVideo && isDowngradeToAudioSupported(call);
+ final boolean showMute = call.can(android.telecom.Call.Details.CAPABILITY_MUTE);
+
+ final boolean hasCameraPermission =
+ isVideo && VideoUtils.hasCameraPermissionAndAllowedByUser(mContext);
+ // Disabling local video doesn't seem to work when dialing. See b/30256571.
+ final boolean showPauseVideo =
+ isVideo
+ && call.getState() != DialerCall.State.DIALING
+ && call.getState() != DialerCall.State.CONNECTING;
+
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_AUDIO, true);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_SWAP, showSwap);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_HOLD, showHold);
+ mInCallButtonUi.setHold(isCallOnHold);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MUTE, showMute);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_ADD_CALL, true);
+ mInCallButtonUi.enableButton(InCallButtonIds.BUTTON_ADD_CALL, showAddCall);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, showUpgradeToVideo);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO, showDowngradeToAudio);
+ mInCallButtonUi.showButton(
+ InCallButtonIds.BUTTON_SWITCH_CAMERA, isVideo && hasCameraPermission);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_PAUSE_VIDEO, showPauseVideo);
+ if (isVideo) {
+ mInCallButtonUi.setVideoPaused(
+ !VideoUtils.isTransmissionEnabled(call) || !hasCameraPermission);
+ }
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
+ mInCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
+
+ mInCallButtonUi.updateButtonStates();
+ }
+
+ private boolean hasVideoCallCapabilities(DialerCall call) {
+ if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
+ return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX)
+ && call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_REMOTE_RX);
+ }
+ // In L, this single flag represents both video transmitting and receiving capabilities
+ return call.can(android.telecom.Call.Details.CAPABILITY_SUPPORTS_VT_LOCAL_TX);
+ }
+
+ /**
+ * Determines if downgrading from a video call to an audio-only call is supported. In order to
+ * support downgrade to audio, the SDK version must be >= N and the call should NOT have the
+ * {@link android.telecom.Call.Details#CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO}.
+ *
+ * @param call The call.
+ * @return {@code true} if downgrading to an audio-only call from a video call is supported.
+ */
+ private boolean isDowngradeToAudioSupported(DialerCall call) {
+ return !call.can(CallCompat.Details.CAPABILITY_CANNOT_DOWNGRADE_VIDEO_TO_AUDIO);
+ }
+
+ @Override
+ public void refreshMuteState() {
+ // Restore the previous mute state
+ if (mAutomaticallyMuted
+ && AudioModeProvider.getInstance().getAudioState().isMuted() != mPreviousMuteState) {
+ if (mInCallButtonUi == null) {
+ return;
+ }
+ muteClicked(mPreviousMuteState);
+ }
+ mAutomaticallyMuted = false;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
+ outState.putBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ mAutomaticallyMuted =
+ savedInstanceState.getBoolean(KEY_AUTOMATICALLY_MUTED, mAutomaticallyMuted);
+ mPreviousMuteState = savedInstanceState.getBoolean(KEY_PREVIOUS_MUTE_STATE, mPreviousMuteState);
+ }
+
+ @Override
+ public void onCameraPermissionGranted() {
+ if (mCall != null) {
+ updateButtonsState(mCall);
+ }
+ }
+
+ @Override
+ public void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera) {
+ if (mInCallButtonUi == null) {
+ return;
+ }
+ mInCallButtonUi.setCameraSwitched(!isUsingFrontFacingCamera);
+ }
+
+ @Override
+ public Context getContext() {
+ return mContext;
+ }
+
+ private InCallActivity getActivity() {
+ if (mInCallButtonUi != null) {
+ Fragment fragment = mInCallButtonUi.getInCallButtonUiFragment();
+ if (fragment != null) {
+ return (InCallActivity) fragment.getActivity();
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/incallui/CallCardPresenter.java b/java/com/android/incallui/CallCardPresenter.java
new file mode 100644
index 000000000..930775772
--- /dev/null
+++ b/java/com/android/incallui/CallCardPresenter.java
@@ -0,0 +1,1110 @@
+/*
+ * 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.incallui;
+
+import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
+
+import android.Manifest;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.hardware.display.DisplayManager;
+import android.os.BatteryManager;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.content.ContextCompat;
+import android.telecom.Call.Details;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.view.Display;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.enrichedcall.EnrichedCallManager;
+import com.android.dialer.enrichedcall.Session;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallEventListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import java.lang.ref.WeakReference;
+
+/**
+ * Controller for the Call Card Fragment. This class listens for changes to InCallState and passes
+ * it along to the fragment.
+ */
+public class CallCardPresenter
+ implements InCallStateListener,
+ IncomingCallListener,
+ InCallDetailsListener,
+ InCallEventListener,
+ InCallScreenDelegate,
+ DialerCallListener,
+ EnrichedCallManager.StateChangedListener {
+
+ /**
+ * Amount of time to wait before sending an announcement via the accessibility manager. When the
+ * call state changes to an outgoing or incoming state for the first time, the UI can often be
+ * changing due to call updates or contact lookup. This allows the UI to settle to a stable state
+ * to ensure that the correct information is announced.
+ */
+ private static final long ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS = 500;
+
+ /** Flag to allow the user's current location to be shown during emergency calls. */
+ private static final String CONFIG_ENABLE_EMERGENCY_LOCATION = "config_enable_emergency_location";
+
+ private static final boolean CONFIG_ENABLE_EMERGENCY_LOCATION_DEFAULT = true;
+
+ /**
+ * Make it possible to not get location during an emergency call if the battery is too low, since
+ * doing so could trigger gps and thus potentially cause the phone to die in the middle of the
+ * call.
+ */
+ private static final String CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION =
+ "min_battery_percent_for_emergency_location";
+
+ private static final long CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT = 10;
+
+ private final Context mContext;
+ private final Handler handler = new Handler();
+
+ private DialerCall mPrimary;
+ private DialerCall mSecondary;
+ private ContactCacheEntry mPrimaryContactInfo;
+ private ContactCacheEntry mSecondaryContactInfo;
+ @Nullable private ContactsPreferences mContactsPreferences;
+ private boolean mIsFullscreen = false;
+ private InCallScreen mInCallScreen;
+ private boolean isInCallScreenReady;
+ private boolean shouldSendAccessibilityEvent;
+ private final String locationModule = null;
+ private final Runnable sendAccessibilityEventRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ shouldSendAccessibilityEvent = !sendAccessibilityEvent(mContext, getUi());
+ LogUtil.i(
+ "CallCardPresenter.sendAccessibilityEventRunnable",
+ "still should send: %b",
+ shouldSendAccessibilityEvent);
+ if (!shouldSendAccessibilityEvent) {
+ handler.removeCallbacks(this);
+ }
+ }
+ };
+
+ public CallCardPresenter(Context context) {
+ LogUtil.i("CallCardController.constructor", null);
+ mContext = Assert.isNotNull(context).getApplicationContext();
+ }
+
+ private static boolean hasCallSubject(DialerCall call) {
+ return !TextUtils.isEmpty(call.getCallSubject());
+ }
+
+ @Override
+ public void onInCallScreenDelegateInit(InCallScreen inCallScreen) {
+ Assert.isNotNull(inCallScreen);
+ mInCallScreen = inCallScreen;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+
+ // Call may be null if disconnect happened already.
+ DialerCall call = CallList.getInstance().getFirstCall();
+ if (call != null) {
+ mPrimary = call;
+ if (shouldShowNoteSentToast(mPrimary)) {
+ mInCallScreen.showNoteSentToast();
+ }
+ call.addListener(this);
+
+ // start processing lookups right away.
+ if (!call.isConferenceCall()) {
+ startContactInfoSearch(call, true, call.getState() == DialerCall.State.INCOMING);
+ } else {
+ updateContactEntry(null, true);
+ }
+ }
+
+ onStateChange(null, InCallPresenter.getInstance().getInCallState(), CallList.getInstance());
+ }
+
+ @Override
+ public void onInCallScreenReady() {
+ LogUtil.i("CallCardController.onInCallScreenReady", null);
+ Assert.checkState(!isInCallScreenReady);
+ if (mContactsPreferences != null) {
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ }
+
+ EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .registerStateChangedListener(this);
+
+ // Contact search may have completed before ui is ready.
+ if (mPrimaryContactInfo != null) {
+ updatePrimaryDisplayInfo();
+ }
+
+ // Register for call state changes last
+ InCallPresenter.getInstance().addListener(this);
+ InCallPresenter.getInstance().addIncomingCallListener(this);
+ InCallPresenter.getInstance().addDetailsListener(this);
+ InCallPresenter.getInstance().addInCallEventListener(this);
+ isInCallScreenReady = true;
+ }
+
+ @Override
+ public void onInCallScreenUnready() {
+ LogUtil.i("CallCardController.onInCallScreenUnready", null);
+ Assert.checkState(isInCallScreenReady);
+
+ EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .unregisterStateChangedListener(this);
+ // stop getting call state changes
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().removeInCallEventListener(this);
+ if (mPrimary != null) {
+ mPrimary.removeListener(this);
+ }
+
+ mPrimary = null;
+ mPrimaryContactInfo = null;
+ mSecondaryContactInfo = null;
+ isInCallScreenReady = false;
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ // same logic should happen as with onStateChange()
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ LogUtil.v("CallCardPresenter.onStateChange", "" + newState);
+ if (mInCallScreen == null) {
+ return;
+ }
+
+ DialerCall primary = null;
+ DialerCall secondary = null;
+
+ if (newState == InCallState.INCOMING) {
+ primary = callList.getIncomingCall();
+ } else if (newState == InCallState.PENDING_OUTGOING || newState == InCallState.OUTGOING) {
+ primary = callList.getOutgoingCall();
+ if (primary == null) {
+ primary = callList.getPendingOutgoingCall();
+ }
+
+ // getCallToDisplay doesn't go through outgoing or incoming calls. It will return the
+ // highest priority call to display as the secondary call.
+ secondary = getCallToDisplay(callList, null, true);
+ } else if (newState == InCallState.INCALL) {
+ primary = getCallToDisplay(callList, null, false);
+ secondary = getCallToDisplay(callList, primary, true);
+ }
+
+ LogUtil.v("CallCardPresenter.onStateChange", "primary call: " + primary);
+ LogUtil.v("CallCardPresenter.onStateChange", "secondary call: " + secondary);
+
+ final boolean primaryChanged =
+ !(DialerCall.areSame(mPrimary, primary) && DialerCall.areSameNumber(mPrimary, primary));
+ final boolean secondaryChanged =
+ !(DialerCall.areSame(mSecondary, secondary)
+ && DialerCall.areSameNumber(mSecondary, secondary));
+
+ mSecondary = secondary;
+ DialerCall previousPrimary = mPrimary;
+ mPrimary = primary;
+
+ if (mPrimary != null) {
+ InCallPresenter.getInstance().onForegroundCallChanged(mPrimary);
+ mInCallScreen.updateInCallScreenColors();
+ }
+
+ if (primaryChanged && shouldShowNoteSentToast(primary)) {
+ mInCallScreen.showNoteSentToast();
+ }
+
+ // Refresh primary call information if either:
+ // 1. Primary call changed.
+ // 2. The call's ability to manage conference has changed.
+ if (shouldRefreshPrimaryInfo(primaryChanged)) {
+ // primary call has changed
+ if (previousPrimary != null) {
+ previousPrimary.removeListener(this);
+ }
+ mPrimary.addListener(this);
+
+ mPrimaryContactInfo =
+ ContactInfoCache.buildCacheEntryFromCall(
+ mContext, mPrimary, mPrimary.getState() == DialerCall.State.INCOMING);
+ updatePrimaryDisplayInfo();
+ maybeStartSearch(mPrimary, true);
+ maybeClearSessionModificationState(mPrimary);
+ }
+
+ if (previousPrimary != null && mPrimary == null) {
+ previousPrimary.removeListener(this);
+ }
+
+ if (mSecondary == null) {
+ // Secondary call may have ended. Update the ui.
+ mSecondaryContactInfo = null;
+ updateSecondaryDisplayInfo();
+ } else if (secondaryChanged) {
+ // secondary call has changed
+ mSecondaryContactInfo =
+ ContactInfoCache.buildCacheEntryFromCall(
+ mContext, mSecondary, mSecondary.getState() == DialerCall.State.INCOMING);
+ updateSecondaryDisplayInfo();
+ maybeStartSearch(mSecondary, false);
+ maybeClearSessionModificationState(mSecondary);
+ }
+
+ // Set the call state
+ int callState = DialerCall.State.IDLE;
+ if (mPrimary != null) {
+ callState = mPrimary.getState();
+ updatePrimaryCallState();
+ } else {
+ getUi().setCallState(PrimaryCallState.createEmptyPrimaryCallState());
+ }
+
+ maybeShowManageConferenceCallButton();
+
+ // Hide the end call button instantly if we're receiving an incoming call.
+ getUi()
+ .setEndCallButtonEnabled(
+ shouldShowEndCallButton(mPrimary, callState),
+ callState != DialerCall.State.INCOMING /* animate */);
+
+ maybeSendAccessibilityEvent(oldState, newState, primaryChanged);
+ }
+
+ @Override
+ public void onDetailsChanged(DialerCall call, Details details) {
+ updatePrimaryCallState();
+
+ if (call.can(Details.CAPABILITY_MANAGE_CONFERENCE)
+ != details.can(Details.CAPABILITY_MANAGE_CONFERENCE)) {
+ maybeShowManageConferenceCallButton();
+ }
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {}
+
+ @Override
+ public void onDialerCallUpdate() {
+ // No-op; specific call updates handled elsewhere.
+ }
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ /** Handles a change to the child number by refreshing the primary call info. */
+ @Override
+ public void onDialerCallChildNumberChange() {
+ LogUtil.v("CallCardPresenter.onDialerCallChildNumberChange", "");
+
+ if (mPrimary == null) {
+ return;
+ }
+ updatePrimaryDisplayInfo();
+ }
+
+ /** Handles a change to the last forwarding number by refreshing the primary call info. */
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {
+ LogUtil.v("CallCardPresenter.onDialerCallLastForwardedNumberChange", "");
+
+ if (mPrimary == null) {
+ return;
+ }
+ updatePrimaryDisplayInfo();
+ updatePrimaryCallState();
+ }
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ /**
+ * Handles a change to the session modification state for a call.
+ *
+ * @param sessionModificationState The new session modification state.
+ */
+ @Override
+ public void onDialerCallSessionModificationStateChange(
+ @SessionModificationState int sessionModificationState) {
+ LogUtil.v(
+ "CallCardPresenter.onDialerCallSessionModificationStateChange",
+ "state: " + sessionModificationState);
+
+ if (mPrimary == null) {
+ return;
+ }
+ getUi()
+ .setEndCallButtonEnabled(
+ sessionModificationState
+ != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ true /* shouldAnimate */);
+ updatePrimaryCallState();
+ }
+
+ @Override
+ public void onEnrichedCallStateChanged() {
+ LogUtil.enterBlock("CallCardPresenter.onEnrichedCallStateChanged");
+ updatePrimaryDisplayInfo();
+ }
+
+ private boolean shouldRefreshPrimaryInfo(boolean primaryChanged) {
+ if (mPrimary == null) {
+ return false;
+ }
+ return primaryChanged
+ || mInCallScreen.isManageConferenceVisible() != shouldShowManageConference();
+ }
+
+ private void updatePrimaryCallState() {
+ if (getUi() != null && mPrimary != null) {
+ boolean isWorkCall =
+ mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL)
+ || (mPrimaryContactInfo != null
+ && mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
+ boolean isHdAudioCall =
+ isPrimaryCallActive() && mPrimary.hasProperty(Details.PROPERTY_HIGH_DEF_AUDIO);
+ // Check for video state change and update the visibility of the contact photo. The contact
+ // photo is hidden when the incoming video surface is shown.
+ // The contact photo visibility can also change in setPrimary().
+ boolean shouldShowContactPhoto =
+ !VideoCallPresenter.showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
+ getUi()
+ .setCallState(
+ new PrimaryCallState(
+ mPrimary.getState(),
+ mPrimary.getVideoState(),
+ mPrimary.getSessionModificationState(),
+ mPrimary.getDisconnectCause(),
+ getConnectionLabel(),
+ getCallStateIcon(),
+ getGatewayNumber(),
+ shouldShowCallSubject(mPrimary) ? mPrimary.getCallSubject() : null,
+ mPrimary.getCallbackNumber(),
+ mPrimary.hasProperty(Details.PROPERTY_WIFI),
+ mPrimary.isConferenceCall(),
+ isWorkCall,
+ isHdAudioCall,
+ !TextUtils.isEmpty(mPrimary.getLastForwardedNumber()),
+ shouldShowContactPhoto,
+ mPrimary.getConnectTimeMillis(),
+ CallerInfoUtils.isVoiceMailNumber(mContext, mPrimary),
+ mPrimary.isRemotelyHeld()));
+
+ InCallActivity activity =
+ (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
+ if (activity != null) {
+ activity.onPrimaryCallStateChanged();
+ }
+ }
+ }
+
+ /** Only show the conference call button if we can manage the conference. */
+ private void maybeShowManageConferenceCallButton() {
+ getUi().showManageConferenceCallButton(shouldShowManageConference());
+ }
+
+ /**
+ * Determines if the manage conference button should be visible, based on the current primary
+ * call.
+ *
+ * @return {@code True} if the manage conference button should be visible.
+ */
+ private boolean shouldShowManageConference() {
+ if (mPrimary == null) {
+ return false;
+ }
+
+ return mPrimary.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)
+ && !mIsFullscreen;
+ }
+
+ @Override
+ public void onCallStateButtonClicked() {
+ Intent broadcastIntent = Bindings.get(mContext).getCallStateButtonBroadcastIntent(mContext);
+ if (broadcastIntent != null) {
+ LogUtil.v(
+ "CallCardPresenter.onCallStateButtonClicked",
+ "sending call state button broadcast: " + broadcastIntent);
+ mContext.sendBroadcast(broadcastIntent, Manifest.permission.READ_PHONE_STATE);
+ }
+ }
+
+ @Override
+ public void onManageConferenceClicked() {
+ InCallActivity activity =
+ (InCallActivity) (mInCallScreen.getInCallScreenFragment().getActivity());
+ activity.showConferenceFragment(true);
+ }
+
+ @Override
+ public void onShrinkAnimationComplete() {
+ InCallPresenter.getInstance().onShrinkAnimationComplete();
+ }
+
+ @Override
+ public Drawable getDefaultContactPhotoDrawable() {
+ return ContactInfoCache.getInstance(mContext).getDefaultContactPhotoDrawable();
+ }
+
+ private void maybeStartSearch(DialerCall call, boolean isPrimary) {
+ // no need to start search for conference calls which show generic info.
+ if (call != null && !call.isConferenceCall()) {
+ startContactInfoSearch(call, isPrimary, call.getState() == DialerCall.State.INCOMING);
+ }
+ }
+
+ private void maybeClearSessionModificationState(DialerCall call) {
+ @SessionModificationState int state = call.getSessionModificationState();
+ if (state != DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST
+ && state != DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ LogUtil.i("CallCardPresenter.maybeClearSessionModificationState", "clearing state");
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ /** Starts a query for more contact data for the save primary and secondary calls. */
+ private void startContactInfoSearch(
+ final DialerCall call, final boolean isPrimary, boolean isIncoming) {
+ final ContactInfoCache cache = ContactInfoCache.getInstance(mContext);
+
+ cache.findInfo(call, isIncoming, new ContactLookupCallback(this, isPrimary));
+ }
+
+ private void onContactInfoComplete(String callId, ContactCacheEntry entry, boolean isPrimary) {
+ final boolean entryMatchesExistingCall =
+ (isPrimary && mPrimary != null && TextUtils.equals(callId, mPrimary.getId()))
+ || (!isPrimary && mSecondary != null && TextUtils.equals(callId, mSecondary.getId()));
+ if (entryMatchesExistingCall) {
+ updateContactEntry(entry, isPrimary);
+ } else {
+ LogUtil.e(
+ "CallCardPresenter.onContactInfoComplete",
+ "dropping stale contact lookup info for " + callId);
+ }
+
+ final DialerCall call = CallList.getInstance().getCallById(callId);
+ if (call != null) {
+ call.getLogState().contactLookupResult = entry.contactLookupResult;
+ }
+ if (entry.contactUri != null) {
+ CallerInfoUtils.sendViewNotification(mContext, entry.contactUri);
+ }
+ }
+
+ private void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ if (getUi() == null) {
+ return;
+ }
+
+ if (entry.photo != null) {
+ if (mPrimary != null && callId.equals(mPrimary.getId())) {
+ updateContactEntry(entry, true /* isPrimary */);
+ } else if (mSecondary != null && callId.equals(mSecondary.getId())) {
+ updateContactEntry(entry, false /* isPrimary */);
+ }
+ }
+ }
+
+ private void updateContactEntry(ContactCacheEntry entry, boolean isPrimary) {
+ if (isPrimary) {
+ mPrimaryContactInfo = entry;
+ updatePrimaryDisplayInfo();
+ } else {
+ mSecondaryContactInfo = entry;
+ updateSecondaryDisplayInfo();
+ }
+ }
+
+ /**
+ * Get the highest priority call to display. Goes through the calls and chooses which to return
+ * based on priority of which type of call to display to the user. Callers can use the "ignore"
+ * feature to get the second best call by passing a previously found primary call as ignore.
+ *
+ * @param ignore A call to ignore if found.
+ */
+ private DialerCall getCallToDisplay(
+ CallList callList, DialerCall ignore, boolean skipDisconnected) {
+ // Active calls come second. An active call always gets precedent.
+ DialerCall retval = callList.getActiveCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+
+ // Sometimes there is intemediate state that two calls are in active even one is about
+ // to be on hold.
+ retval = callList.getSecondActiveCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+
+ // Disconnected calls get primary position if there are no active calls
+ // to let user know quickly what call has disconnected. Disconnected
+ // calls are very short lived.
+ if (!skipDisconnected) {
+ retval = callList.getDisconnectingCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+ retval = callList.getDisconnectedCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+ }
+
+ // Then we go to background call (calls on hold)
+ retval = callList.getBackgroundCall();
+ if (retval != null && retval != ignore) {
+ return retval;
+ }
+
+ // Lastly, we go to a second background call.
+ retval = callList.getSecondBackgroundCall();
+
+ return retval;
+ }
+
+ private void updatePrimaryDisplayInfo() {
+ if (mInCallScreen == null) {
+ // TODO: May also occur if search result comes back after ui is destroyed. Look into
+ // removing that case completely.
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "updatePrimaryDisplayInfo called but ui is null!");
+ return;
+ }
+
+ if (mPrimary == null) {
+ // Clear the primary display info.
+ mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
+ return;
+ }
+
+ // Hide the contact photo if we are in a video call and the incoming video surface is
+ // showing.
+ boolean showContactPhoto =
+ !VideoCallPresenter.showIncomingVideo(mPrimary.getVideoState(), mPrimary.getState());
+
+ // DialerCall placed through a work phone account.
+ boolean hasWorkCallProperty = mPrimary.hasProperty(PROPERTY_ENTERPRISE_CALL);
+
+ Session enrichedCallSession =
+ mPrimary.getNumber() == null
+ ? null
+ : EnrichedCallManager.Accessor.getInstance(((Application) mContext))
+ .getSession(mPrimary.getNumber());
+ MultimediaData enrichedCallMultimediaData =
+ enrichedCallSession == null ? null : enrichedCallSession.getMultimediaData();
+
+ if (mPrimary.isConferenceCall()) {
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "update primary display info for conference call.");
+
+ mInCallScreen.setPrimary(
+ new PrimaryInfo(
+ null /* number */,
+ getConferenceString(mPrimary),
+ false /* nameIsNumber */,
+ null /* location */,
+ null /* label */,
+ getConferencePhoto(mPrimary),
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ false /* isSipCall */,
+ showContactPhoto,
+ hasWorkCallProperty,
+ false /* isSpam */,
+ false /* answeringDisconnectsOngoingCall */,
+ shouldShowLocation(),
+ null /* contactInfoLookupKey */,
+ null /* enrichedCallMultimediaData */));
+ } else if (mPrimaryContactInfo != null) {
+ LogUtil.v(
+ "CallCardPresenter.updatePrimaryDisplayInfo",
+ "update primary display info for " + mPrimaryContactInfo);
+
+ String name = getNameForCall(mPrimaryContactInfo);
+ String number;
+
+ boolean isChildNumberShown = !TextUtils.isEmpty(mPrimary.getChildNumber());
+ boolean isForwardedNumberShown = !TextUtils.isEmpty(mPrimary.getLastForwardedNumber());
+ boolean isCallSubjectShown = shouldShowCallSubject(mPrimary);
+
+ if (isCallSubjectShown) {
+ number = null;
+ } else if (isChildNumberShown) {
+ number = mContext.getString(R.string.child_number, mPrimary.getChildNumber());
+ } else if (isForwardedNumberShown) {
+ // Use last forwarded number instead of second line, if present.
+ number = mPrimary.getLastForwardedNumber();
+ } else {
+ number = mPrimaryContactInfo.number;
+ }
+
+ boolean nameIsNumber = name != null && name.equals(mPrimaryContactInfo.number);
+ // DialerCall with caller that is a work contact.
+ boolean isWorkContact = (mPrimaryContactInfo.userType == ContactsUtils.USER_TYPE_WORK);
+ mInCallScreen.setPrimary(
+ new PrimaryInfo(
+ number,
+ name,
+ nameIsNumber,
+ mPrimaryContactInfo.location,
+ isChildNumberShown || isCallSubjectShown ? null : mPrimaryContactInfo.label,
+ mPrimaryContactInfo.photo,
+ mPrimaryContactInfo.photoType,
+ mPrimaryContactInfo.isSipCall,
+ showContactPhoto,
+ hasWorkCallProperty || isWorkContact,
+ mPrimary.isSpam(),
+ mPrimary.answeringDisconnectsForegroundVideoCall(),
+ shouldShowLocation(),
+ mPrimaryContactInfo.lookupKey,
+ enrichedCallMultimediaData));
+ } else {
+ // Clear the primary display info.
+ mInCallScreen.setPrimary(PrimaryInfo.createEmptyPrimaryInfo());
+ }
+
+ mInCallScreen.showLocationUi(null);
+ }
+
+ private boolean shouldShowLocation() {
+ if (isOutgoingEmergencyCall(mPrimary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "new emergency call");
+ return true;
+ } else if (isIncomingEmergencyCall(mPrimary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "potential emergency callback");
+ return true;
+ } else if (isIncomingEmergencyCall(mSecondary)) {
+ LogUtil.i("CallCardPresenter.shouldShowLocation", "has potential emergency callback");
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isOutgoingEmergencyCall(@Nullable DialerCall call) {
+ return call != null && !call.isIncoming() && call.isEmergencyCall();
+ }
+
+ private static boolean isIncomingEmergencyCall(@Nullable DialerCall call) {
+ return call != null && call.isIncoming() && call.isPotentialEmergencyCallback();
+ }
+
+ private boolean hasLocationPermission() {
+ return ContextCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private boolean isBatteryTooLowForEmergencyLocation() {
+ Intent batteryStatus =
+ mContext.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ if (status == BatteryManager.BATTERY_STATUS_CHARGING
+ || status == BatteryManager.BATTERY_STATUS_FULL) {
+ // Plugged in or full battery
+ return false;
+ }
+ int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
+ int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
+ float batteryPercent = (100f * level) / scale;
+ long threshold =
+ ConfigProviderBindings.get(mContext)
+ .getLong(
+ CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION,
+ CONFIG_MIN_BATTERY_PERCENT_FOR_EMERGENCY_LOCATION_DEFAULT);
+ LogUtil.i(
+ "CallCardPresenter.isBatteryTooLowForEmergencyLocation",
+ "percent charged: " + batteryPercent + ", min required charge: " + threshold);
+ return batteryPercent < threshold;
+ }
+
+ private void updateSecondaryDisplayInfo() {
+ if (mInCallScreen == null) {
+ return;
+ }
+
+ if (mSecondary == null) {
+ // Clear the secondary display info.
+ mInCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(mIsFullscreen));
+ return;
+ }
+
+ if (mSecondary.isConferenceCall()) {
+ mInCallScreen.setSecondary(
+ new SecondaryInfo(
+ true /* show */,
+ getConferenceString(mSecondary),
+ false /* nameIsNumber */,
+ null /* label */,
+ mSecondary.getCallProviderLabel(),
+ true /* isConference */,
+ mSecondary.isVideoCall(),
+ mIsFullscreen));
+ } else if (mSecondaryContactInfo != null) {
+ LogUtil.v("CallCardPresenter.updateSecondaryDisplayInfo", "" + mSecondaryContactInfo);
+ String name = getNameForCall(mSecondaryContactInfo);
+ boolean nameIsNumber = name != null && name.equals(mSecondaryContactInfo.number);
+ mInCallScreen.setSecondary(
+ new SecondaryInfo(
+ true /* show */,
+ name,
+ nameIsNumber,
+ mSecondaryContactInfo.label,
+ mSecondary.getCallProviderLabel(),
+ false /* isConference */,
+ mSecondary.isVideoCall(),
+ mIsFullscreen));
+ } else {
+ // Clear the secondary display info.
+ mInCallScreen.setSecondary(SecondaryInfo.createEmptySecondaryInfo(mIsFullscreen));
+ }
+ }
+
+ /** Returns the gateway number for any existing outgoing call. */
+ private String getGatewayNumber() {
+ if (hasOutgoingGatewayCall()) {
+ return DialerCall.getNumberFromHandle(mPrimary.getGatewayInfo().getGatewayAddress());
+ }
+ return null;
+ }
+
+ /**
+ * Returns the label (line of text above the number/name) for any given call. For example,
+ * "calling via [Account/Google Voice]" for outgoing calls.
+ */
+ private String getConnectionLabel() {
+ if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ return null;
+ }
+ StatusHints statusHints = mPrimary.getStatusHints();
+ if (statusHints != null && !TextUtils.isEmpty(statusHints.getLabel())) {
+ return statusHints.getLabel().toString();
+ }
+
+ if (hasOutgoingGatewayCall() && getUi() != null) {
+ // Return the label for the gateway app on outgoing calls.
+ final PackageManager pm = mContext.getPackageManager();
+ try {
+ ApplicationInfo info =
+ pm.getApplicationInfo(mPrimary.getGatewayInfo().getGatewayProviderPackageName(), 0);
+ return pm.getApplicationLabel(info).toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ LogUtil.e("CallCardPresenter.getConnectionLabel", "gateway Application Not Found.", e);
+ return null;
+ }
+ }
+ return mPrimary.getCallProviderLabel();
+ }
+
+ private Drawable getCallStateIcon() {
+ // Return connection icon if one exists.
+ StatusHints statusHints = mPrimary.getStatusHints();
+ if (statusHints != null && statusHints.getIcon() != null) {
+ Drawable icon = statusHints.getIcon().loadDrawable(mContext);
+ if (icon != null) {
+ return icon;
+ }
+ }
+
+ return null;
+ }
+
+ private boolean hasOutgoingGatewayCall() {
+ // We only display the gateway information while STATE_DIALING so return false for any other
+ // call state.
+ // TODO: mPrimary can be null because this is called from updatePrimaryDisplayInfo which
+ // is also called after a contact search completes (call is not present yet). Split the
+ // UI update so it can receive independent updates.
+ if (mPrimary == null) {
+ return false;
+ }
+ return DialerCall.State.isDialing(mPrimary.getState())
+ && mPrimary.getGatewayInfo() != null
+ && !mPrimary.getGatewayInfo().isEmpty();
+ }
+
+ /** Gets the name to display for the call. */
+ String getNameForCall(ContactCacheEntry contactInfo) {
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return contactInfo.number;
+ }
+ return preferredName;
+ }
+
+ /** Gets the number to display for a call. */
+ String getNumberForCall(ContactCacheEntry contactInfo) {
+ // If the name is empty, we use the number for the name...so don't show a second
+ // number in the number field
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return contactInfo.location;
+ }
+ return contactInfo.number;
+ }
+
+ @Override
+ public void onSecondaryInfoClicked() {
+ if (mSecondary == null) {
+ LogUtil.e(
+ "CallCardPresenter.onSecondaryInfoClicked",
+ "secondary info clicked but no secondary call.");
+ return;
+ }
+
+ LogUtil.i(
+ "CallCardPresenter.onSecondaryInfoClicked", "swapping call to foreground: " + mSecondary);
+ mSecondary.unhold();
+ }
+
+ @Override
+ public void onEndCallClicked() {
+ LogUtil.i("CallCardPresenter.onEndCallClicked", "disconnecting call: " + mPrimary);
+ if (mPrimary != null) {
+ mPrimary.disconnect();
+ }
+ }
+
+ /**
+ * Handles a change to the fullscreen mode of the in-call UI.
+ *
+ * @param isFullscreenMode {@code True} if the in-call UI is entering full screen mode.
+ */
+ @Override
+ public void onFullscreenModeChanged(boolean isFullscreenMode) {
+ mIsFullscreen = isFullscreenMode;
+ if (mInCallScreen == null) {
+ return;
+ }
+ maybeShowManageConferenceCallButton();
+ }
+
+ private boolean isPrimaryCallActive() {
+ return mPrimary != null && mPrimary.getState() == DialerCall.State.ACTIVE;
+ }
+
+ private String getConferenceString(DialerCall call) {
+ boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
+ LogUtil.v("CallCardPresenter.getConferenceString", "" + isGenericConference);
+
+ final int resId =
+ isGenericConference ? R.string.generic_conference_call_name : R.string.conference_call_name;
+ return mContext.getResources().getString(resId);
+ }
+
+ private Drawable getConferencePhoto(DialerCall call) {
+ boolean isGenericConference = call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE);
+ LogUtil.v("CallCardPresenter.getConferencePhoto", "" + isGenericConference);
+
+ final int resId = isGenericConference ? R.drawable.img_phone : R.drawable.img_conference;
+ Drawable photo = mContext.getResources().getDrawable(resId);
+ photo.setAutoMirrored(true);
+ return photo;
+ }
+
+ private boolean shouldShowEndCallButton(DialerCall primary, int callState) {
+ if (primary == null) {
+ return false;
+ }
+ if ((!DialerCall.State.isConnectingOrConnected(callState)
+ && callState != DialerCall.State.DISCONNECTING
+ && callState != DialerCall.State.DISCONNECTED)
+ || callState == DialerCall.State.INCOMING) {
+ return false;
+ }
+ if (mPrimary.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void onInCallScreenResumed() {
+ if (shouldSendAccessibilityEvent) {
+ handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
+ }
+ }
+
+ static boolean sendAccessibilityEvent(Context context, InCallScreen inCallScreen) {
+ AccessibilityManager am =
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "accessibility is off");
+ return false;
+ }
+ if (inCallScreen == null) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "incallscreen is null");
+ return false;
+ }
+ Fragment fragment = inCallScreen.getInCallScreenFragment();
+ if (fragment == null || fragment.getView() == null || fragment.getView().getParent() == null) {
+ LogUtil.w("CallCardPresenter.sendAccessibilityEvent", "fragment/view/parent is null");
+ return false;
+ }
+
+ DisplayManager displayManager =
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ Display display = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
+ boolean screenIsOn = display.getState() == Display.STATE_ON;
+ LogUtil.d("CallCardPresenter.sendAccessibilityEvent", "screen is on: %b", screenIsOn);
+ if (!screenIsOn) {
+ return false;
+ }
+
+ AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT);
+ inCallScreen.dispatchPopulateAccessibilityEvent(event);
+ View view = inCallScreen.getInCallScreenFragment().getView();
+ view.getParent().requestSendAccessibilityEvent(view, event);
+ return true;
+ }
+
+ private void maybeSendAccessibilityEvent(
+ InCallState oldState, final InCallState newState, boolean primaryChanged) {
+ shouldSendAccessibilityEvent = false;
+ if (mContext == null) {
+ return;
+ }
+ final AccessibilityManager am =
+ (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (!am.isEnabled()) {
+ return;
+ }
+ // Announce the current call if it's new incoming/outgoing call or primary call is changed
+ // due to switching calls between two ongoing calls (one is on hold).
+ if ((oldState != InCallState.OUTGOING && newState == InCallState.OUTGOING)
+ || (oldState != InCallState.INCOMING && newState == InCallState.INCOMING)
+ || primaryChanged) {
+ LogUtil.i(
+ "CallCardPresenter.maybeSendAccessibilityEvent", "schedule accessibility announcement");
+ shouldSendAccessibilityEvent = true;
+ handler.postDelayed(sendAccessibilityEventRunnable, ACCESSIBILITY_ANNOUNCEMENT_DELAY_MILLIS);
+ }
+ }
+
+ /**
+ * Determines whether the call subject should be visible on the UI. For the call subject to be
+ * visible, the call has to be in an incoming or waiting state, and the subject must not be empty.
+ *
+ * @param call The call.
+ * @return {@code true} if the subject should be shown, {@code false} otherwise.
+ */
+ private boolean shouldShowCallSubject(DialerCall call) {
+ if (call == null) {
+ return false;
+ }
+
+ boolean isIncomingOrWaiting =
+ mPrimary.getState() == DialerCall.State.INCOMING
+ || mPrimary.getState() == DialerCall.State.CALL_WAITING;
+ return isIncomingOrWaiting
+ && !TextUtils.isEmpty(call.getCallSubject())
+ && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED
+ && call.isCallSubjectSupported();
+ }
+
+ /**
+ * Determines whether the "note sent" toast should be shown. It should be shown for a new outgoing
+ * call with a subject.
+ *
+ * @param call The call
+ * @return {@code true} if the toast should be shown, {@code false} otherwise.
+ */
+ private boolean shouldShowNoteSentToast(DialerCall call) {
+ return call != null
+ && hasCallSubject(call)
+ && (call.getState() == DialerCall.State.DIALING
+ || call.getState() == DialerCall.State.CONNECTING);
+ }
+
+ private InCallScreen getUi() {
+ return mInCallScreen;
+ }
+
+ public static class ContactLookupCallback implements ContactInfoCacheCallback {
+
+ private final WeakReference<CallCardPresenter> mCallCardPresenter;
+ private final boolean mIsPrimary;
+
+ public ContactLookupCallback(CallCardPresenter callCardPresenter, boolean isPrimary) {
+ mCallCardPresenter = new WeakReference<CallCardPresenter>(callCardPresenter);
+ mIsPrimary = isPrimary;
+ }
+
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ CallCardPresenter presenter = mCallCardPresenter.get();
+ if (presenter != null) {
+ presenter.onContactInfoComplete(callId, entry, mIsPrimary);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ CallCardPresenter presenter = mCallCardPresenter.get();
+ if (presenter != null) {
+ presenter.onImageLoadComplete(callId, entry);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/CallerInfo.java b/java/com/android/incallui/CallerInfo.java
new file mode 100644
index 000000000..473bb8f4e
--- /dev/null
+++ b/java/com/android/incallui/CallerInfo.java
@@ -0,0 +1,573 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.RawContacts;
+import android.support.annotation.RequiresApi;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.util.TelephonyManagerUtils;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumbercache.PhoneLookupUtil;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+/**
+ * Looks up caller information for the given phone number. This is intermediate data and should NOT
+ * be used by any UI.
+ */
+public class CallerInfo {
+
+ private static final String TAG = "CallerInfo";
+
+ // We should always use this projection starting from N onward.
+ @RequiresApi(VERSION_CODES.N)
+ private static final String[] DEFAULT_PHONELOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup.CONTACT_ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.LABEL,
+ PhoneLookup.TYPE,
+ PhoneLookup.PHOTO_URI,
+ PhoneLookup.CUSTOM_RINGTONE,
+ PhoneLookup.SEND_TO_VOICEMAIL
+ };
+
+ // In pre-N, contact id is stored in {@link PhoneLookup._ID} in non-sip query.
+ private static final String[] BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION =
+ new String[] {
+ PhoneLookup._ID,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup.LOOKUP_KEY,
+ PhoneLookup.NUMBER,
+ PhoneLookup.NORMALIZED_NUMBER,
+ PhoneLookup.LABEL,
+ PhoneLookup.TYPE,
+ PhoneLookup.PHOTO_URI,
+ PhoneLookup.CUSTOM_RINGTONE,
+ PhoneLookup.SEND_TO_VOICEMAIL
+ };
+ /**
+ * Please note that, any one of these member variables can be null, and any accesses to them
+ * should be prepared to handle such a case.
+ *
+ * <p>Also, it is implied that phoneNumber is more often populated than name is, (think of calls
+ * being dialed/received using numbers where names are not known to the device), so phoneNumber
+ * should serve as a dependable fallback when name is unavailable.
+ *
+ * <p>One other detail here is that this CallerInfo object reflects information found on a
+ * connection, it is an OUTPUT that serves mainly to display information to the user. In no way is
+ * this object used as input to make a connection, so we can choose to display whatever
+ * human-readable text makes sense to the user for a connection. This is especially relevant for
+ * the phone number field, since it is the one field that is most likely exposed to the user.
+ *
+ * <p>As an example: 1. User dials "911" 2. Device recognizes that this is an emergency number 3.
+ * We use the "Emergency Number" string instead of "911" in the phoneNumber field.
+ *
+ * <p>What we're really doing here is treating phoneNumber as an essential field here, NOT name.
+ * We're NOT always guaranteed to have a name for a connection, but the number should be
+ * displayable.
+ */
+ public String name;
+
+ public String nameAlternative;
+ public String phoneNumber;
+ public String normalizedNumber;
+ public String forwardingNumber;
+ public String geoDescription;
+ public String cnapName;
+ public int numberPresentation;
+ public int namePresentation;
+ public boolean contactExists;
+ public String phoneLabel;
+ /* Split up the phoneLabel into number type and label name */
+ public int numberType;
+ public String numberLabel;
+ public int photoResource;
+ // Contact ID, which will be 0 if a contact comes from the corp CP2.
+ public long contactIdOrZero;
+ public String lookupKeyOrNull;
+ public boolean needUpdate;
+ public Uri contactRefUri;
+ public @UserType long userType;
+ /**
+ * Contact display photo URI. If a contact has no display photo but a thumbnail, it'll be the
+ * thumbnail URI instead.
+ */
+ public Uri contactDisplayPhotoUri;
+ // fields to hold individual contact preference data,
+ // including the send to voicemail flag and the ringtone
+ // uri reference.
+ public Uri contactRingtoneUri;
+ public boolean shouldSendToVoicemail;
+ /**
+ * Drawable representing the caller image. This is essentially a cache for the image data tied
+ * into the connection / callerinfo object.
+ *
+ * <p>This might be a high resolution picture which is more suitable for full-screen image view
+ * than for smaller icons used in some kinds of notifications.
+ *
+ * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
+ */
+ public Drawable cachedPhoto;
+ /**
+ * Bitmap representing the caller image which has possibly lower resolution than {@link
+ * #cachedPhoto} and thus more suitable for icons (like notification icons).
+ *
+ * <p>In usual cases this is just down-scaled image of {@link #cachedPhoto}. If the down-scaling
+ * fails, this will just become null.
+ *
+ * <p>The {@link #isCachedPhotoCurrent} flag indicates if the image data needs to be reloaded.
+ */
+ public Bitmap cachedPhotoIcon;
+ /**
+ * Boolean which indicates if {@link #cachedPhoto} and {@link #cachedPhotoIcon} is fresh enough.
+ * If it is false, those images aren't pointing to valid objects.
+ */
+ public boolean isCachedPhotoCurrent;
+ /**
+ * String which holds the call subject sent as extra from the lower layers for this call. This is
+ * used to display the no-caller ID reason for restricted/unknown number presentation.
+ */
+ public String callSubject;
+
+ private boolean mIsEmergency;
+ private boolean mIsVoiceMail;
+
+ public CallerInfo() {
+ // TODO: Move all the basic initialization here?
+ mIsEmergency = false;
+ mIsVoiceMail = false;
+ userType = ContactsUtils.USER_TYPE_CURRENT;
+ }
+
+ public static String[] getDefaultPhoneLookupProjection(Uri phoneLookupUri) {
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ return DEFAULT_PHONELOOKUP_PROJECTION;
+ }
+ // Pre-N
+ boolean isSip =
+ phoneLookupUri.getBooleanQueryParameter(
+ ContactsContract.PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false);
+ return (isSip)
+ ? DEFAULT_PHONELOOKUP_PROJECTION
+ : BACKWARD_COMPATIBLE_NON_SIP_DEFAULT_PHONELOOKUP_PROJECTION;
+ }
+
+ /**
+ * getCallerInfo given a Cursor.
+ *
+ * @param context the context used to retrieve string constants
+ * @param contactRef the URI to attach to this CallerInfo object
+ * @param cursor the first object in the cursor is used to build the CallerInfo object.
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied.
+ */
+ public static CallerInfo getCallerInfo(Context context, Uri contactRef, Cursor cursor) {
+ CallerInfo info = new CallerInfo();
+ info.photoResource = 0;
+ info.phoneLabel = null;
+ info.numberType = 0;
+ info.numberLabel = null;
+ info.cachedPhoto = null;
+ info.isCachedPhotoCurrent = false;
+ info.contactExists = false;
+ info.userType = ContactsUtils.USER_TYPE_CURRENT;
+
+ Log.v(TAG, "getCallerInfo() based on cursor...");
+
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ // TODO: photo_id is always available but not taken
+ // care of here. Maybe we should store it in the
+ // CallerInfo object as well.
+
+ long contactId = 0L;
+ int columnIndex;
+
+ // Look for the name
+ columnIndex = cursor.getColumnIndex(PhoneLookup.DISPLAY_NAME);
+ if (columnIndex != -1) {
+ info.name = cursor.getString(columnIndex);
+ }
+
+ // Look for the number
+ columnIndex = cursor.getColumnIndex(PhoneLookup.NUMBER);
+ if (columnIndex != -1) {
+ info.phoneNumber = cursor.getString(columnIndex);
+ }
+
+ // Look for the normalized number
+ columnIndex = cursor.getColumnIndex(PhoneLookup.NORMALIZED_NUMBER);
+ if (columnIndex != -1) {
+ info.normalizedNumber = cursor.getString(columnIndex);
+ }
+
+ // Look for the label/type combo
+ columnIndex = cursor.getColumnIndex(PhoneLookup.LABEL);
+ if (columnIndex != -1) {
+ int typeColumnIndex = cursor.getColumnIndex(PhoneLookup.TYPE);
+ if (typeColumnIndex != -1) {
+ info.numberType = cursor.getInt(typeColumnIndex);
+ info.numberLabel = cursor.getString(columnIndex);
+ info.phoneLabel =
+ Phone.getTypeLabel(context.getResources(), info.numberType, info.numberLabel)
+ .toString();
+ }
+ }
+
+ // cache the lookup key for later use to create lookup URIs
+ columnIndex = cursor.getColumnIndex(PhoneLookup.LOOKUP_KEY);
+ if (columnIndex != -1) {
+ info.lookupKeyOrNull = cursor.getString(columnIndex);
+ }
+
+ // Look for the person_id.
+ columnIndex = getColumnIndexForPersonId(contactRef, cursor);
+ if (columnIndex != -1) {
+ contactId = cursor.getLong(columnIndex);
+ // QuickContacts in M doesn't support enterprise contact id
+ if (contactId != 0
+ && (VERSION.SDK_INT >= VERSION_CODES.N
+ || !Contacts.isEnterpriseContactId(contactId))) {
+ info.contactIdOrZero = contactId;
+ Log.v(TAG, "==> got info.contactIdOrZero: " + info.contactIdOrZero);
+ }
+ } else {
+ // No valid columnIndex, so we can't look up person_id.
+ Log.v(TAG, "Couldn't find contactId column for " + contactRef);
+ // Watch out: this means that anything that depends on
+ // person_id will be broken (like contact photo lookups in
+ // the in-call UI, for example.)
+ }
+
+ // Display photo URI.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.PHOTO_URI);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ info.contactDisplayPhotoUri = Uri.parse(cursor.getString(columnIndex));
+ } else {
+ info.contactDisplayPhotoUri = null;
+ }
+
+ // look for the custom ringtone, create from the string stored
+ // in the database.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.CUSTOM_RINGTONE);
+ if ((columnIndex != -1) && (cursor.getString(columnIndex) != null)) {
+ if (TextUtils.isEmpty(cursor.getString(columnIndex))) {
+ // make it consistent with frameworks/base/.../CallerInfo.java
+ info.contactRingtoneUri = Uri.EMPTY;
+ } else {
+ info.contactRingtoneUri = Uri.parse(cursor.getString(columnIndex));
+ }
+ } else {
+ info.contactRingtoneUri = null;
+ }
+
+ // look for the send to voicemail flag, set it to true only
+ // under certain circumstances.
+ columnIndex = cursor.getColumnIndex(PhoneLookup.SEND_TO_VOICEMAIL);
+ info.shouldSendToVoicemail = (columnIndex != -1) && ((cursor.getInt(columnIndex)) == 1);
+ info.contactExists = true;
+
+ // Determine userType by directoryId and contactId
+ final String directory =
+ contactRef == null
+ ? null
+ : contactRef.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ Long directoryId = null;
+ if (directory != null) {
+ try {
+ directoryId = Long.parseLong(directory);
+ } catch (NumberFormatException e) {
+ // do nothing
+ }
+ }
+ info.userType = ContactsUtils.determineUserType(directoryId, contactId);
+
+ info.nameAlternative =
+ ContactInfoHelper.lookUpDisplayNameAlternative(
+ context, info.lookupKeyOrNull, info.userType, directoryId);
+ }
+ cursor.close();
+ }
+
+ info.needUpdate = false;
+ info.name = normalize(info.name);
+ info.contactRefUri = contactRef;
+
+ return info;
+ }
+
+ /**
+ * getCallerInfo given a URI, look up in the call-log database for the uri unique key.
+ *
+ * @param context the context used to get the ContentResolver
+ * @param contactRef the URI used to lookup caller id
+ * @return the CallerInfo which contains the caller id for the given number. The returned
+ * CallerInfo is null if no number is supplied.
+ */
+ private static CallerInfo getCallerInfo(Context context, Uri contactRef) {
+
+ return getCallerInfo(
+ context,
+ contactRef,
+ context.getContentResolver().query(contactRef, null, null, null, null));
+ }
+
+ /**
+ * Performs another lookup if previous lookup fails and it's a SIP call and the peer's username is
+ * all numeric. Look up the username as it could be a PSTN number in the contact database.
+ *
+ * @param context the query context
+ * @param number the original phone number, could be a SIP URI
+ * @param previousResult the result of previous lookup
+ * @return previousResult if it's not the case
+ */
+ static CallerInfo doSecondaryLookupIfNecessary(
+ Context context, String number, CallerInfo previousResult) {
+ if (!previousResult.contactExists && PhoneNumberHelper.isUriNumber(number)) {
+ String username = PhoneNumberHelper.getUsernameFromUriNumber(number);
+ if (PhoneNumberUtils.isGlobalPhoneNumber(username)) {
+ previousResult =
+ getCallerInfo(
+ context,
+ Uri.withAppendedPath(
+ PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(username)));
+ }
+ }
+ return previousResult;
+ }
+
+ // Accessors
+
+ private static String normalize(String s) {
+ if (s == null || s.length() > 0) {
+ return s;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the column index to use to find the "person_id" field in the specified cursor, based on
+ * the contact URI that was originally queried.
+ *
+ * <p>This is a helper function for the getCallerInfo() method that takes a Cursor. Looking up the
+ * person_id is nontrivial (compared to all the other CallerInfo fields) since the column we need
+ * to use depends on what query we originally ran.
+ *
+ * <p>Watch out: be sure to not do any database access in this method, since it's run from the UI
+ * thread (see comments below for more info.)
+ *
+ * @return the columnIndex to use (with cursor.getLong()) to get the person_id, or -1 if we
+ * couldn't figure out what colum to use.
+ * <p>TODO: Add a unittest for this method. (This is a little tricky to test, since we'll need
+ * a live contacts database to test against, preloaded with at least some phone numbers and
+ * SIP addresses. And we'll probably have to hardcode the column indexes we expect, so the
+ * test might break whenever the contacts schema changes. But we can at least make sure we
+ * handle all the URI patterns we claim to, and that the mime types match what we expect...)
+ */
+ private static int getColumnIndexForPersonId(Uri contactRef, Cursor cursor) {
+ // TODO: This is pretty ugly now, see bug 2269240 for
+ // more details. The column to use depends upon the type of URL:
+ // - content://com.android.contacts/data/phones ==> use the "contact_id" column
+ // - content://com.android.contacts/phone_lookup ==> use the "_ID" column
+ // - content://com.android.contacts/data ==> use the "contact_id" column
+ // If it's none of the above, we leave columnIndex=-1 which means
+ // that the person_id field will be left unset.
+ //
+ // The logic here *used* to be based on the mime type of contactRef
+ // (for example Phone.CONTENT_ITEM_TYPE would tell us to use the
+ // RawContacts.CONTACT_ID column). But looking up the mime type requires
+ // a call to context.getContentResolver().getType(contactRef), which
+ // isn't safe to do from the UI thread since it can cause an ANR if
+ // the contacts provider is slow or blocked (like during a sync.)
+ //
+ // So instead, figure out the column to use for person_id by just
+ // looking at the URI itself.
+
+ Log.v(TAG, "- getColumnIndexForPersonId: contactRef URI = '" + contactRef + "'...");
+ // Warning: Do not enable the following logging (due to ANR risk.)
+ // if (VDBG) Rlog.v(TAG, "- MIME type: "
+ // + context.getContentResolver().getType(contactRef));
+
+ String url = contactRef.toString();
+ String columnName = null;
+ if (url.startsWith("content://com.android.contacts/data/phones")) {
+ // Direct lookup in the Phone table.
+ // MIME type: Phone.CONTENT_ITEM_TYPE (= "vnd.android.cursor.item/phone_v2")
+ Log.v(TAG, "'data/phones' URI; using RawContacts.CONTACT_ID");
+ columnName = RawContacts.CONTACT_ID;
+ } else if (url.startsWith("content://com.android.contacts/data")) {
+ // Direct lookup in the Data table.
+ // MIME type: Data.CONTENT_TYPE (= "vnd.android.cursor.dir/data")
+ Log.v(TAG, "'data' URI; using Data.CONTACT_ID");
+ // (Note Data.CONTACT_ID and RawContacts.CONTACT_ID are equivalent.)
+ columnName = Data.CONTACT_ID;
+ } else if (url.startsWith("content://com.android.contacts/phone_lookup")) {
+ // Lookup in the PhoneLookup table, which provides "fuzzy matching"
+ // for phone numbers.
+ // MIME type: PhoneLookup.CONTENT_TYPE (= "vnd.android.cursor.dir/phone_lookup")
+ Log.v(TAG, "'phone_lookup' URI; using PhoneLookup._ID");
+ columnName = PhoneLookupUtil.getContactIdColumnNameForUri(contactRef);
+ } else {
+ Log.v(TAG, "Unexpected prefix for contactRef '" + url + "'");
+ }
+ int columnIndex = (columnName != null) ? cursor.getColumnIndex(columnName) : -1;
+ Log.v(
+ TAG,
+ "==> Using column '"
+ + columnName
+ + "' (columnIndex = "
+ + columnIndex
+ + ") for person_id lookup...");
+ return columnIndex;
+ }
+
+ /** @return true if the caller info is an emergency number. */
+ public boolean isEmergencyNumber() {
+ return mIsEmergency;
+ }
+
+ /** @return true if the caller info is a voicemail number. */
+ public boolean isVoiceMailNumber() {
+ return mIsVoiceMail;
+ }
+
+ /**
+ * Mark this CallerInfo as an emergency call.
+ *
+ * @param context To lookup the localized 'Emergency Number' string.
+ * @return this instance.
+ */
+ /* package */ CallerInfo markAsEmergency(Context context) {
+ name = context.getString(R.string.emergency_call_dialog_number_for_display);
+ phoneNumber = null;
+
+ photoResource = R.drawable.img_phone;
+ mIsEmergency = true;
+ return this;
+ }
+
+ /**
+ * Mark this CallerInfo as a voicemail call. The voicemail label is obtained from the telephony
+ * manager. Caller must hold the READ_PHONE_STATE permission otherwise the phoneNumber will be set
+ * to null.
+ *
+ * @return this instance.
+ */
+ /* package */ CallerInfo markAsVoiceMail(Context context) {
+ mIsVoiceMail = true;
+
+ try {
+ // For voicemail calls, we display the voice mail tag
+ // instead of the real phone number in the "number"
+ // field.
+ name = TelephonyManagerUtils.getVoiceMailAlphaTag(context);
+ phoneNumber = null;
+ } catch (SecurityException se) {
+ // Should never happen: if this process does not have
+ // permission to retrieve VM tag, it should not have
+ // permission to retrieve VM number and would not call
+ // this method.
+ // Leave phoneNumber untouched.
+ Log.e(TAG, "Cannot access VoiceMail.", se);
+ }
+ // TODO: There is no voicemail picture?
+ // FIXME: FIND ANOTHER ICON
+ // photoResource = android.R.drawable.badge_voicemail;
+ return this;
+ }
+
+ /**
+ * Updates this CallerInfo's geoDescription field, based on the raw phone number in the
+ * phoneNumber field.
+ *
+ * <p>(Note that the various getCallerInfo() methods do *not* set the geoDescription
+ * automatically; you need to call this method explicitly to get it.)
+ *
+ * @param context the context used to look up the current locale / country
+ * @param fallbackNumber if this CallerInfo's phoneNumber field is empty, this specifies a
+ * fallback number to use instead.
+ */
+ public void updateGeoDescription(Context context, String fallbackNumber) {
+ String number = TextUtils.isEmpty(phoneNumber) ? fallbackNumber : phoneNumber;
+ geoDescription = PhoneNumberHelper.getGeoDescription(context, number);
+ }
+
+ /** @return a string debug representation of this instance. */
+ @Override
+ public String toString() {
+ // Warning: never check in this file with VERBOSE_DEBUG = true
+ // because that will result in PII in the system log.
+ final boolean VERBOSE_DEBUG = false;
+
+ if (VERBOSE_DEBUG) {
+ return new StringBuilder(384)
+ .append(super.toString() + " { ")
+ .append("\nname: " + name)
+ .append("\nphoneNumber: " + phoneNumber)
+ .append("\nnormalizedNumber: " + normalizedNumber)
+ .append("\forwardingNumber: " + forwardingNumber)
+ .append("\ngeoDescription: " + geoDescription)
+ .append("\ncnapName: " + cnapName)
+ .append("\nnumberPresentation: " + numberPresentation)
+ .append("\nnamePresentation: " + namePresentation)
+ .append("\ncontactExists: " + contactExists)
+ .append("\nphoneLabel: " + phoneLabel)
+ .append("\nnumberType: " + numberType)
+ .append("\nnumberLabel: " + numberLabel)
+ .append("\nphotoResource: " + photoResource)
+ .append("\ncontactIdOrZero: " + contactIdOrZero)
+ .append("\nneedUpdate: " + needUpdate)
+ .append("\ncontactRefUri: " + contactRefUri)
+ .append("\ncontactRingtoneUri: " + contactRingtoneUri)
+ .append("\ncontactDisplayPhotoUri: " + contactDisplayPhotoUri)
+ .append("\nshouldSendToVoicemail: " + shouldSendToVoicemail)
+ .append("\ncachedPhoto: " + cachedPhoto)
+ .append("\nisCachedPhotoCurrent: " + isCachedPhotoCurrent)
+ .append("\nemergency: " + mIsEmergency)
+ .append("\nvoicemail: " + mIsVoiceMail)
+ .append("\nuserType: " + userType)
+ .append(" }")
+ .toString();
+ } else {
+ return new StringBuilder(128)
+ .append(super.toString() + " { ")
+ .append("name " + ((name == null) ? "null" : "non-null"))
+ .append(", phoneNumber " + ((phoneNumber == null) ? "null" : "non-null"))
+ .append(" }")
+ .toString();
+ }
+ }
+}
diff --git a/java/com/android/incallui/CallerInfoAsyncQuery.java b/java/com/android/incallui/CallerInfoAsyncQuery.java
new file mode 100644
index 000000000..f8d7ac65a
--- /dev/null
+++ b/java/com/android/incallui/CallerInfoAsyncQuery.java
@@ -0,0 +1,638 @@
+/*
+ * 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.incallui;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.MainThread;
+import android.support.annotation.RequiresPermission;
+import android.support.annotation.WorkerThread;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.DirectoryCompat;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Helper class to make it easier to run asynchronous caller-id lookup queries.
+ *
+ * @see CallerInfo
+ */
+@TargetApi(VERSION_CODES.M)
+public class CallerInfoAsyncQuery {
+
+ /** Interface for a CallerInfoAsyncQueryHandler result return. */
+ public interface OnQueryCompleteListener {
+
+ /** Called when the query is complete. */
+ @MainThread
+ void onQueryComplete(int token, Object cookie, CallerInfo ci);
+
+ /** Called when data is loaded. Must be called in worker thread. */
+ @WorkerThread
+ void onDataLoaded(int token, Object cookie, CallerInfo ci);
+ }
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "CallerInfoAsyncQuery";
+
+ private static final int EVENT_NEW_QUERY = 1;
+ private static final int EVENT_ADD_LISTENER = 2;
+ private static final int EVENT_EMERGENCY_NUMBER = 3;
+ private static final int EVENT_VOICEMAIL_NUMBER = 4;
+ // If the CallerInfo query finds no contacts, should we use the
+ // PhoneNumberOfflineGeocoder to look up a "geo description"?
+ // (TODO: This could become a flag in config.xml if it ever needs to be
+ // configured on a per-product basis.)
+ private static final boolean ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION = true;
+ /* Directory lookup related code - START */
+ private static final String[] DIRECTORY_PROJECTION = new String[] {Directory._ID};
+
+ /** Private constructor for factory methods. */
+ private CallerInfoAsyncQuery() {}
+
+ @RequiresPermission(Manifest.permission.READ_CONTACTS)
+ public static void startQuery(
+ final int token,
+ final Context context,
+ final CallerInfo info,
+ final OnQueryCompleteListener listener,
+ final Object cookie) {
+ Log.d(LOG_TAG, "##### CallerInfoAsyncQuery startContactProviderQuery()... #####");
+ Log.d(LOG_TAG, "- number: " + info.phoneNumber);
+ Log.d(LOG_TAG, "- cookie: " + cookie);
+
+ OnQueryCompleteListener contactsProviderQueryCompleteListener =
+ new OnQueryCompleteListener() {
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ Log.d(LOG_TAG, "contactsProviderQueryCompleteListener done");
+ // If there are no other directory queries, make sure that the listener is
+ // notified of this result. see b/27621628
+ if ((ci != null && ci.contactExists)
+ || !startOtherDirectoriesQuery(token, context, info, listener, cookie)) {
+ if (listener != null && ci != null) {
+ listener.onQueryComplete(token, cookie, ci);
+ }
+ }
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ listener.onDataLoaded(token, cookie, ci);
+ }
+ };
+ startDefaultDirectoryQuery(token, context, info, contactsProviderQueryCompleteListener, cookie);
+ }
+
+ // Private methods
+ private static void startDefaultDirectoryQuery(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie) {
+ // Construct the URI object and query params, and start the query.
+ Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber);
+ startQueryInternal(token, context, info, listener, cookie, uri);
+ }
+
+ /**
+ * Factory method to start the query based on a CallerInfo object.
+ *
+ * <p>Note: if the number contains an "@" character we treat it as a SIP address, and look it up
+ * directly in the Data table rather than using the PhoneLookup table. TODO: But eventually we
+ * should expose two separate methods, one for numbers and one for SIP addresses, and then have
+ * PhoneUtils.startGetCallerInfo() decide which one to call based on the phone type of the
+ * incoming connection.
+ */
+ private static void startQueryInternal(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie,
+ Uri contactRef) {
+ if (DBG) {
+ Log.d(LOG_TAG, "==> contactRef: " + sanitizeUriToString(contactRef));
+ }
+
+ if ((context == null) || (contactRef == null)) {
+ throw new QueryPoolException("Bad context or query uri.");
+ }
+ CallerInfoAsyncQueryHandler handler = new CallerInfoAsyncQueryHandler(context, contactRef);
+
+ //create cookieWrapper, start query
+ CookieWrapper cw = new CookieWrapper();
+ cw.listener = listener;
+ cw.cookie = cookie;
+ cw.number = info.phoneNumber;
+
+ // check to see if these are recognized numbers, and use shortcuts if we can.
+ if (PhoneNumberUtils.isLocalEmergencyNumber(context, info.phoneNumber)) {
+ cw.event = EVENT_EMERGENCY_NUMBER;
+ } else if (info.isVoiceMailNumber()) {
+ cw.event = EVENT_VOICEMAIL_NUMBER;
+ } else {
+ cw.event = EVENT_NEW_QUERY;
+ }
+
+ String[] proejection = CallerInfo.getDefaultPhoneLookupProjection(contactRef);
+ handler.startQuery(
+ token,
+ cw, // cookie
+ contactRef, // uri
+ proejection, // projection
+ null, // selection
+ null, // selectionArgs
+ null); // orderBy
+ }
+
+ // Return value indicates if listener was notified.
+ private static boolean startOtherDirectoriesQuery(
+ int token,
+ Context context,
+ CallerInfo info,
+ OnQueryCompleteListener listener,
+ Object cookie) {
+ long[] directoryIds = getDirectoryIds(context);
+ int size = directoryIds.length;
+ if (size == 0) {
+ return false;
+ }
+
+ DirectoryQueryCompleteListenerFactory listenerFactory =
+ new DirectoryQueryCompleteListenerFactory(context, size, listener);
+
+ // The current implementation of multiple async query runs in single handler thread
+ // in AsyncQueryHandler.
+ // intermediateListener.onQueryComplete is also called from the same caller thread.
+ // TODO(b/26019872): use thread pool instead of single thread.
+ for (int i = 0; i < size; i++) {
+ long directoryId = directoryIds[i];
+ Uri uri = ContactInfoHelper.getContactInfoLookupUri(info.phoneNumber, directoryId);
+ if (DBG) {
+ Log.d(LOG_TAG, "directoryId: " + directoryId + " uri: " + uri);
+ }
+ OnQueryCompleteListener intermediateListener = listenerFactory.newListener(directoryId);
+ startQueryInternal(token, context, info, intermediateListener, cookie, uri);
+ }
+ return true;
+ }
+
+ private static long[] getDirectoryIds(Context context) {
+ ArrayList<Long> results = new ArrayList<>();
+
+ Uri uri = Directory.CONTENT_URI;
+ if (VERSION.SDK_INT >= VERSION_CODES.N) {
+ uri = Uri.withAppendedPath(ContactsContract.AUTHORITY_URI, "directories_enterprise");
+ }
+
+ ContentResolver cr = context.getContentResolver();
+ Cursor cursor = cr.query(uri, DIRECTORY_PROJECTION, null, null, null);
+ addDirectoryIdsFromCursor(cursor, results);
+
+ long[] result = new long[results.size()];
+ for (int i = 0; i < results.size(); i++) {
+ result[i] = results.get(i);
+ }
+ return result;
+ }
+
+ private static void addDirectoryIdsFromCursor(Cursor cursor, ArrayList<Long> results) {
+ if (cursor != null) {
+ int idIndex = cursor.getColumnIndex(Directory._ID);
+ while (cursor.moveToNext()) {
+ long id = cursor.getLong(idIndex);
+ if (DirectoryCompat.isRemoteDirectoryId(id)) {
+ results.add(id);
+ }
+ }
+ cursor.close();
+ }
+ }
+
+ private static String sanitizeUriToString(Uri uri) {
+ if (uri != null) {
+ String uriString = uri.toString();
+ int indexOfLastSlash = uriString.lastIndexOf('/');
+ if (indexOfLastSlash > 0) {
+ return uriString.substring(0, indexOfLastSlash) + "/xxxxxxx";
+ } else {
+ return uriString;
+ }
+ } else {
+ return "";
+ }
+ }
+
+ /** Wrap the cookie from the WorkerArgs with additional information needed by our classes. */
+ private static final class CookieWrapper {
+
+ public OnQueryCompleteListener listener;
+ public Object cookie;
+ public int event;
+ public String number;
+ }
+ /* Directory lookup related code - END */
+
+ /** Simple exception used to communicate problems with the query pool. */
+ public static class QueryPoolException extends SQLException {
+
+ public QueryPoolException(String error) {
+ super(error);
+ }
+ }
+
+ private static final class DirectoryQueryCompleteListenerFactory {
+
+ private final OnQueryCompleteListener mListener;
+ private final Context mContext;
+ // Make sure listener to be called once and only once
+ private int mCount;
+ private boolean mIsListenerCalled;
+
+ DirectoryQueryCompleteListenerFactory(
+ Context context, int size, OnQueryCompleteListener listener) {
+ mCount = size;
+ mListener = listener;
+ mIsListenerCalled = false;
+ mContext = context;
+ }
+
+ private void onDirectoryQueryComplete(
+ int token, Object cookie, CallerInfo ci, long directoryId) {
+ boolean shouldCallListener = false;
+ synchronized (this) {
+ mCount = mCount - 1;
+ if (!mIsListenerCalled && (ci.contactExists || mCount == 0)) {
+ mIsListenerCalled = true;
+ shouldCallListener = true;
+ }
+ }
+
+ // Don't call callback in synchronized block because mListener.onQueryComplete may
+ // take long time to complete
+ if (shouldCallListener && mListener != null) {
+ addCallerInfoIntoCache(ci, directoryId);
+ mListener.onQueryComplete(token, cookie, ci);
+ }
+ }
+
+ private void addCallerInfoIntoCache(CallerInfo ci, long directoryId) {
+ CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(mContext).getCachedNumberLookupService();
+ if (ci.contactExists && cachedNumberLookupService != null) {
+ // 1. Cache caller info
+ CachedContactInfo cachedContactInfo =
+ CallerInfoUtils.buildCachedContactInfo(cachedNumberLookupService, ci);
+ String directoryLabel = mContext.getString(R.string.directory_search_label);
+ cachedContactInfo.setDirectorySource(directoryLabel, directoryId);
+ cachedNumberLookupService.addContact(mContext, cachedContactInfo);
+
+ // 2. Cache photo
+ if (ci.contactDisplayPhotoUri != null && ci.normalizedNumber != null) {
+ try (InputStream in =
+ mContext.getContentResolver().openInputStream(ci.contactDisplayPhotoUri)) {
+ if (in != null) {
+ cachedNumberLookupService.addPhoto(mContext, ci.normalizedNumber, in);
+ }
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "failed to fetch directory contact photo", e);
+ }
+ }
+ }
+ }
+
+ public OnQueryCompleteListener newListener(long directoryId) {
+ return new DirectoryQueryCompleteListener(directoryId);
+ }
+
+ private class DirectoryQueryCompleteListener implements OnQueryCompleteListener {
+
+ private final long mDirectoryId;
+
+ DirectoryQueryCompleteListener(long directoryId) {
+ mDirectoryId = directoryId;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ mListener.onDataLoaded(token, cookie, ci);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ onDirectoryQueryComplete(token, cookie, ci, mDirectoryId);
+ }
+ }
+ }
+
+ /** Our own implementation of the AsyncQueryHandler. */
+ private static class CallerInfoAsyncQueryHandler extends AsyncQueryHandler {
+
+ /**
+ * The information relevant to each CallerInfo query. Each query may have multiple listeners, so
+ * each AsyncCursorInfo is associated with 2 or more CookieWrapper objects in the queue (one
+ * with a new query event, and one with a end event, with 0 or more additional listeners in
+ * between).
+ */
+ private Context mQueryContext;
+
+ private Uri mQueryUri;
+ private CallerInfo mCallerInfo;
+
+ /** Asynchronous query handler class for the contact / callerinfo object. */
+ private CallerInfoAsyncQueryHandler(Context context, Uri contactRef) {
+ super(context.getContentResolver());
+ this.mQueryContext = context;
+ this.mQueryUri = contactRef;
+ }
+
+ @Override
+ public void startQuery(
+ int token,
+ Object cookie,
+ Uri uri,
+ String[] projection,
+ String selection,
+ String[] selectionArgs,
+ String orderBy) {
+ if (DBG) {
+ // Show stack trace with the arguments.
+ Log.d(
+ LOG_TAG,
+ "InCall: startQuery: url="
+ + uri
+ + " projection=["
+ + Arrays.toString(projection)
+ + "]"
+ + " selection="
+ + selection
+ + " "
+ + " args=["
+ + Arrays.toString(selectionArgs)
+ + "]",
+ new RuntimeException("STACKTRACE"));
+ }
+ super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy);
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ return new CallerInfoWorkerHandler(looper);
+ }
+
+ /**
+ * Overrides onQueryComplete from AsyncQueryHandler.
+ *
+ * <p>This method takes into account the state of this class; we construct the CallerInfo object
+ * only once for each set of listeners. When the query thread has done its work and calls this
+ * method, we inform the remaining listeners in the queue, until we're out of listeners. Once we
+ * get the message indicating that we should expect no new listeners for this CallerInfo object,
+ * we release the AsyncCursorInfo back into the pool.
+ */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ Log.d(this, "##### onQueryComplete() ##### query complete for token: " + token);
+
+ CookieWrapper cw = (CookieWrapper) cookie;
+
+ if (cw.listener != null) {
+ Log.d(
+ this,
+ "notifying listener: "
+ + cw.listener.getClass().toString()
+ + " for token: "
+ + token
+ + mCallerInfo);
+ cw.listener.onQueryComplete(token, cw.cookie, mCallerInfo);
+ }
+ mQueryContext = null;
+ mQueryUri = null;
+ mCallerInfo = null;
+ }
+
+ protected void updateData(int token, Object cookie, Cursor cursor) {
+ try {
+ Log.d(this, "##### updateData() ##### for token: " + token);
+
+ //get the cookie and notify the listener.
+ CookieWrapper cw = (CookieWrapper) cookie;
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that calls this method, we should
+ // check the parameters to make sure they're viable.
+ Log.d(this, "Cookie is null, ignoring onQueryComplete() request.");
+ return;
+ }
+
+ // check the token and if needed, create the callerinfo object.
+ if (mCallerInfo == null) {
+ if ((mQueryContext == null) || (mQueryUri == null)) {
+ throw new QueryPoolException(
+ "Bad context or query uri, or CallerInfoAsyncQuery already released.");
+ }
+
+ // adjust the callerInfo data as needed, and only if it was set from the
+ // initial query request.
+ // Change the callerInfo number ONLY if it is an emergency number or the
+ // voicemail number, and adjust other data (including photoResource)
+ // accordingly.
+ if (cw.event == EVENT_EMERGENCY_NUMBER) {
+ // Note we're setting the phone number here (refer to javadoc
+ // comments at the top of CallerInfo class).
+ mCallerInfo = new CallerInfo().markAsEmergency(mQueryContext);
+ } else if (cw.event == EVENT_VOICEMAIL_NUMBER) {
+ mCallerInfo = new CallerInfo().markAsVoiceMail(mQueryContext);
+ } else {
+ mCallerInfo = CallerInfo.getCallerInfo(mQueryContext, mQueryUri, cursor);
+ Log.d(this, "==> Got mCallerInfo: " + mCallerInfo);
+
+ CallerInfo newCallerInfo =
+ CallerInfo.doSecondaryLookupIfNecessary(mQueryContext, cw.number, mCallerInfo);
+ if (newCallerInfo != mCallerInfo) {
+ mCallerInfo = newCallerInfo;
+ Log.d(this, "#####async contact look up with numeric username" + mCallerInfo);
+ }
+
+ // Final step: look up the geocoded description.
+ if (ENABLE_UNKNOWN_NUMBER_GEO_DESCRIPTION) {
+ // Note we do this only if we *don't* have a valid name (i.e. if
+ // no contacts matched the phone number of the incoming call),
+ // since that's the only case where the incoming-call UI cares
+ // about this field.
+ //
+ // (TODO: But if we ever want the UI to show the geoDescription
+ // even when we *do* match a contact, we'll need to either call
+ // updateGeoDescription() unconditionally here, or possibly add a
+ // new parameter to CallerInfoAsyncQuery.startQuery() to force
+ // the geoDescription field to be populated.)
+
+ if (TextUtils.isEmpty(mCallerInfo.name)) {
+ // Actually when no contacts match the incoming phone number,
+ // the CallerInfo object is totally blank here (i.e. no name
+ // *or* phoneNumber). So we need to pass in cw.number as
+ // a fallback number.
+ mCallerInfo.updateGeoDescription(mQueryContext, cw.number);
+ }
+ }
+
+ // Use the number entered by the user for display.
+ if (!TextUtils.isEmpty(cw.number)) {
+ mCallerInfo.phoneNumber = cw.number;
+ }
+ }
+
+ Log.d(this, "constructing CallerInfo object for token: " + token);
+
+ if (cw.listener != null) {
+ cw.listener.onDataLoaded(token, cw.cookie, mCallerInfo);
+ }
+ }
+
+ } finally {
+ // The cursor may have been closed in CallerInfo.getCallerInfo()
+ if (cursor != null && !cursor.isClosed()) {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * Our own query worker thread.
+ *
+ * <p>This thread handles the messages enqueued in the looper. The normal sequence of events is
+ * that a new query shows up in the looper queue, followed by 0 or more add listener requests,
+ * and then an end request. Of course, these requests can be interlaced with requests from other
+ * tokens, but is irrelevant to this handler since the handler has no state.
+ *
+ * <p>Note that we depend on the queue to keep things in order; in other words, the looper queue
+ * must be FIFO with respect to input from the synchronous startQuery calls and output to this
+ * handleMessage call.
+ *
+ * <p>This use of the queue is required because CallerInfo objects may be accessed multiple
+ * times before the query is complete. All accesses (listeners) must be queued up and informed
+ * in order when the query is complete.
+ */
+ protected class CallerInfoWorkerHandler extends WorkerHandler {
+
+ public CallerInfoWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ CookieWrapper cw = (CookieWrapper) args.cookie;
+
+ if (cw == null) {
+ // Normally, this should never be the case for calls originating
+ // from within this code.
+ // However, if there is any code that this Handler calls (such as in
+ // super.handleMessage) that DOES place unexpected messages on the
+ // queue, then we need pass these messages on.
+ Log.d(
+ this,
+ "Unexpected command (CookieWrapper is null): "
+ + msg.what
+ + " ignored by CallerInfoWorkerHandler, passing onto parent.");
+
+ super.handleMessage(msg);
+ } else {
+ Log.d(
+ this,
+ "Processing event: "
+ + cw.event
+ + " token (arg1): "
+ + msg.arg1
+ + " command: "
+ + msg.what
+ + " query URI: "
+ + sanitizeUriToString(args.uri));
+
+ switch (cw.event) {
+ case EVENT_NEW_QUERY:
+ final ContentResolver resolver = mQueryContext.getContentResolver();
+
+ // This should never happen.
+ if (resolver == null) {
+ Log.e(this, "Content Resolver is null!");
+ return;
+ }
+ //start the sql command.
+ Cursor cursor;
+ try {
+ cursor =
+ resolver.query(
+ args.uri,
+ args.projection,
+ args.selection,
+ args.selectionArgs,
+ args.orderBy);
+ // Calling getCount() causes the cursor window to be filled,
+ // which will make the first access on the main thread a lot faster.
+ if (cursor != null) {
+ cursor.getCount();
+ }
+ } catch (Exception e) {
+ Log.e(this, "Exception thrown during handling EVENT_ARG_QUERY", e);
+ cursor = null;
+ }
+
+ args.result = cursor;
+ updateData(msg.arg1, cw, cursor);
+ break;
+
+ // shortcuts to avoid query for recognized numbers.
+ case EVENT_EMERGENCY_NUMBER:
+ case EVENT_VOICEMAIL_NUMBER:
+ case EVENT_ADD_LISTENER:
+ updateData(msg.arg1, cw, (Cursor) args.result);
+ break;
+ default:
+ }
+ Message reply = args.handler.obtainMessage(msg.what);
+ reply.obj = args;
+ reply.arg1 = msg.arg1;
+
+ reply.sendToTarget();
+ }
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
new file mode 100644
index 000000000..9f57fba65
--- /dev/null
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -0,0 +1,279 @@
+/*
+ * 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.incallui;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import com.android.contacts.common.model.Contact;
+import com.android.contacts.common.model.ContactLoader;
+import com.android.dialer.common.LogUtil;
+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.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.incallui.call.DialerCall;
+import java.util.Arrays;
+
+/** Utility methods for contact and caller info related functionality */
+public class CallerInfoUtils {
+
+ private static final String TAG = CallerInfoUtils.class.getSimpleName();
+
+ private static final int QUERY_TOKEN = -1;
+
+ public CallerInfoUtils() {}
+
+ /**
+ * This is called to get caller info for a call. This will return a CallerInfo object immediately
+ * based off information in the call, but more information is returned to the
+ * OnQueryCompleteListener (which contains information about the phone number label, user's name,
+ * etc).
+ */
+ public static CallerInfo getCallerInfoForCall(
+ Context context,
+ DialerCall call,
+ Object cookie,
+ CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
+ CallerInfo info = buildCallerInfo(context, call);
+
+ // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
+
+ if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
+ if (PermissionsUtil.hasContactsPermissions(context)) {
+ // Start the query with the number provided from the call.
+ LogUtil.d(
+ "CallerInfoUtils.getCallerInfoForCall",
+ "Actually starting CallerInfoAsyncQuery.startQuery()...");
+
+ //noinspection MissingPermission
+ CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, cookie);
+ } else {
+ LogUtil.w(
+ "CallerInfoUtils.getCallerInfoForCall",
+ "Dialer doesn't have permission to read contacts."
+ + " Not calling CallerInfoAsyncQuery.startQuery().");
+ }
+ }
+ return info;
+ }
+
+ public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+ CallerInfo info = new CallerInfo();
+
+ // Store CNAP information retrieved from the Connection (we want to do this
+ // here regardless of whether the number is empty or not).
+ info.cnapName = call.getCnapName();
+ info.name = info.cnapName;
+ info.numberPresentation = call.getNumberPresentation();
+ info.namePresentation = call.getCnapNamePresentation();
+ info.callSubject = call.getCallSubject();
+
+ String number = call.getNumber();
+ if (!TextUtils.isEmpty(number)) {
+ // Don't split it if it's a SIP number.
+ if (!PhoneNumberHelper.isUriNumber(number)) {
+ final String[] numbers = number.split("&");
+ number = numbers[0];
+ if (numbers.length > 1) {
+ info.forwardingNumber = numbers[1];
+ }
+ number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
+ }
+ info.phoneNumber = number;
+ }
+
+ // Because the InCallUI is immediately launched before the call is connected, occasionally
+ // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
+ // This call should still be handled as a voicemail call.
+ if ((call.getHandle() != null
+ && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
+ || isVoiceMailNumber(context, call)) {
+ info.markAsVoiceMail(context);
+ }
+
+ ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call, info);
+
+ return info;
+ }
+
+ /**
+ * Creates a new {@link CachedContactInfo} from a {@link CallerInfo}
+ *
+ * @param lookupService the {@link CachedNumberLookupService} used to build a new {@link
+ * CachedContactInfo}
+ * @param {@link CallerInfo} object
+ * @return a CachedContactInfo object created from this CallerInfo
+ * @throws NullPointerException if lookupService or ci are null
+ */
+ public static CachedContactInfo buildCachedContactInfo(
+ CachedNumberLookupService lookupService, CallerInfo ci) {
+ ContactInfo info = new ContactInfo();
+ info.name = ci.name;
+ info.type = ci.numberType;
+ info.label = ci.phoneLabel;
+ info.number = ci.phoneNumber;
+ info.normalizedNumber = ci.normalizedNumber;
+ info.photoUri = ci.contactDisplayPhotoUri;
+ info.userType = ci.userType;
+
+ CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info);
+ cacheInfo.setLookupKey(ci.lookupKeyOrNull);
+ return cacheInfo;
+ }
+
+ public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
+ }
+
+ /**
+ * Handles certain "corner cases" for CNAP. When we receive weird phone numbers from the network
+ * to indicate different number presentations, convert them to expected number and presentation
+ * values within the CallerInfo object.
+ *
+ * @param number number we use to verify if we are in a corner case
+ * @param presentation presentation value used to verify if we are in a corner case
+ * @return the new String that should be used for the phone number
+ */
+ /* package */
+ static String modifyForSpecialCnapCases(
+ Context context, CallerInfo ci, String number, int presentation) {
+ // Obviously we return number if ci == null, but still return number if
+ // number == null, because in these cases the correct string will still be
+ // displayed/logged after this function returns based on the presentation value.
+ if (ci == null || number == null) {
+ return number;
+ }
+
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "modifyForSpecialCnapCases: initially, number="
+ + toLogSafePhoneNumber(number)
+ + ", presentation="
+ + presentation
+ + " ci "
+ + ci);
+
+ // "ABSENT NUMBER" is a possible value we could get from the network as the
+ // phone number, so if this happens, change it to "Unknown" in the CallerInfo
+ // and fix the presentation to be the same.
+ final String[] absentNumberValues = context.getResources().getStringArray(R.array.absent_num);
+ if (Arrays.asList(absentNumberValues).contains(number)
+ && presentation == TelecomManager.PRESENTATION_ALLOWED) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ }
+
+ // Check for other special "corner cases" for CNAP and fix them similarly. Corner
+ // cases only apply if we received an allowed presentation from the network, so check
+ // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't
+ // match the presentation passed in for verification (meaning we changed it previously
+ // because it's a corner case and we're being called from a different entry point).
+ if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED
+ || (ci.numberPresentation != presentation
+ && presentation == TelecomManager.PRESENTATION_ALLOWED)) {
+ // For all special strings, change number & numberPrentation.
+ if (isCnapSpecialCaseRestricted(number)) {
+ number = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
+ ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED;
+ } else if (isCnapSpecialCaseUnknown(number)) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ }
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "SpecialCnap: number="
+ + toLogSafePhoneNumber(number)
+ + "; presentation now="
+ + ci.numberPresentation);
+ }
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "returning number string=" + toLogSafePhoneNumber(number));
+ return number;
+ }
+
+ private static boolean isCnapSpecialCaseRestricted(String n) {
+ return n.equals("PRIVATE") || n.equals("P") || n.equals("RES") || n.equals("PRIVATENUMBER");
+ }
+
+ private static boolean isCnapSpecialCaseUnknown(String n) {
+ return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U");
+ }
+
+ /* package */
+ static String toLogSafePhoneNumber(String number) {
+ // For unknown number, log empty string.
+ if (number == null) {
+ return "";
+ }
+
+ // Todo: Figure out an equivalent for VDBG
+ if (false) {
+ // When VDBG is true we emit PII.
+ return number;
+ }
+
+ // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+ // sanitized phone numbers.
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < number.length(); i++) {
+ char c = number.charAt(i);
+ if (c == '-' || c == '@' || c == '.' || c == '&') {
+ builder.append(c);
+ } else {
+ builder.append('x');
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are
+ * viewing a particular contact, so that it can download the high-res photo.
+ */
+ public static void sendViewNotification(Context context, Uri contactUri) {
+ final ContactLoader loader =
+ new ContactLoader(context, contactUri, true /* postViewNotification */);
+ loader.registerListener(
+ 0,
+ new OnLoadCompleteListener<Contact>() {
+ @Override
+ public void onLoadComplete(Loader<Contact> loader, Contact contact) {
+ try {
+ loader.reset();
+ } catch (RuntimeException e) {
+ LogUtil.e("CallerInfoUtils.onLoadComplete", "Error resetting loader", e);
+ }
+ }
+ });
+ loader.startLoading();
+ }
+}
diff --git a/java/com/android/incallui/ConferenceManagerFragment.java b/java/com/android/incallui/ConferenceManagerFragment.java
new file mode 100644
index 000000000..8696bb8ec
--- /dev/null
+++ b/java/com/android/incallui/ConferenceManagerFragment.java
@@ -0,0 +1,106 @@
+/*
+ * 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.incallui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import com.android.contacts.common.ContactPhotoManager;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.incallui.ConferenceManagerPresenter.ConferenceManagerUi;
+import com.android.incallui.baseui.BaseFragment;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import java.util.List;
+
+/** Fragment that allows the user to manage a conference call. */
+public class ConferenceManagerFragment
+ extends BaseFragment<ConferenceManagerPresenter, ConferenceManagerUi>
+ implements ConferenceManagerPresenter.ConferenceManagerUi {
+
+ private ListView mConferenceParticipantList;
+ private ContactPhotoManager mContactPhotoManager;
+ private ConferenceParticipantListAdapter mConferenceParticipantListAdapter;
+
+ @Override
+ public ConferenceManagerPresenter createPresenter() {
+ return new ConferenceManagerPresenter();
+ }
+
+ @Override
+ public ConferenceManagerPresenter.ConferenceManagerUi getUi() {
+ return this;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ Logger.get(getContext()).logScreenView(ScreenEvent.Type.CONFERENCE_MANAGEMENT, getActivity());
+ }
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View parent = inflater.inflate(R.layout.conference_manager_fragment, container, false);
+
+ mConferenceParticipantList = (ListView) parent.findViewById(R.id.participantList);
+ mContactPhotoManager = ContactPhotoManager.getInstance(getActivity().getApplicationContext());
+
+ return parent;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final CallList calls = CallList.getInstance();
+ getPresenter().init(calls);
+ // Request focus on the list of participants for accessibility purposes. This ensures
+ // that once the list of participants is shown, the first participant is announced.
+ mConferenceParticipantList.requestFocus();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public boolean isFragmentVisible() {
+ return isVisible();
+ }
+
+ @Override
+ public void update(List<DialerCall> participants, boolean parentCanSeparate) {
+ if (mConferenceParticipantListAdapter == null) {
+ mConferenceParticipantListAdapter =
+ new ConferenceParticipantListAdapter(mConferenceParticipantList, mContactPhotoManager);
+
+ mConferenceParticipantList.setAdapter(mConferenceParticipantListAdapter);
+ }
+ mConferenceParticipantListAdapter.updateParticipants(participants, parentCanSeparate);
+ }
+
+ @Override
+ public void refreshCall(DialerCall call) {
+ mConferenceParticipantListAdapter.refreshCall(call);
+ }
+}
diff --git a/java/com/android/incallui/ConferenceManagerPresenter.java b/java/com/android/incallui/ConferenceManagerPresenter.java
new file mode 100644
index 000000000..226741dcd
--- /dev/null
+++ b/java/com/android/incallui/ConferenceManagerPresenter.java
@@ -0,0 +1,139 @@
+/*
+ * 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.incallui;
+
+import com.android.incallui.ConferenceManagerPresenter.ConferenceManagerUi;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Logic for call buttons. */
+public class ConferenceManagerPresenter extends Presenter<ConferenceManagerUi>
+ implements InCallStateListener, InCallDetailsListener, IncomingCallListener {
+
+ @Override
+ public void onUiReady(ConferenceManagerUi ui) {
+ super.onUiReady(ui);
+
+ // register for call state changes last
+ InCallPresenter.getInstance().addListener(this);
+ InCallPresenter.getInstance().addIncomingCallListener(this);
+ }
+
+ @Override
+ public void onUiUnready(ConferenceManagerUi ui) {
+ super.onUiUnready(ui);
+
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ if (getUi().isFragmentVisible()) {
+ Log.v(this, "onStateChange" + newState);
+ if (newState == InCallState.INCALL) {
+ final DialerCall call = callList.getActiveOrBackgroundCall();
+ if (call != null && call.isConferenceCall()) {
+ Log.v(
+ this, "Number of existing calls is " + String.valueOf(call.getChildCallIds().size()));
+ update(callList);
+ } else {
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ } else {
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ }
+ }
+
+ @Override
+ public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
+ boolean canDisconnect =
+ details.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE);
+ boolean canSeparate =
+ details.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE);
+
+ if (call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE)
+ != canDisconnect
+ || call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE)
+ != canSeparate) {
+ getUi().refreshCall(call);
+ }
+
+ if (!details.can(android.telecom.Call.Details.CAPABILITY_MANAGE_CONFERENCE)) {
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ }
+
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ // When incoming call exists, set conference ui invisible.
+ if (getUi().isFragmentVisible()) {
+ Log.d(this, "onIncomingCall()... Conference ui is showing, hide it.");
+ InCallPresenter.getInstance().showConferenceCallManager(false);
+ }
+ }
+
+ public void init(CallList callList) {
+ update(callList);
+ }
+
+ /**
+ * Updates the conference participant adapter.
+ *
+ * @param callList The callList.
+ */
+ private void update(CallList callList) {
+ // callList is non null, but getActiveOrBackgroundCall() may return null
+ final DialerCall currentCall = callList.getActiveOrBackgroundCall();
+ if (currentCall == null) {
+ return;
+ }
+
+ ArrayList<DialerCall> calls = new ArrayList<>(currentCall.getChildCallIds().size());
+ for (String callerId : currentCall.getChildCallIds()) {
+ calls.add(callList.getCallById(callerId));
+ }
+
+ Log.d(this, "Number of calls is " + String.valueOf(calls.size()));
+
+ // Users can split out a call from the conference call if either the active call or the
+ // holding call is empty. If both are filled, users can not split out another call.
+ final boolean hasActiveCall = (callList.getActiveCall() != null);
+ final boolean hasHoldingCall = (callList.getBackgroundCall() != null);
+ boolean canSeparate = !(hasActiveCall && hasHoldingCall);
+
+ getUi().update(calls, canSeparate);
+ }
+
+ public interface ConferenceManagerUi extends Ui {
+
+ boolean isFragmentVisible();
+
+ void update(List<DialerCall> participants, boolean parentCanSeparate);
+
+ void refreshCall(DialerCall call);
+ }
+}
diff --git a/java/com/android/incallui/ConferenceParticipantListAdapter.java b/java/com/android/incallui/ConferenceParticipantListAdapter.java
new file mode 100644
index 000000000..72c0fcd20
--- /dev/null
+++ b/java/com/android/incallui/ConferenceParticipantListAdapter.java
@@ -0,0 +1,523 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+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.preference.ContactsPreferences;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+/** Adapter for a ListView containing conference call participant information. */
+public class ConferenceParticipantListAdapter extends BaseAdapter {
+
+ /** The ListView containing the participant information. */
+ private final ListView mListView;
+ /** Hashmap to make accessing participant info by call Id faster. */
+ private final Map<String, ParticipantInfo> mParticipantsByCallId = new ArrayMap<>();
+ /** ContactsPreferences used to lookup displayName preferences */
+ @Nullable private final ContactsPreferences mContactsPreferences;
+ /** Contact photo manager to retrieve cached contact photo information. */
+ private final ContactPhotoManager mContactPhotoManager;
+ /** Listener used to handle tap of the "disconnect' button for a participant. */
+ private View.OnClickListener mDisconnectListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DialerCall call = getCallFromView(view);
+ LogUtil.i(
+ "ConferenceParticipantListAdapter.mDisconnectListener.onClick", "call: " + call);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ };
+ /** Listener used to handle tap of the "separate' button for a participant. */
+ private View.OnClickListener mSeparateListener =
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ DialerCall call = getCallFromView(view);
+ LogUtil.i("ConferenceParticipantListAdapter.mSeparateListener.onClick", "call: " + call);
+ if (call != null) {
+ call.splitFromConference();
+ }
+ }
+ };
+ /** The conference participants to show in the ListView. */
+ private List<ParticipantInfo> mConferenceParticipants = new ArrayList<>();
+ /** {@code True} if the conference parent supports separating calls from the conference. */
+ private boolean mParentCanSeparate;
+
+ /**
+ * Creates an instance of the ConferenceParticipantListAdapter.
+ *
+ * @param listView The listview.
+ * @param contactPhotoManager The contact photo manager, used to load contact photos.
+ */
+ public ConferenceParticipantListAdapter(
+ ListView listView, ContactPhotoManager contactPhotoManager) {
+
+ mListView = listView;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(getContext());
+ mContactPhotoManager = contactPhotoManager;
+ }
+
+ /**
+ * Updates the adapter with the new conference participant information provided.
+ *
+ * @param conferenceParticipants The list of conference participants.
+ * @param parentCanSeparate {@code True} if the parent supports separating calls from the
+ * conference.
+ */
+ public void updateParticipants(
+ List<DialerCall> conferenceParticipants, boolean parentCanSeparate) {
+ if (mContactsPreferences != null) {
+ mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
+ mContactsPreferences.refreshValue(ContactsPreferences.SORT_ORDER_KEY);
+ }
+ mParentCanSeparate = parentCanSeparate;
+ updateParticipantInfo(conferenceParticipants);
+ }
+
+ /**
+ * Determines the number of participants in the conference.
+ *
+ * @return The number of participants.
+ */
+ @Override
+ public int getCount() {
+ return mConferenceParticipants.size();
+ }
+
+ /**
+ * Retrieves an item from the list of participants.
+ *
+ * @param position Position of the item whose data we want within the adapter's data set.
+ * @return The {@link ParticipantInfo}.
+ */
+ @Override
+ public Object getItem(int position) {
+ return mConferenceParticipants.get(position);
+ }
+
+ /**
+ * Retreives the adapter-specific item id for an item at a specified position.
+ *
+ * @param position The position of the item within the adapter's data set whose row id we want.
+ * @return The item id.
+ */
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ /**
+ * Refreshes call information for the call passed in.
+ *
+ * @param call The new call information.
+ */
+ public void refreshCall(DialerCall call) {
+ String callId = call.getId();
+
+ if (mParticipantsByCallId.containsKey(callId)) {
+ ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
+ participantInfo.setCall(call);
+ refreshView(callId);
+ }
+ }
+
+ private Context getContext() {
+ return mListView.getContext();
+ }
+
+ /**
+ * Attempts to refresh the view for the specified call ID. This ensures the contact info and photo
+ * loaded from cache are updated.
+ *
+ * @param callId The call id.
+ */
+ private void refreshView(String callId) {
+ int first = mListView.getFirstVisiblePosition();
+ int last = mListView.getLastVisiblePosition();
+
+ for (int position = 0; position <= last - first; position++) {
+ View view = mListView.getChildAt(position);
+ String rowCallId = (String) view.getTag();
+ if (rowCallId.equals(callId)) {
+ getView(position + first, view, mListView);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Creates or populates an existing conference participant row.
+ *
+ * @param position The position of the item within the adapter's data set of the item whose view
+ * we want.
+ * @param convertView The old view to reuse, if possible.
+ * @param parent The parent that this view will eventually be attached to
+ * @return The populated view.
+ */
+ @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
+ ? LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.caller_in_conference, parent, false)
+ : convertView;
+
+ ParticipantInfo participantInfo = mConferenceParticipants.get(position);
+ DialerCall call = participantInfo.getCall();
+ ContactCacheEntry contactCache = participantInfo.getContactCacheEntry();
+
+ final ContactInfoCache cache = ContactInfoCache.getInstance(getContext());
+
+ // If a cache lookup has not yet been performed to retrieve the contact information and
+ // photo, do it now.
+ if (!participantInfo.isCacheLookupComplete()) {
+ cache.findInfo(
+ participantInfo.getCall(),
+ participantInfo.getCall().getState() == DialerCall.State.INCOMING,
+ new ContactLookupCallback(this));
+ }
+
+ boolean thisRowCanSeparate =
+ mParentCanSeparate
+ && call.can(android.telecom.Call.Details.CAPABILITY_SEPARATE_FROM_CONFERENCE);
+ boolean thisRowCanDisconnect =
+ call.can(android.telecom.Call.Details.CAPABILITY_DISCONNECT_FROM_CONFERENCE);
+
+ setCallerInfoForRow(
+ result,
+ contactCache.namePrimary,
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactCache.namePrimary, contactCache.nameAlternative, mContactsPreferences),
+ contactCache.number,
+ contactCache.label,
+ contactCache.lookupKey,
+ contactCache.displayPhotoUri,
+ thisRowCanSeparate,
+ thisRowCanDisconnect);
+
+ // Tag the row in the conference participant list with the call id to make it easier to
+ // find calls when contact cache information is loaded.
+ result.setTag(call.getId());
+
+ return result;
+ }
+
+ /**
+ * Replaces the contact info for a participant and triggers a refresh of the UI.
+ *
+ * @param callId The call id.
+ * @param entry The new contact info.
+ */
+ /* package */ void updateContactInfo(String callId, ContactCacheEntry entry) {
+ if (mParticipantsByCallId.containsKey(callId)) {
+ ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
+ participantInfo.setContactCacheEntry(entry);
+ participantInfo.setCacheLookupComplete(true);
+ refreshView(callId);
+ }
+ }
+
+ /**
+ * Sets the caller information for a row in the conference participant list.
+ *
+ * @param view The view to set the details on.
+ * @param callerName The participant's name.
+ * @param callerNumber The participant's phone number.
+ * @param callerNumberType The participant's phone number typ.e
+ * @param lookupKey The lookup key for the participant (for photo lookup).
+ * @param photoUri The URI of the contact photo.
+ * @param thisRowCanSeparate {@code True} if this participant can separate from the conference.
+ * @param thisRowCanDisconnect {@code True} if this participant can be disconnected.
+ */
+ private void setCallerInfoForRow(
+ View view,
+ String callerName,
+ String preferredName,
+ String callerNumber,
+ String callerNumberType,
+ String lookupKey,
+ Uri photoUri,
+ boolean thisRowCanSeparate,
+ boolean thisRowCanDisconnect) {
+
+ final ImageView photoView = (ImageView) view.findViewById(R.id.callerPhoto);
+ final TextView nameTextView = (TextView) view.findViewById(R.id.conferenceCallerName);
+ final TextView numberTextView = (TextView) view.findViewById(R.id.conferenceCallerNumber);
+ final TextView numberTypeTextView =
+ (TextView) view.findViewById(R.id.conferenceCallerNumberType);
+ final View endButton = view.findViewById(R.id.conferenceCallerDisconnect);
+ final View separateButton = view.findViewById(R.id.conferenceCallerSeparate);
+
+ endButton.setVisibility(thisRowCanDisconnect ? View.VISIBLE : View.GONE);
+ if (thisRowCanDisconnect) {
+ endButton.setOnClickListener(mDisconnectListener);
+ } else {
+ endButton.setOnClickListener(null);
+ }
+
+ separateButton.setVisibility(thisRowCanSeparate ? View.VISIBLE : View.GONE);
+ if (thisRowCanSeparate) {
+ separateButton.setOnClickListener(mSeparateListener);
+ } else {
+ separateButton.setOnClickListener(null);
+ }
+
+ DefaultImageRequest imageRequest =
+ (photoUri != null)
+ ? null
+ : new DefaultImageRequest(callerName, lookupKey, true /* isCircularPhoto */);
+
+ mContactPhotoManager.loadDirectoryPhoto(photoView, photoUri, false, true, imageRequest);
+
+ // set the caller name
+ nameTextView.setText(preferredName);
+
+ // set the caller number in subscript, or make the field disappear.
+ if (TextUtils.isEmpty(callerNumber)) {
+ numberTextView.setVisibility(View.GONE);
+ numberTypeTextView.setVisibility(View.GONE);
+ } else {
+ numberTextView.setVisibility(View.VISIBLE);
+ numberTextView.setText(
+ PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(callerNumber, TextDirectionHeuristics.LTR)));
+ numberTypeTextView.setVisibility(View.VISIBLE);
+ numberTypeTextView.setText(callerNumberType);
+ }
+ }
+
+ /**
+ * Updates the participant info list which is bound to the ListView. Stores the call and contact
+ * info for all entries. The list is sorted alphabetically by participant name.
+ *
+ * @param conferenceParticipants The calls which make up the conference participants.
+ */
+ private void updateParticipantInfo(List<DialerCall> conferenceParticipants) {
+ final ContactInfoCache cache = ContactInfoCache.getInstance(getContext());
+ boolean newParticipantAdded = false;
+ Set<String> newCallIds = new ArraySet<>(conferenceParticipants.size());
+
+ // Update or add conference participant info.
+ for (DialerCall call : conferenceParticipants) {
+ String callId = call.getId();
+ newCallIds.add(callId);
+ ContactCacheEntry contactCache = cache.getInfo(callId);
+ if (contactCache == null) {
+ contactCache =
+ ContactInfoCache.buildCacheEntryFromCall(
+ getContext(), call, call.getState() == DialerCall.State.INCOMING);
+ }
+
+ if (mParticipantsByCallId.containsKey(callId)) {
+ ParticipantInfo participantInfo = mParticipantsByCallId.get(callId);
+ participantInfo.setCall(call);
+ participantInfo.setContactCacheEntry(contactCache);
+ } else {
+ newParticipantAdded = true;
+ ParticipantInfo participantInfo = new ParticipantInfo(call, contactCache);
+ mConferenceParticipants.add(participantInfo);
+ mParticipantsByCallId.put(call.getId(), participantInfo);
+ }
+ }
+
+ // Remove any participants that no longer exist.
+ Iterator<Map.Entry<String, ParticipantInfo>> it = mParticipantsByCallId.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, ParticipantInfo> entry = it.next();
+ String existingCallId = entry.getKey();
+ if (!newCallIds.contains(existingCallId)) {
+ ParticipantInfo existingInfo = entry.getValue();
+ mConferenceParticipants.remove(existingInfo);
+ it.remove();
+ }
+ }
+
+ if (newParticipantAdded) {
+ // Sort the list of participants by contact name.
+ sortParticipantList();
+ }
+ notifyDataSetChanged();
+ }
+
+ /** Sorts the participant list by contact name. */
+ private void sortParticipantList() {
+ Collections.sort(
+ mConferenceParticipants,
+ new Comparator<ParticipantInfo>() {
+ @Override
+ public int compare(ParticipantInfo p1, ParticipantInfo p2) {
+ // Contact names might be null, so replace with empty string.
+ ContactCacheEntry c1 = p1.getContactCacheEntry();
+ String p1Name =
+ ContactDisplayUtils.getPreferredSortName(
+ c1.namePrimary, c1.nameAlternative, mContactsPreferences);
+ p1Name = p1Name != null ? p1Name : "";
+
+ ContactCacheEntry c2 = p2.getContactCacheEntry();
+ String p2Name =
+ ContactDisplayUtils.getPreferredSortName(
+ c2.namePrimary, c2.nameAlternative, mContactsPreferences);
+ p2Name = p2Name != null ? p2Name : "";
+
+ return p1Name.compareToIgnoreCase(p2Name);
+ }
+ });
+ }
+
+ private DialerCall getCallFromView(View view) {
+ View parent = (View) view.getParent();
+ String callId = (String) parent.getTag();
+ return CallList.getInstance().getCallById(callId);
+ }
+
+ /**
+ * Callback class used when making requests to the {@link ContactInfoCache} to resolve contact
+ * info and contact photos for conference participants.
+ */
+ public static class ContactLookupCallback implements ContactInfoCache.ContactInfoCacheCallback {
+
+ private final WeakReference<ConferenceParticipantListAdapter> mListAdapter;
+
+ public ContactLookupCallback(ConferenceParticipantListAdapter listAdapter) {
+ mListAdapter = new WeakReference<>(listAdapter);
+ }
+
+ /**
+ * Called when contact info has been resolved.
+ *
+ * @param callId The call id.
+ * @param entry The new contact information.
+ */
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ update(callId, entry);
+ }
+
+ /**
+ * Called when contact photo has been loaded into the cache.
+ *
+ * @param callId The call id.
+ * @param entry The new contact information.
+ */
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ update(callId, entry);
+ }
+
+ /**
+ * Updates the contact information for a participant.
+ *
+ * @param callId The call id.
+ * @param entry The new contact information.
+ */
+ private void update(String callId, ContactCacheEntry entry) {
+ ConferenceParticipantListAdapter listAdapter = mListAdapter.get();
+ if (listAdapter != null) {
+ listAdapter.updateContactInfo(callId, entry);
+ }
+ }
+ }
+
+ /**
+ * Internal class which represents a participant. Includes a reference to the {@link DialerCall}
+ * and the corresponding {@link ContactCacheEntry} for the participant.
+ */
+ private static class ParticipantInfo {
+
+ private DialerCall mCall;
+ private ContactCacheEntry mContactCacheEntry;
+ private boolean mCacheLookupComplete = false;
+
+ public ParticipantInfo(DialerCall call, ContactCacheEntry contactCacheEntry) {
+ mCall = call;
+ mContactCacheEntry = contactCacheEntry;
+ }
+
+ public DialerCall getCall() {
+ return mCall;
+ }
+
+ public void setCall(DialerCall call) {
+ mCall = call;
+ }
+
+ public ContactCacheEntry getContactCacheEntry() {
+ return mContactCacheEntry;
+ }
+
+ public void setContactCacheEntry(ContactCacheEntry entry) {
+ mContactCacheEntry = entry;
+ }
+
+ public boolean isCacheLookupComplete() {
+ return mCacheLookupComplete;
+ }
+
+ public void setCacheLookupComplete(boolean cacheLookupComplete) {
+ mCacheLookupComplete = cacheLookupComplete;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof ParticipantInfo) {
+ ParticipantInfo p = (ParticipantInfo) o;
+ return Objects.equals(p.getCall().getId(), mCall.getId());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mCall.getId().hashCode();
+ }
+ }
+}
diff --git a/java/com/android/incallui/ContactInfoCache.java b/java/com/android/incallui/ContactInfoCache.java
new file mode 100644
index 000000000..4d4d94a17
--- /dev/null
+++ b/java/com/android/incallui/ContactInfoCache.java
@@ -0,0 +1,759 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.support.v4.os.UserManagerCompat;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.contacts.common.ContactsUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.PhoneNumberCache;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.MoreStrings;
+import com.android.incallui.CallerInfoAsyncQuery.OnQueryCompleteListener;
+import com.android.incallui.ContactsAsyncHelper.OnImageLoadCompleteListener;
+import com.android.incallui.bindings.PhoneNumberService;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Class responsible for querying Contact Information for DialerCall objects. Can perform
+ * asynchronous requests to the Contact Provider for information as well as respond synchronously
+ * for any data that it currently has cached from previous queries. This class always gets called
+ * from the UI thread so it does not need thread protection.
+ */
+public class ContactInfoCache implements OnImageLoadCompleteListener {
+
+ private static final String TAG = ContactInfoCache.class.getSimpleName();
+ private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
+ private static ContactInfoCache sCache = null;
+ private final Context mContext;
+ private final PhoneNumberService mPhoneNumberService;
+ // Cache info map needs to be thread-safe since it could be modified by both main thread and
+ // worker thread.
+ private final Map<String, ContactCacheEntry> mInfoMap = new ConcurrentHashMap<>();
+ private final Map<String, Set<ContactInfoCacheCallback>> mCallBacks = new ArrayMap<>();
+ private Drawable mDefaultContactPhotoDrawable;
+ private Drawable mConferencePhotoDrawable;
+
+ private ContactInfoCache(Context context) {
+ mContext = context;
+ mPhoneNumberService = Bindings.get(context).newPhoneNumberService(context);
+ }
+
+ public static synchronized ContactInfoCache getInstance(Context mContext) {
+ if (sCache == null) {
+ sCache = new ContactInfoCache(mContext.getApplicationContext());
+ }
+ return sCache;
+ }
+
+ public static ContactCacheEntry buildCacheEntryFromCall(
+ Context context, DialerCall call, boolean isIncoming) {
+ final ContactCacheEntry entry = new ContactCacheEntry();
+
+ // TODO: get rid of caller info.
+ final CallerInfo info = CallerInfoUtils.buildCallerInfo(context, call);
+ ContactInfoCache.populateCacheEntry(
+ context, info, entry, call.getNumberPresentation(), isIncoming);
+ return entry;
+ }
+
+ /** Populate a cache entry from a call (which got converted into a caller info). */
+ public static void populateCacheEntry(
+ @NonNull Context context,
+ @NonNull CallerInfo info,
+ @NonNull ContactCacheEntry cce,
+ int presentation,
+ boolean isIncoming) {
+ Objects.requireNonNull(info);
+ String displayName = null;
+ String displayNumber = null;
+ String displayLocation = null;
+ String label = null;
+ boolean isSipCall = false;
+
+ // It appears that there is a small change in behaviour with the
+ // PhoneUtils' startGetCallerInfo whereby if we query with an
+ // empty number, we will get a valid CallerInfo object, but with
+ // fields that are all null, and the isTemporary boolean input
+ // parameter as true.
+
+ // In the past, we would see a NULL callerinfo object, but this
+ // ends up causing null pointer exceptions elsewhere down the
+ // line in other cases, so we need to make this fix instead. It
+ // appears that this was the ONLY call to PhoneUtils
+ // .getCallerInfo() that relied on a NULL CallerInfo to indicate
+ // an unknown contact.
+
+ // Currently, info.phoneNumber may actually be a SIP address, and
+ // if so, it might sometimes include the "sip:" prefix. That
+ // prefix isn't really useful to the user, though, so strip it off
+ // if present. (For any other URI scheme, though, leave the
+ // prefix alone.)
+ // TODO: It would be cleaner for CallerInfo to explicitly support
+ // SIP addresses instead of overloading the "phoneNumber" field.
+ // Then we could remove this hack, and instead ask the CallerInfo
+ // for a "user visible" form of the SIP address.
+ String number = info.phoneNumber;
+
+ if (!TextUtils.isEmpty(number)) {
+ isSipCall = PhoneNumberHelper.isUriNumber(number);
+ if (number.startsWith("sip:")) {
+ number = number.substring(4);
+ }
+ }
+
+ if (TextUtils.isEmpty(info.name)) {
+ // No valid "name" in the CallerInfo, so fall back to
+ // something else.
+ // (Typically, we promote the phone number up to the "name" slot
+ // onscreen, and possibly display a descriptive string in the
+ // "number" slot.)
+ if (TextUtils.isEmpty(number)) {
+ // No name *or* number! Display a generic "unknown" string
+ // (or potentially some other default based on the presentation.)
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(TAG, " ==> no name *or* number! displayName = " + displayName);
+ } else if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a phone #
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(TAG, " ==> presentation not allowed! displayName = " + displayName);
+ } else if (!TextUtils.isEmpty(info.cnapName)) {
+ // No name, but we do have a valid CNAP name, so use that.
+ displayName = info.cnapName;
+ info.name = info.cnapName;
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+ Log.d(
+ TAG,
+ " ==> cnapName available: displayName '"
+ + displayName
+ + "', displayNumber '"
+ + displayNumber
+ + "'");
+ } else {
+ // No name; all we have is a number. This is the typical
+ // case when an incoming call doesn't match any contact,
+ // or if you manually dial an outgoing number using the
+ // dialpad.
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+
+ // Display a geographical description string if available
+ // (but only for incoming calls.)
+ if (isIncoming) {
+ // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
+ // query to only do the geoDescription lookup in the first
+ // place for incoming calls.
+ displayLocation = info.geoDescription; // may be null
+ Log.d(TAG, "Geodescrption: " + info.geoDescription);
+ }
+
+ Log.d(
+ TAG,
+ " ==> no name; falling back to number:"
+ + " displayNumber '"
+ + Log.pii(displayNumber)
+ + "', displayLocation '"
+ + displayLocation
+ + "'");
+ }
+ } else {
+ // We do have a valid "name" in the CallerInfo. Display that
+ // in the "name" slot, and the phone number in the "number" slot.
+ if (presentation != TelecomManager.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a name
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = getPresentationString(context, presentation, info.callSubject);
+ Log.d(
+ TAG,
+ " ==> valid name, but presentation not allowed!" + " displayName = " + displayName);
+ } else {
+ // Causes cce.namePrimary to be set as info.name below. CallCardPresenter will
+ // later determine whether to use the name or nameAlternative when presenting
+ displayName = info.name;
+ cce.nameAlternative = info.nameAlternative;
+ displayNumber = PhoneNumberHelper.formatNumber(number, context);
+ label = info.phoneLabel;
+ Log.d(
+ TAG,
+ " ==> name is present in CallerInfo: displayName '"
+ + displayName
+ + "', displayNumber '"
+ + displayNumber
+ + "'");
+ }
+ }
+
+ cce.namePrimary = displayName;
+ cce.number = displayNumber;
+ cce.location = displayLocation;
+ cce.label = label;
+ cce.isSipCall = isSipCall;
+ cce.userType = info.userType;
+
+ if (info.contactExists) {
+ cce.contactLookupResult = ContactLookupResult.Type.LOCAL_CONTACT;
+ }
+ }
+
+ /** Gets name strings based on some special presentation modes and the associated custom label. */
+ private static String getPresentationString(
+ Context context, int presentation, String customLabel) {
+ String name = context.getString(R.string.unknown);
+ if (!TextUtils.isEmpty(customLabel)
+ && ((presentation == TelecomManager.PRESENTATION_UNKNOWN)
+ || (presentation == TelecomManager.PRESENTATION_RESTRICTED))) {
+ name = customLabel;
+ return name;
+ } else {
+ if (presentation == TelecomManager.PRESENTATION_RESTRICTED) {
+ name = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
+ } else if (presentation == TelecomManager.PRESENTATION_PAYPHONE) {
+ name = context.getString(R.string.payphone);
+ }
+ }
+ return name;
+ }
+
+ public ContactCacheEntry getInfo(String callId) {
+ return mInfoMap.get(callId);
+ }
+
+ public void maybeInsertCnapInformationIntoCache(
+ Context context, final DialerCall call, final CallerInfo info) {
+ final CachedNumberLookupService cachedNumberLookupService =
+ PhoneNumberCache.get(context).getCachedNumberLookupService();
+ if (!UserManagerCompat.isUserUnlocked(context)) {
+ Log.i(TAG, "User locked, not inserting cnap info into cache");
+ return;
+ }
+ if (cachedNumberLookupService == null
+ || TextUtils.isEmpty(info.cnapName)
+ || mInfoMap.get(call.getId()) != null) {
+ return;
+ }
+ final Context applicationContext = context.getApplicationContext();
+ Log.i(TAG, "Found contact with CNAP name - inserting into cache");
+ new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ ContactInfo contactInfo = new ContactInfo();
+ CachedContactInfo cacheInfo = cachedNumberLookupService.buildCachedContactInfo(contactInfo);
+ cacheInfo.setSource(CachedContactInfo.SOURCE_TYPE_CNAP, "CNAP", 0);
+ contactInfo.name = info.cnapName;
+ contactInfo.number = call.getNumber();
+ contactInfo.type = ContactsContract.CommonDataKinds.Phone.TYPE_MAIN;
+ try {
+ final JSONObject contactRows =
+ new JSONObject()
+ .put(
+ Phone.CONTENT_ITEM_TYPE,
+ new JSONObject()
+ .put(Phone.NUMBER, contactInfo.number)
+ .put(Phone.TYPE, Phone.TYPE_MAIN));
+ final String jsonString =
+ new JSONObject()
+ .put(Contacts.DISPLAY_NAME, contactInfo.name)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.STRUCTURED_NAME)
+ .put(Contacts.CONTENT_ITEM_TYPE, contactRows)
+ .toString();
+ cacheInfo.setLookupKey(jsonString);
+ } catch (JSONException e) {
+ Log.w(TAG, "Creation of lookup key failed when caching CNAP information");
+ }
+ cachedNumberLookupService.addContact(applicationContext, cacheInfo);
+ return null;
+ }
+ }.execute();
+ }
+
+ /**
+ * Requests contact data for the DialerCall object passed in. Returns the data through callback.
+ * If callback is null, no response is made, however the query is still performed and cached.
+ *
+ * @param callback The function to call back when the call is found. Can be null.
+ */
+ @MainThread
+ public void findInfo(
+ @NonNull final DialerCall call,
+ final boolean isIncoming,
+ @NonNull ContactInfoCacheCallback callback) {
+ Assert.isMainThread();
+ Objects.requireNonNull(callback);
+
+ final String callId = call.getId();
+ final ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+
+ // If we have a previously obtained intermediate result return that now
+ if (cacheEntry != null) {
+ Log.d(
+ TAG,
+ "Contact lookup. In memory cache hit; lookup "
+ + (callBacks == null ? "complete" : "still running"));
+ callback.onContactInfoComplete(callId, cacheEntry);
+ // If no other callbacks are in flight, we're done.
+ if (callBacks == null) {
+ return;
+ }
+ }
+
+ // If the entry already exists, add callback
+ if (callBacks != null) {
+ callBacks.add(callback);
+ return;
+ }
+ Log.d(TAG, "Contact lookup. In memory cache miss; searching provider.");
+ // New lookup
+ callBacks = new ArraySet<>();
+ callBacks.add(callback);
+ mCallBacks.put(callId, callBacks);
+
+ /**
+ * Performs a query for caller information. Save any immediate data we get from the query. An
+ * asynchronous query may also be made for any data that we do not already have. Some queries,
+ * such as those for voicemail and emergency call information, will not perform an additional
+ * asynchronous query.
+ */
+ final CallerInfo callerInfo =
+ CallerInfoUtils.getCallerInfoForCall(
+ mContext,
+ call,
+ new DialerCallCookieWrapper(callId, call.getNumberPresentation()),
+ new FindInfoCallback(isIncoming));
+
+ updateCallerInfoInCacheOnAnyThread(
+ callId, call.getNumberPresentation(), callerInfo, isIncoming, false);
+ sendInfoNotifications(callId, mInfoMap.get(callId));
+ }
+
+ @AnyThread
+ private void updateCallerInfoInCacheOnAnyThread(
+ String callId,
+ int numberPresentation,
+ CallerInfo callerInfo,
+ boolean isIncoming,
+ boolean didLocalLookup) {
+ int presentationMode = numberPresentation;
+ if (callerInfo.contactExists
+ || callerInfo.isEmergencyNumber()
+ || callerInfo.isVoiceMailNumber()) {
+ presentationMode = TelecomManager.PRESENTATION_ALLOWED;
+ }
+
+ synchronized (mInfoMap) {
+ ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ // Ensure we always have a cacheEntry. Replace the existing entry if
+ // it has no name or if we found a local contact.
+ if (cacheEntry == null
+ || TextUtils.isEmpty(cacheEntry.namePrimary)
+ || callerInfo.contactExists) {
+ cacheEntry = buildEntry(mContext, callerInfo, presentationMode, isIncoming);
+ mInfoMap.put(callId, cacheEntry);
+ }
+ if (didLocalLookup) {
+ // Before issuing a request for more data from other services, we only check that the
+ // contact wasn't found in the local DB. We don't check the if the cache entry already
+ // has a name because we allow overriding cnap data with data from other services.
+ if (!callerInfo.contactExists && mPhoneNumberService != null) {
+ Log.d(TAG, "Contact lookup. Local contacts miss, checking remote");
+ final PhoneNumberServiceListener listener = new PhoneNumberServiceListener(callId);
+ mPhoneNumberService.getPhoneNumberInfo(cacheEntry.number, listener, listener, isIncoming);
+ } else if (cacheEntry.displayPhotoUri != null) {
+ Log.d(TAG, "Contact lookup. Local contact found, starting image load");
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ cacheEntry.hasPhotoToLoad = true;
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ mContext,
+ cacheEntry.displayPhotoUri,
+ ContactInfoCache.this,
+ callId);
+ }
+ }
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. Update contact photo
+ * when image is loaded in worker thread.
+ */
+ @WorkerThread
+ @Override
+ public void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ Assert.isWorkerThread();
+ loadImage(photo, photoIcon, cookie);
+ }
+
+ private void loadImage(Drawable photo, Bitmap photoIcon, Object cookie) {
+ Log.d(this, "Image load complete with context: ", mContext);
+ // TODO: may be nice to update the image view again once the newer one
+ // is available on contacts database.
+ String callId = (String) cookie;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+
+ if (entry == null) {
+ Log.e(this, "Image Load received for empty search entry.");
+ clearCallbacks(callId);
+ return;
+ }
+
+ Log.d(this, "setting photo for entry: ", entry);
+
+ // Conference call icons are being handled in CallCardPresenter.
+ if (photo != null) {
+ Log.v(this, "direct drawable: ", photo);
+ entry.photo = photo;
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else if (photoIcon != null) {
+ Log.v(this, "photo icon: ", photoIcon);
+ entry.photo = new BitmapDrawable(mContext.getResources(), photoIcon);
+ entry.photoType = ContactPhotoType.CONTACT;
+ } else {
+ Log.v(this, "unknown photo");
+ entry.photo = null;
+ entry.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface. make sure that the
+ * call state is reflected after the image is loaded.
+ */
+ @MainThread
+ @Override
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ Assert.isMainThread();
+ String callId = (String) cookie;
+ ContactCacheEntry entry = mInfoMap.get(callId);
+ sendImageNotifications(callId, entry);
+
+ clearCallbacks(callId);
+ }
+
+ /** Blows away the stored cache values. */
+ public void clearCache() {
+ mInfoMap.clear();
+ mCallBacks.clear();
+ }
+
+ private ContactCacheEntry buildEntry(
+ Context context, CallerInfo info, int presentation, boolean isIncoming) {
+ final ContactCacheEntry cce = new ContactCacheEntry();
+ populateCacheEntry(context, info, cce, presentation, isIncoming);
+
+ // This will only be true for emergency numbers
+ if (info.photoResource != 0) {
+ cce.photo = context.getResources().getDrawable(info.photoResource);
+ } else if (info.isCachedPhotoCurrent) {
+ if (info.cachedPhoto != null) {
+ cce.photo = info.cachedPhoto;
+ cce.photoType = ContactPhotoType.CONTACT;
+ } else {
+ cce.photo = getDefaultContactPhotoDrawable();
+ cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ }
+ } else if (info.contactDisplayPhotoUri == null) {
+ cce.photo = getDefaultContactPhotoDrawable();
+ cce.photoType = ContactPhotoType.DEFAULT_PLACEHOLDER;
+ } else {
+ cce.displayPhotoUri = info.contactDisplayPhotoUri;
+ cce.photo = null;
+ }
+
+ // Support any contact id in N because QuickContacts in N starts supporting enterprise
+ // contact id
+ if (info.lookupKeyOrNull != null
+ && (VERSION.SDK_INT >= VERSION_CODES.N || info.contactIdOrZero != 0)) {
+ cce.lookupUri = Contacts.getLookupUri(info.contactIdOrZero, info.lookupKeyOrNull);
+ } else {
+ Log.v(TAG, "lookup key is null or contact ID is 0 on M. Don't create a lookup uri.");
+ cce.lookupUri = null;
+ }
+
+ cce.lookupKey = info.lookupKeyOrNull;
+ cce.contactRingtoneUri = info.contactRingtoneUri;
+ if (cce.contactRingtoneUri == null || Uri.EMPTY.equals(cce.contactRingtoneUri)) {
+ cce.contactRingtoneUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
+ }
+
+ return cce;
+ }
+
+ /** Sends the updated information to call the callbacks for the entry. */
+ private void sendInfoNotifications(String callId, ContactCacheEntry entry) {
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onContactInfoComplete(callId, entry);
+ }
+ }
+ }
+
+ private void sendImageNotifications(String callId, ContactCacheEntry entry) {
+ final Set<ContactInfoCacheCallback> callBacks = mCallBacks.get(callId);
+ if (callBacks != null && entry.photo != null) {
+ for (ContactInfoCacheCallback callBack : callBacks) {
+ callBack.onImageLoadComplete(callId, entry);
+ }
+ }
+ }
+
+ private void clearCallbacks(String callId) {
+ mCallBacks.remove(callId);
+ }
+
+ public Drawable getDefaultContactPhotoDrawable() {
+ if (mDefaultContactPhotoDrawable == null) {
+ mDefaultContactPhotoDrawable =
+ mContext.getResources().getDrawable(R.drawable.img_no_image_automirrored);
+ }
+ return mDefaultContactPhotoDrawable;
+ }
+
+ public Drawable getConferenceDrawable() {
+ if (mConferencePhotoDrawable == null) {
+ mConferencePhotoDrawable =
+ mContext.getResources().getDrawable(R.drawable.img_conference_automirrored);
+ }
+ return mConferencePhotoDrawable;
+ }
+
+ /** Callback interface for the contact query. */
+ public interface ContactInfoCacheCallback {
+
+ void onContactInfoComplete(String callId, ContactCacheEntry entry);
+
+ void onImageLoadComplete(String callId, ContactCacheEntry entry);
+ }
+
+ /** This is cached contact info, which should be the ONLY info used by UI. */
+ public static class ContactCacheEntry {
+
+ public String namePrimary;
+ public String nameAlternative;
+ public String number;
+ public String location;
+ public String label;
+ public Drawable photo;
+ @ContactPhotoType public int photoType;
+ public boolean isSipCall;
+ // Note in cache entry whether this is a pending async loading action to know whether to
+ // wait for its callback or not.
+ public boolean hasPhotoToLoad;
+ /** This will be used for the "view" notification. */
+ public Uri contactUri;
+ /** Either a display photo or a thumbnail URI. */
+ public Uri displayPhotoUri;
+
+ public Uri lookupUri; // Sent to NotificationMananger
+ public String lookupKey;
+ public int contactLookupResult = ContactLookupResult.Type.NOT_FOUND;
+ public long userType = ContactsUtils.USER_TYPE_CURRENT;
+ public Uri contactRingtoneUri;
+
+ @Override
+ public String toString() {
+ return "ContactCacheEntry{"
+ + "name='"
+ + MoreStrings.toSafeString(namePrimary)
+ + '\''
+ + ", nameAlternative='"
+ + MoreStrings.toSafeString(nameAlternative)
+ + '\''
+ + ", number='"
+ + MoreStrings.toSafeString(number)
+ + '\''
+ + ", location='"
+ + MoreStrings.toSafeString(location)
+ + '\''
+ + ", label='"
+ + label
+ + '\''
+ + ", photo="
+ + photo
+ + ", isSipCall="
+ + isSipCall
+ + ", contactUri="
+ + contactUri
+ + ", displayPhotoUri="
+ + displayPhotoUri
+ + ", contactLookupResult="
+ + contactLookupResult
+ + ", userType="
+ + userType
+ + ", contactRingtoneUri="
+ + contactRingtoneUri
+ + '}';
+ }
+ }
+
+ private static final class DialerCallCookieWrapper {
+ public final String callId;
+ public final int numberPresentation;
+
+ public DialerCallCookieWrapper(String callId, int numberPresentation) {
+ this.callId = callId;
+ this.numberPresentation = numberPresentation;
+ }
+ }
+
+ private class FindInfoCallback implements OnQueryCompleteListener {
+
+ private final boolean mIsIncoming;
+
+ public FindInfoCallback(boolean isIncoming) {
+ mIsIncoming = isIncoming;
+ }
+
+ @Override
+ public void onDataLoaded(int token, Object cookie, CallerInfo ci) {
+ Assert.isWorkerThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ updateCallerInfoInCacheOnAnyThread(cw.callId, cw.numberPresentation, ci, mIsIncoming, true);
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
+ Assert.isMainThread();
+ DialerCallCookieWrapper cw = (DialerCallCookieWrapper) cookie;
+ String callId = cw.callId;
+ ContactCacheEntry cacheEntry = mInfoMap.get(callId);
+ // This may happen only when InCallPresenter attempt to cleanup.
+ if (cacheEntry == null) {
+ Log.w(TAG, "Contact lookup done, but cache entry is not found.");
+ clearCallbacks(callId);
+ return;
+ }
+ sendInfoNotifications(callId, cacheEntry);
+ if (!cacheEntry.hasPhotoToLoad) {
+ if (callerInfo.contactExists) {
+ Log.d(TAG, "Contact lookup done. Local contact found, no image.");
+ } else {
+ Log.d(
+ TAG,
+ "Contact lookup done. Local contact not found and"
+ + " no remote lookup service available.");
+ }
+ clearCallbacks(callId);
+ }
+ }
+ }
+
+ class PhoneNumberServiceListener
+ implements PhoneNumberService.NumberLookupListener, PhoneNumberService.ImageLookupListener {
+
+ private final String mCallId;
+
+ PhoneNumberServiceListener(String callId) {
+ mCallId = callId;
+ }
+
+ @Override
+ public void onPhoneNumberInfoComplete(final PhoneNumberService.PhoneNumberInfo info) {
+ // If we got a miss, this is the end of the lookup pipeline,
+ // so clear the callbacks and return.
+ if (info == null) {
+ Log.d(TAG, "Contact lookup done. Remote contact not found.");
+ clearCallbacks(mCallId);
+ return;
+ }
+
+ ContactCacheEntry entry = new ContactCacheEntry();
+ entry.namePrimary = info.getDisplayName();
+ entry.number = info.getNumber();
+ entry.contactLookupResult = info.getLookupSource();
+ final int type = info.getPhoneType();
+ final String label = info.getPhoneLabel();
+ if (type == Phone.TYPE_CUSTOM) {
+ entry.label = label;
+ } else {
+ final CharSequence typeStr = Phone.getTypeLabel(mContext.getResources(), type, label);
+ entry.label = typeStr == null ? null : typeStr.toString();
+ }
+ synchronized (mInfoMap) {
+ final ContactCacheEntry oldEntry = mInfoMap.get(mCallId);
+ if (oldEntry != null) {
+ // Location is only obtained from local lookup so persist
+ // the value for remote lookups. Once we have a name this
+ // field is no longer used; it is persisted here in case
+ // the UI is ever changed to use it.
+ entry.location = oldEntry.location;
+ // Contact specific ringtone is obtained from local lookup.
+ entry.contactRingtoneUri = oldEntry.contactRingtoneUri;
+ }
+
+ // If no image and it's a business, switch to using the default business avatar.
+ if (info.getImageUrl() == null && info.isBusiness()) {
+ Log.d(TAG, "Business has no image. Using default.");
+ entry.photo = mContext.getResources().getDrawable(R.drawable.img_business);
+ entry.photoType = ContactPhotoType.BUSINESS;
+ }
+
+ mInfoMap.put(mCallId, entry);
+ }
+ sendInfoNotifications(mCallId, entry);
+
+ entry.hasPhotoToLoad = info.getImageUrl() != null;
+
+ // If there is no image then we should not expect another callback.
+ if (!entry.hasPhotoToLoad) {
+ // We're done, so clear callbacks
+ clearCallbacks(mCallId);
+ }
+ }
+
+ @Override
+ public void onImageFetchComplete(Bitmap bitmap) {
+ loadImage(null, bitmap, mCallId);
+ onImageLoadComplete(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE, null, bitmap, mCallId);
+ }
+ }
+}
diff --git a/java/com/android/incallui/ContactsAsyncHelper.java b/java/com/android/incallui/ContactsAsyncHelper.java
new file mode 100644
index 000000000..08ff74d0e
--- /dev/null
+++ b/java/com/android/incallui/ContactsAsyncHelper.java
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui;
+
+import android.app.Notification;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Helper class for loading contacts photo asynchronously. */
+public class ContactsAsyncHelper {
+
+ /** Interface for a WorkerHandler result return. */
+ public interface OnImageLoadCompleteListener {
+
+ /**
+ * Called when the image load is complete. Must be called in main thread.
+ *
+ * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
+ * Uri, OnImageLoadCompleteListener, Object)}.
+ * @param photo Drawable object obtained by the async load.
+ * @param photoIcon Bitmap object obtained by the async load.
+ * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context,
+ * Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original cookie is null.
+ */
+ @MainThread
+ void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie);
+
+ /** Called when image is loaded to udpate data. Must be called in worker thread. */
+ @WorkerThread
+ void onImageLoaded(int token, Drawable photo, Bitmap photoIcon, Object cookie);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+ /** Handler run on a worker thread to load photo asynchronously. */
+ private static Handler sThreadHandler;
+ /** For forcing the system to call its constructor */
+ @SuppressWarnings("unused")
+ private static ContactsAsyncHelper sInstance;
+
+ static {
+ sInstance = new ContactsAsyncHelper();
+ }
+
+ private final Handler mResultHandler =
+ /** A handler that handles message to call listener notifying UI change on main thread. */
+ new Handler(Looper.getMainLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ if (args.listener != null) {
+ Log.d(
+ this,
+ "Notifying listener: "
+ + args.listener.toString()
+ + " image: "
+ + args.displayPhotoUri
+ + " completed");
+ args.listener.onImageLoadComplete(
+ msg.what, args.photo, args.photoIcon, args.cookie);
+ }
+ break;
+ default:
+ }
+ }
+ };
+
+ /** Private constructor for static class */
+ private ContactsAsyncHelper() {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ /**
+ * Starts an asynchronous image load. After finishing the load, {@link
+ * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)} will be called.
+ *
+ * @param token Arbitrary integer which will be returned as the first argument of {@link
+ * OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * @param context Context object used to do the time-consuming operation.
+ * @param displayPhotoUri Uri to be used to fetch the photo
+ * @param listener Callback object which will be used when the asynchronous load is done. Can be
+ * null, which means only the asynchronous load is done while there's no way to obtain the
+ * loaded photos.
+ * @param cookie Arbitrary object the caller wants to remember, which will become the fourth
+ * argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap,
+ * Object)}. Can be null, at which the callback will also has null for the argument.
+ */
+ public static final void startObtainPhotoAsync(
+ int token,
+ Context context,
+ Uri displayPhotoUri,
+ OnImageLoadCompleteListener listener,
+ Object cookie) {
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (displayPhotoUri == null) {
+ Log.e("startObjectPhotoAsync", "Uri is missing");
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.displayPhotoUri = displayPhotoUri;
+ args.listener = listener;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ Log.d(
+ "startObjectPhotoAsync",
+ "Begin loading image: " + args.displayPhotoUri + ", displaying default image for now.");
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+ private static final class WorkerArgs {
+
+ public Context context;
+ public Uri displayPhotoUri;
+ public Drawable photo;
+ public Bitmap photoIcon;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ }
+
+ /** Thread worker class that handles the task of opening the stream and loading the images. */
+ private class WorkerHandler extends Handler {
+
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = null;
+ try {
+ try {
+ inputStream = args.context.getContentResolver().openInputStream(args.displayPhotoUri);
+ } catch (Exception e) {
+ Log.e(this, "Error opening photo input stream", e);
+ }
+
+ if (inputStream != null) {
+ args.photo = Drawable.createFromStream(inputStream, args.displayPhotoUri.toString());
+
+ // This assumes Drawable coming from contact database is usually
+ // BitmapDrawable and thus we can have (down)scaled version of it.
+ args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+ Log.d(
+ ContactsAsyncHelper.this,
+ "Loading image: "
+ + msg.arg1
+ + " token: "
+ + msg.what
+ + " image URI: "
+ + args.displayPhotoUri);
+ } else {
+ args.photo = null;
+ args.photoIcon = null;
+ Log.d(
+ ContactsAsyncHelper.this,
+ "Problem with image: "
+ + msg.arg1
+ + " token: "
+ + msg.what
+ + " image URI: "
+ + args.displayPhotoUri
+ + ", using default image.");
+ }
+ if (args.listener != null) {
+ args.listener.onImageLoaded(msg.what, args.photo, args.photoIcon, args.cookie);
+ }
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(this, "Unable to close input stream.", e);
+ }
+ }
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+
+ /**
+ * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might return
+ * null when the given Drawable isn't BitmapDrawable, or if the system fails to create a scaled
+ * Bitmap for the Drawable.
+ */
+ private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+ if (!(photo instanceof BitmapDrawable)) {
+ return null;
+ }
+ int iconSize = context.getResources().getDimensionPixelSize(R.dimen.notification_icon_size);
+ Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+ int orgWidth = orgBitmap.getWidth();
+ int orgHeight = orgBitmap.getHeight();
+ int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+ // We want downscaled one only when the original icon is too big.
+ if (longerEdge > iconSize) {
+ float ratio = ((float) longerEdge) / iconSize;
+ int newWidth = (int) (orgWidth / ratio);
+ int newHeight = (int) (orgHeight / ratio);
+ // If the longer edge is much longer than the shorter edge, the latter may
+ // become 0 which will cause a crash.
+ if (newWidth <= 0 || newHeight <= 0) {
+ Log.w(this, "Photo icon's width or height become 0.");
+ return null;
+ }
+
+ // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+ // should be smaller than the original.
+ return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+ } else {
+ return orgBitmap;
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/ContactsPreferencesFactory.java b/java/com/android/incallui/ContactsPreferencesFactory.java
new file mode 100644
index 000000000..429de7bc9
--- /dev/null
+++ b/java/com/android/incallui/ContactsPreferencesFactory.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.incallui;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.v4.os.UserManagerCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+
+/** Factory class for {@link ContactsPreferences}. */
+public class ContactsPreferencesFactory {
+
+ private static boolean sUseTestInstance;
+ private static ContactsPreferences sTestInstance;
+
+ /**
+ * Creates a new {@link ContactsPreferences} object if possible.
+ *
+ * @param context the context to use when creating the ContactsPreferences.
+ * @return a new ContactsPreferences object or {@code null} if the user is locked.
+ */
+ @Nullable
+ public static ContactsPreferences newContactsPreferences(Context context) {
+ if (sUseTestInstance) {
+ return sTestInstance;
+ }
+ if (UserManagerCompat.isUserUnlocked(context)) {
+ return new ContactsPreferences(context);
+ }
+ return null;
+ }
+
+ /**
+ * Sets the instance to be returned by all calls to {@link #newContactsPreferences(Context)}.
+ *
+ * @param testInstance the instance to return.
+ */
+ static void setTestInstance(ContactsPreferences testInstance) {
+ sUseTestInstance = true;
+ sTestInstance = testInstance;
+ }
+}
diff --git a/java/com/android/incallui/DialpadFragment.java b/java/com/android/incallui/DialpadFragment.java
new file mode 100644
index 000000000..7f494aa61
--- /dev/null
+++ b/java/com/android/incallui/DialpadFragment.java
@@ -0,0 +1,461 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.method.DialerKeyListener;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.dialpadview.DialpadKeyButton;
+import com.android.dialer.dialpadview.DialpadKeyButton.OnPressedListener;
+import com.android.dialer.dialpadview.DialpadView;
+import com.android.incallui.DialpadPresenter.DialpadUi;
+import com.android.incallui.baseui.BaseFragment;
+import java.util.Map;
+
+/** Fragment for call control buttons */
+public class DialpadFragment extends BaseFragment<DialpadPresenter, DialpadUi>
+ implements DialpadUi, OnKeyListener, OnClickListener, OnPressedListener {
+
+ /** Hash Map to map a view id to a character */
+ private static final Map<Integer, Character> mDisplayMap = new ArrayMap<>();
+
+ /** Set up the static maps */
+ static {
+ // Map the buttons to the display characters
+ mDisplayMap.put(R.id.one, '1');
+ mDisplayMap.put(R.id.two, '2');
+ mDisplayMap.put(R.id.three, '3');
+ mDisplayMap.put(R.id.four, '4');
+ mDisplayMap.put(R.id.five, '5');
+ mDisplayMap.put(R.id.six, '6');
+ mDisplayMap.put(R.id.seven, '7');
+ mDisplayMap.put(R.id.eight, '8');
+ mDisplayMap.put(R.id.nine, '9');
+ mDisplayMap.put(R.id.zero, '0');
+ mDisplayMap.put(R.id.pound, '#');
+ mDisplayMap.put(R.id.star, '*');
+ }
+
+ private final int[] mButtonIds =
+ new int[] {
+ R.id.zero,
+ 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.pound
+ };
+ private EditText mDtmfDialerField;
+ // KeyListener used with the "dialpad digits" EditText widget.
+ private DTMFKeyListener mDialerKeyListener;
+ private DialpadView mDialpadView;
+ private int mCurrentTextColor;
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.dialpad_back) {
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ Log.d(this, "onKey: keyCode " + keyCode + ", view " + v);
+
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
+ int viewId = v.getId();
+ if (mDisplayMap.containsKey(viewId)) {
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (event.getRepeatCount() == 0) {
+ getPresenter().processDtmf(mDisplayMap.get(viewId));
+ }
+ break;
+ case KeyEvent.ACTION_UP:
+ getPresenter().stopDtmf();
+ break;
+ }
+ // do not return true [handled] here, since we want the
+ // press / click animation to be handled by the framework.
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public DialpadPresenter createPresenter() {
+ return new DialpadPresenter();
+ }
+
+ @Override
+ public DialpadPresenter.DialpadUi getUi() {
+ return this;
+ }
+
+ // TODO Adds hardware keyboard listener
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ final View parent = inflater.inflate(R.layout.incall_dialpad_fragment, container, false);
+ mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view);
+ mDialpadView.setCanDigitsBeEdited(false);
+ mDialpadView.setBackgroundResource(R.color.incall_dialpad_background);
+ mDtmfDialerField = (EditText) parent.findViewById(R.id.digits);
+ if (mDtmfDialerField != null) {
+ mDialerKeyListener = new DTMFKeyListener();
+ mDtmfDialerField.setKeyListener(mDialerKeyListener);
+ // remove the long-press context menus that support
+ // the edit (copy / paste / select) functions.
+ mDtmfDialerField.setLongClickable(false);
+ mDtmfDialerField.setElegantTextHeight(false);
+ configureKeypadListeners();
+ }
+ View backButton = mDialpadView.findViewById(R.id.dialpad_back);
+ backButton.setVisibility(View.VISIBLE);
+ backButton.setOnClickListener(this);
+
+ return parent;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateColors();
+ }
+
+ public void updateColors() {
+ int textColor = InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor();
+
+ if (mCurrentTextColor == textColor) {
+ return;
+ }
+
+ DialpadKeyButton dialpadKey;
+ for (int i = 0; i < mButtonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]);
+ ((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor);
+ }
+
+ mCurrentTextColor = textColor;
+ }
+
+ @Override
+ public void onDestroyView() {
+ mDialerKeyListener = null;
+ super.onDestroyView();
+ }
+
+ /**
+ * Getter for Dialpad text.
+ *
+ * @return String containing current Dialpad EditText text.
+ */
+ public String getDtmfText() {
+ return mDtmfDialerField.getText().toString();
+ }
+
+ /**
+ * Sets the Dialpad text field with some text.
+ *
+ * @param text Text to set Dialpad EditText to.
+ */
+ public void setDtmfText(String text) {
+ mDtmfDialerField.setText(PhoneNumberUtilsCompat.createTtsSpannable(text));
+ }
+
+ @Override
+ public void setVisible(boolean on) {
+ if (on) {
+ getView().setVisibility(View.VISIBLE);
+ } else {
+ getView().setVisibility(View.INVISIBLE);
+ }
+ }
+
+ /** Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. */
+ public void animateShowDialpad() {
+ final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
+ dialpadView.animateShow();
+ }
+
+ @Override
+ public void appendDigitsToField(char digit) {
+ if (mDtmfDialerField != null) {
+ // TODO: maybe *don't* manually append this digit if
+ // mDialpadDigits is focused and this key came from the HW
+ // keyboard, since in that case the EditText field will
+ // get the key event directly and automatically appends
+ // whetever the user types.
+ // (Or, a cleaner fix would be to just make mDialpadDigits
+ // *not* handle HW key presses. That seems to be more
+ // complicated than just setting focusable="false" on it,
+ // though.)
+ mDtmfDialerField.getText().append(digit);
+ }
+ }
+
+ /** Called externally (from InCallScreen) to play a DTMF Tone. */
+ /* package */ boolean onDialerKeyDown(KeyEvent event) {
+ Log.d(this, "Notifying dtmf key down.");
+ if (mDialerKeyListener != null) {
+ return mDialerKeyListener.onKeyDown(event);
+ } else {
+ return false;
+ }
+ }
+
+ /** Called externally (from InCallScreen) to cancel the last DTMF Tone played. */
+ public boolean onDialerKeyUp(KeyEvent event) {
+ Log.d(this, "Notifying dtmf key up.");
+ if (mDialerKeyListener != null) {
+ return mDialerKeyListener.onKeyUp(event);
+ } else {
+ return false;
+ }
+ }
+
+ private void configureKeypadListeners() {
+ DialpadKeyButton dialpadKey;
+ for (int i = 0; i < mButtonIds.length; i++) {
+ dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]);
+ dialpadKey.setOnKeyListener(this);
+ dialpadKey.setOnClickListener(this);
+ dialpadKey.setOnPressedListener(this);
+ }
+ }
+
+ @Override
+ public void onPressed(View view, boolean pressed) {
+ if (pressed && mDisplayMap.containsKey(view.getId())) {
+ Log.d(this, "onPressed: " + pressed + " " + mDisplayMap.get(view.getId()));
+ getPresenter().processDtmf(mDisplayMap.get(view.getId()));
+ }
+ if (!pressed) {
+ Log.d(this, "onPressed: " + pressed);
+ getPresenter().stopDtmf();
+ }
+ }
+
+ /**
+ * LinearLayout with getter and setter methods for the translationY property using floats, for
+ * animation purposes.
+ */
+ public static class DialpadSlidingLinearLayout extends LinearLayout {
+
+ public DialpadSlidingLinearLayout(Context context) {
+ super(context);
+ }
+
+ public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public float getYFraction() {
+ final int height = getHeight();
+ if (height == 0) {
+ return 0;
+ }
+ return getTranslationY() / height;
+ }
+
+ public void setYFraction(float yFraction) {
+ setTranslationY(yFraction * getHeight());
+ }
+ }
+
+ /**
+ * Our own key listener, specialized for dealing with DTMF codes. 1. Ignore the backspace since it
+ * is irrelevant. 2. Allow ONLY valid DTMF characters to generate a tone and be sent as a DTMF
+ * code. 3. All other remaining characters are handled by the superclass.
+ *
+ * <p>This code is purely here to handle events from the hardware keyboard while the DTMF dialpad
+ * is up.
+ */
+ private class DTMFKeyListener extends DialerKeyListener {
+
+ /**
+ * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} These are the valid
+ * dtmf characters.
+ */
+ public final char[] DTMF_CHARACTERS =
+ new char[] {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'};
+
+ private DTMFKeyListener() {
+ super();
+ }
+
+ /** Overriden to return correct DTMF-dialable characters. */
+ @Override
+ protected char[] getAcceptedChars() {
+ return DTMF_CHARACTERS;
+ }
+
+ /** special key listener ignores backspace. */
+ @Override
+ public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Overriden so that with each valid button press, we start sending a dtmf code and play a local
+ * dtmf tone.
+ */
+ @Override
+ public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) {
+ // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view);
+
+ // find the character
+ char c = (char) lookup(event, content);
+
+ // if not a long press, and parent onKeyDown accepts the input
+ if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) {
+
+ boolean keyOK = ok(getAcceptedChars(), c);
+
+ // if the character is a valid dtmf code, start playing the tone and send the
+ // code.
+ if (keyOK) {
+ Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
+ getPresenter().processDtmf(c);
+ } else {
+ Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input.");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Overriden so that with each valid button up, we stop sending a dtmf code and the dtmf tone.
+ */
+ @Override
+ public boolean onKeyUp(View view, Editable content, int keyCode, KeyEvent event) {
+ // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view);
+
+ super.onKeyUp(view, content, keyCode, event);
+
+ // find the character
+ char c = (char) lookup(event, content);
+
+ boolean keyOK = ok(getAcceptedChars(), c);
+
+ if (keyOK) {
+ Log.d(this, "Stopping the tone for '" + c + "'");
+ getPresenter().stopDtmf();
+ return true;
+ }
+
+ return false;
+ }
+
+ /** Handle individual keydown events when we DO NOT have an Editable handy. */
+ public boolean onKeyDown(KeyEvent event) {
+ char c = lookup(event);
+ Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'");
+
+ // if not a long press, and parent onKeyDown accepts the input
+ if (event.getRepeatCount() == 0 && c != 0) {
+ // if the character is a valid dtmf code, start playing the tone and send the
+ // code.
+ if (ok(getAcceptedChars(), c)) {
+ Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
+ getPresenter().processDtmf(c);
+ return true;
+ } else {
+ Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input.");
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handle individual keyup events.
+ *
+ * @param event is the event we are trying to stop. If this is null, then we just force-stop the
+ * last tone without checking if the event is an acceptable dialer event.
+ */
+ public boolean onKeyUp(KeyEvent event) {
+ if (event == null) {
+ //the below piece of code sends stopDTMF event unnecessarily even when a null event
+ //is received, hence commenting it.
+ /*if (DBG) log("Stopping the last played tone.");
+ stopTone();*/
+ return true;
+ }
+
+ char c = lookup(event);
+ Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'");
+
+ // TODO: stopTone does not take in character input, we may want to
+ // consider checking for this ourselves.
+ if (ok(getAcceptedChars(), c)) {
+ Log.d(this, "Stopping the tone for '" + c + "'");
+ getPresenter().stopDtmf();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Find the Dialer Key mapped to this event.
+ *
+ * @return The char value of the input event, otherwise 0 if no matching character was found.
+ */
+ private char lookup(KeyEvent event) {
+ // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup}
+ int meta = event.getMetaState();
+ int number = event.getNumber();
+
+ if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) {
+ int match = event.getMatch(getAcceptedChars(), meta);
+ number = (match != 0) ? match : number;
+ }
+
+ return (char) number;
+ }
+
+ /** Check to see if the keyEvent is dialable. */
+ boolean isKeyEventAcceptable(KeyEvent event) {
+ return (ok(getAcceptedChars(), lookup(event)));
+ }
+ }
+}
diff --git a/java/com/android/incallui/DialpadPresenter.java b/java/com/android/incallui/DialpadPresenter.java
new file mode 100644
index 000000000..7a784c279
--- /dev/null
+++ b/java/com/android/incallui/DialpadPresenter.java
@@ -0,0 +1,91 @@
+/*
+ * 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.incallui;
+
+import android.telephony.PhoneNumberUtils;
+import com.android.incallui.DialpadPresenter.DialpadUi;
+import com.android.incallui.baseui.Presenter;
+import com.android.incallui.baseui.Ui;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.TelecomAdapter;
+
+/** Logic for call buttons. */
+public class DialpadPresenter extends Presenter<DialpadUi>
+ implements InCallPresenter.InCallStateListener {
+
+ private DialerCall mCall;
+
+ @Override
+ public void onUiReady(DialpadUi ui) {
+ super.onUiReady(ui);
+ InCallPresenter.getInstance().addListener(this);
+ mCall = CallList.getInstance().getOutgoingOrActive();
+ }
+
+ @Override
+ public void onUiUnready(DialpadUi ui) {
+ super.onUiUnready(ui);
+ InCallPresenter.getInstance().removeListener(this);
+ }
+
+ @Override
+ public void onStateChange(
+ InCallPresenter.InCallState oldState,
+ InCallPresenter.InCallState newState,
+ CallList callList) {
+ mCall = callList.getOutgoingOrActive();
+ Log.d(this, "DialpadPresenter mCall = " + mCall);
+ }
+
+ /**
+ * Processes the specified digit as a DTMF key, by playing the appropriate DTMF tone, and
+ * appending the digit to the EditText field that displays the DTMF digits sent so far.
+ */
+ public final void processDtmf(char c) {
+ Log.d(this, "Processing dtmf key " + c);
+ // if it is a valid key, then update the display and send the dtmf tone.
+ if (PhoneNumberUtils.is12Key(c) && mCall != null) {
+ Log.d(this, "updating display and sending dtmf tone for '" + c + "'");
+
+ // Append this key to the "digits" widget.
+ DialpadUi dialpadUi = getUi();
+ if (dialpadUi != null) {
+ dialpadUi.appendDigitsToField(c);
+ }
+ // Plays the tone through Telecom.
+ TelecomAdapter.getInstance().playDtmfTone(mCall.getId(), c);
+ } else {
+ Log.d(this, "ignoring dtmf request for '" + c + "'");
+ }
+ }
+
+ /** Stops the local tone based on the phone type. */
+ public void stopDtmf() {
+ if (mCall != null) {
+ Log.d(this, "stopping remote tone");
+ TelecomAdapter.getInstance().stopDtmfTone(mCall.getId());
+ }
+ }
+
+ public interface DialpadUi extends Ui {
+
+ void setVisible(boolean on);
+
+ void appendDigitsToField(char digit);
+ }
+}
diff --git a/java/com/android/incallui/ExternalCallNotifier.java b/java/com/android/incallui/ExternalCallNotifier.java
new file mode 100644
index 000000000..466e12a6d
--- /dev/null
+++ b/java/com/android/incallui/ExternalCallNotifier.java
@@ -0,0 +1,465 @@
+/*
+ * 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.incallui;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.PhoneAccount;
+import android.telecom.VideoProfile;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCallDelegate;
+import com.android.incallui.call.ExternalCallList;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.util.Map;
+
+/**
+ * Handles the display of notifications for "external calls".
+ *
+ * <p>External calls are a representation of a call which is in progress on the user's other device
+ * (e.g. another phone, or a watch).
+ */
+public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener {
+
+ /** Tag used with the notification manager to uniquely identify external call notifications. */
+ private static final String NOTIFICATION_TAG = "EXTERNAL_CALL";
+
+ private static final int SUMMARY_ID = -1;
+ private final Context mContext;
+ private final ContactInfoCache mContactInfoCache;
+ private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>();
+ private int mNextUniqueNotificationId;
+ private ContactsPreferences mContactsPreferences;
+ private boolean mShowingSummary;
+
+ /** Initializes a new instance of the external call notifier. */
+ public ExternalCallNotifier(
+ @NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ mContext = context;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+ mContactInfoCache = contactInfoCache;
+ }
+
+ /**
+ * Handles the addition of a new external call by showing a new notification. Triggered by {@link
+ * CallList#onCallAdded(android.telecom.Call)}.
+ */
+ @Override
+ public void onExternalCallAdded(android.telecom.Call call) {
+ Log.i(this, "onExternalCallAdded " + call);
+ if (mNotifications.containsKey(call)) {
+ throw new IllegalArgumentException();
+ }
+ NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++);
+ mNotifications.put(call, info);
+
+ showNotifcation(info);
+ }
+
+ /**
+ * Handles the removal of an external call by hiding its associated notification. Triggered by
+ * {@link CallList#onCallRemoved(android.telecom.Call)}.
+ */
+ @Override
+ public void onExternalCallRemoved(android.telecom.Call call) {
+ Log.i(this, "onExternalCallRemoved " + call);
+
+ dismissNotification(call);
+ }
+
+ /** Handles updates to an external call. */
+ @Override
+ public void onExternalCallUpdated(Call call) {
+ if (!mNotifications.containsKey(call)) {
+ throw new IllegalArgumentException();
+ }
+ postNotification(mNotifications.get(call));
+ }
+
+ @Override
+ public void onExternalCallPulled(Call call) {
+ // no-op; if an external call is pulled, it will be removed via onExternalCallRemoved.
+ }
+
+ /**
+ * Initiates a call pull given a notification ID.
+ *
+ * @param notificationId The notification ID associated with the external call which is to be
+ * pulled.
+ */
+ @TargetApi(VERSION_CODES.N_MR1)
+ public void pullExternalCall(int notificationId) {
+ for (NotificationInfo info : mNotifications.values()) {
+ if (info.getNotificationId() == notificationId
+ && CallCompat.canPullExternalCall(info.getCall())) {
+ info.getCall().pullExternalCall();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Shows a notification for a new external call. Performs a contact cache lookup to find any
+ * associated photo and information for the call.
+ */
+ private void showNotifcation(final NotificationInfo info) {
+ // We make a call to the contact info cache to query for supplemental data to what the
+ // call provides. This includes the contact name and photo.
+ // This callback will always get called immediately and synchronously with whatever data
+ // it has available, and may make a subsequent call later (same thread) if it had to
+ // call into the contacts provider for more data.
+ DialerCall dialerCall =
+ new DialerCall(
+ mContext,
+ new DialerCallDelegateStub(),
+ info.getCall(),
+ new LatencyReport(),
+ false /* registerCallback */);
+
+ mContactInfoCache.findInfo(
+ dialerCall,
+ false /* isIncoming */,
+ new ContactInfoCache.ContactInfoCacheCallback() {
+ @Override
+ public void onContactInfoComplete(
+ String callId, ContactInfoCache.ContactCacheEntry entry) {
+
+ // Ensure notification still exists as the external call could have been
+ // removed during async contact info lookup.
+ if (mNotifications.containsKey(info.getCall())) {
+ saveContactInfo(info, entry);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactInfoCache.ContactCacheEntry entry) {
+
+ // Ensure notification still exists as the external call could have been
+ // removed during async contact info lookup.
+ if (mNotifications.containsKey(info.getCall())) {
+ savePhoto(info, entry);
+ }
+ }
+ });
+ }
+
+ /** Dismisses a notification for an external call. */
+ private void dismissNotification(Call call) {
+ if (!mNotifications.containsKey(call)) {
+ throw new IllegalArgumentException();
+ }
+
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId());
+
+ mNotifications.remove(call);
+
+ if (mShowingSummary && mNotifications.size() <= 1) {
+ // Where a summary notification is showing and there is now not enough notifications to
+ // necessitate a summary, cancel the summary.
+ notificationManager.cancel(NOTIFICATION_TAG, SUMMARY_ID);
+ mShowingSummary = false;
+
+ // If there is still a single call requiring a notification, re-post the notification as a
+ // standalone notification without a summary notification.
+ if (mNotifications.size() == 1) {
+ postNotification(mNotifications.values().iterator().next());
+ }
+ }
+ }
+
+ /**
+ * Attempts to build a large icon to use for the notification based on the contact info and post
+ * the updated notification to the notification manager.
+ */
+ private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
+ Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall());
+ if (largeIcon != null) {
+ largeIcon = getRoundedIcon(mContext, largeIcon);
+ }
+ info.setLargeIcon(largeIcon);
+ postNotification(info);
+ }
+
+ /**
+ * Builds and stores the contact information the notification will display and posts the updated
+ * notification to the notification manager.
+ */
+ private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) {
+ info.setContentTitle(getContentTitle(mContext, mContactsPreferences, entry, info.getCall()));
+ info.setPersonReference(getPersonReference(entry, info.getCall()));
+ postNotification(info);
+ }
+
+ /** Rebuild an existing or show a new notification given {@link NotificationInfo}. */
+ private void postNotification(NotificationInfo info) {
+ Notification.Builder builder = new Notification.Builder(mContext);
+ // Set notification as ongoing since calls are long-running versus a point-in-time notice.
+ builder.setOngoing(true);
+ // Make the notification prioritized over the other normal notifications.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+ builder.setGroup(NOTIFICATION_TAG);
+
+ boolean isVideoCall = VideoProfile.isVideo(info.getCall().getDetails().getVideoState());
+ // Set the content ("Ongoing call on another device")
+ builder.setContentText(
+ mContext.getString(
+ isVideoCall
+ ? R.string.notification_external_video_call
+ : R.string.notification_external_call));
+ builder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
+ builder.setContentTitle(info.getContentTitle());
+ builder.setLargeIcon(info.getLargeIcon());
+ builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+ builder.addPerson(info.getPersonReference());
+
+ // Where the external call supports being transferred to the local device, add an action
+ // to the notification to initiate the call pull process.
+ if (CallCompat.canPullExternalCall(info.getCall())) {
+
+ Intent intent =
+ new Intent(
+ NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL,
+ null,
+ mContext,
+ NotificationBroadcastReceiver.class);
+ intent.putExtra(
+ NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, info.getNotificationId());
+ builder.addAction(
+ new Notification.Action.Builder(
+ R.drawable.quantum_ic_call_white_24,
+ mContext.getString(
+ isVideoCall
+ ? R.string.notification_take_video_call
+ : R.string.notification_take_call),
+ PendingIntent.getBroadcast(mContext, info.getNotificationId(), intent, 0))
+ .build());
+ }
+
+ /**
+ * This builder is used for the notification shown when the device is locked and the user has
+ * set their notification settings to 'hide sensitive content' {@see
+ * Notification.Builder#setPublicVersion}.
+ */
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder.setSmallIcon(R.drawable.quantum_ic_call_white_24);
+ publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+
+ builder.setPublicVersion(publicBuilder.build());
+ Notification notification = builder.build();
+
+ NotificationManager notificationManager =
+ (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification);
+
+ if (!mShowingSummary && mNotifications.size() > 1) {
+ // If the number of notifications shown is > 1, and we're not already showing a group summary,
+ // build one now. This will ensure the like notifications are grouped together.
+
+ Notification.Builder summary = new Notification.Builder(mContext);
+ // Set notification as ongoing since calls are long-running versus a point-in-time notice.
+ summary.setOngoing(true);
+ // Make the notification prioritized over the other normal notifications.
+ summary.setPriority(Notification.PRIORITY_HIGH);
+ summary.setGroup(NOTIFICATION_TAG);
+ summary.setGroupSummary(true);
+ summary.setSmallIcon(R.drawable.quantum_ic_call_white_24);
+ notificationManager.notify(NOTIFICATION_TAG, SUMMARY_ID, summary.build());
+ mShowingSummary = true;
+ }
+ }
+
+ /**
+ * Finds a large icon to display in a notification for a call. For conference calls, a conference
+ * call icon is used, otherwise if contact info is specified, the user's contact photo or avatar
+ * is used.
+ *
+ * @param context The context.
+ * @param contactInfo The contact cache info.
+ * @param call The call.
+ * @return The large icon to use for the notification.
+ */
+ private @Nullable Bitmap getLargeIconToDisplay(
+ Context context, ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) {
+
+ Bitmap largeIcon = null;
+ if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)
+ && !call.getDetails()
+ .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
+
+ largeIcon = BitmapFactory.decodeResource(context.getResources(), R.drawable.img_conference);
+ }
+ if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+ largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
+ }
+ return largeIcon;
+ }
+
+ /**
+ * Given a bitmap, returns a rounded version of the icon suitable for display in a notification.
+ *
+ * @param context The context.
+ * @param bitmap The bitmap to round.
+ * @return The rounded bitmap.
+ */
+ private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ final int height =
+ (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ final int width =
+ (int) context.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ return BitmapUtil.getRoundedBitmap(bitmap, width, height);
+ }
+
+ /**
+ * Builds a notification content title for a call. If the call is a conference call, it is
+ * identified as such. Otherwise an attempt is made to show an associated contact name or phone
+ * number.
+ *
+ * @param context The context.
+ * @param contactsPreferences Contacts preferences, used to determine the preferred formatting for
+ * contact names.
+ * @param contactInfo The contact info which was looked up in the contact cache.
+ * @param call The call to generate a title for.
+ * @return The content title.
+ */
+ private @Nullable String getContentTitle(
+ Context context,
+ @Nullable ContactsPreferences contactsPreferences,
+ ContactInfoCache.ContactCacheEntry contactInfo,
+ android.telecom.Call call) {
+
+ if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE)
+ && !call.getDetails()
+ .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) {
+
+ return context.getResources().getString(R.string.conference_call_name);
+ }
+
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, contactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return TextUtils.isEmpty(contactInfo.number)
+ ? null
+ : BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
+ }
+ return preferredName;
+ }
+
+ /**
+ * Gets a "person reference" for a notification, used by the system to determine whether the
+ * notification should be allowed past notification interruption filters.
+ *
+ * @param contactInfo The contact info from cache.
+ * @param call The call.
+ * @return the person reference.
+ */
+ private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, Call call) {
+
+ String number = TelecomCallUtil.getNumber(call);
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
+ // NotificationManager using it.
+ if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
+ return contactInfo.lookupUri.toString();
+ } else if (!TextUtils.isEmpty(number)) {
+ return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString();
+ }
+ return "";
+ }
+
+ private static class DialerCallDelegateStub implements DialerCallDelegate {
+
+ @Override
+ public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
+ return null;
+ }
+ }
+
+ /** Represents a call and associated cached notification data. */
+ private static class NotificationInfo {
+
+ @NonNull private final Call mCall;
+ private final int mNotificationId;
+ @Nullable private String mContentTitle;
+ @Nullable private Bitmap mLargeIcon;
+ @Nullable private String mPersonReference;
+
+ public NotificationInfo(@NonNull Call call, int notificationId) {
+ mCall = call;
+ mNotificationId = notificationId;
+ }
+
+ public Call getCall() {
+ return mCall;
+ }
+
+ public int getNotificationId() {
+ return mNotificationId;
+ }
+
+ public @Nullable String getContentTitle() {
+ return mContentTitle;
+ }
+
+ public void setContentTitle(@Nullable String contentTitle) {
+ mContentTitle = contentTitle;
+ }
+
+ public @Nullable Bitmap getLargeIcon() {
+ return mLargeIcon;
+ }
+
+ public void setLargeIcon(@Nullable Bitmap largeIcon) {
+ mLargeIcon = largeIcon;
+ }
+
+ public @Nullable String getPersonReference() {
+ return mPersonReference;
+ }
+
+ public void setPersonReference(@Nullable String personReference) {
+ mPersonReference = personReference;
+ }
+ }
+}
diff --git a/java/com/android/incallui/InCallActivity.java b/java/com/android/incallui/InCallActivity.java
new file mode 100644
index 000000000..307415916
--- /dev/null
+++ b/java/com/android/incallui/InCallActivity.java
@@ -0,0 +1,756 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.GradientDrawable.Orientation;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.FloatRange;
+import android.support.annotation.Nullable;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.graphics.ColorUtils;
+import android.telecom.DisconnectCause;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.incallui.answer.bindings.AnswerBindings;
+import com.android.incallui.answer.protocol.AnswerScreen;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
+import com.android.incallui.answerproximitysensor.PseudoScreenState;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.incall.bindings.InCallBindings;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.video.bindings.VideoBindings;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+
+/** Version of {@link InCallActivity} that shows the new UI */
+public class InCallActivity extends TransactionSafeFragmentActivity
+ implements AnswerScreenDelegateFactory,
+ InCallScreenDelegateFactory,
+ InCallButtonUiDelegateFactory,
+ VideoCallScreenDelegateFactory,
+ PseudoScreenState.StateChangedListener {
+
+ private static final String TAG_IN_CALL_SCREEN = "tag_in_call_screen";
+ private static final String TAG_ANSWER_SCREEN = "tag_answer_screen";
+ private static final String TAG_VIDEO_CALL_SCREEN = "tag_video_call_screen";
+
+ private static final String DID_SHOW_ANSWER_SCREEN_KEY = "did_show_answer_screen";
+ private static final String DID_SHOW_IN_CALL_SCREEN_KEY = "did_show_in_call_screen";
+ private static final String DID_SHOW_VIDEO_CALL_SCREEN_KEY = "did_show_video_call_screen";
+
+ private final InCallActivityCommon common;
+ private boolean didShowAnswerScreen;
+ private boolean didShowInCallScreen;
+ private boolean didShowVideoCallScreen;
+ private int[] backgroundDrawableColors;
+ private GradientDrawable backgroundDrawable;
+ private boolean isVisible;
+ private View pseudoBlackScreenOverlay;
+ private boolean touchDownWhenPseudoScreenOff;
+ private boolean isInShowMainInCallFragment;
+ private boolean needDismissPendingDialogs;
+
+ public InCallActivity() {
+ common = new InCallActivityCommon(this);
+ }
+
+ public static Intent getIntent(
+ Context context,
+ boolean showDialpad,
+ boolean newOutgoingCall,
+ boolean isVideoCall,
+ boolean isForFullScreen) {
+ Intent intent = new Intent(Intent.ACTION_MAIN, null);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClass(context, InCallActivity.class);
+ InCallActivityCommon.setIntentExtras(intent, showDialpad, newOutgoingCall, isForFullScreen);
+ return intent;
+ }
+
+ @Override
+ protected void onResumeFragments() {
+ super.onResumeFragments();
+ if (needDismissPendingDialogs) {
+ dismissPendingDialogs();
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ LogUtil.i("InCallActivity.onCreate", "");
+ super.onCreate(icicle);
+
+ if (icicle != null) {
+ didShowAnswerScreen = icicle.getBoolean(DID_SHOW_ANSWER_SCREEN_KEY);
+ didShowInCallScreen = icicle.getBoolean(DID_SHOW_IN_CALL_SCREEN_KEY);
+ didShowVideoCallScreen = icicle.getBoolean(DID_SHOW_VIDEO_CALL_SCREEN_KEY);
+ }
+
+ common.onCreate(icicle);
+
+ getWindow()
+ .getDecorView()
+ .setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
+
+ pseudoBlackScreenOverlay = findViewById(R.id.psuedo_black_screen_overlay);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle out) {
+ LogUtil.i("InCallActivity.onSaveInstanceState", "");
+ common.onSaveInstanceState(out);
+ out.putBoolean(DID_SHOW_ANSWER_SCREEN_KEY, didShowAnswerScreen);
+ out.putBoolean(DID_SHOW_IN_CALL_SCREEN_KEY, didShowInCallScreen);
+ out.putBoolean(DID_SHOW_VIDEO_CALL_SCREEN_KEY, didShowVideoCallScreen);
+ super.onSaveInstanceState(out);
+ isVisible = false;
+ }
+
+ @Override
+ protected void onStart() {
+ LogUtil.i("InCallActivity.onStart", "");
+ super.onStart();
+ isVisible = true;
+ showMainInCallFragment();
+ common.onStart();
+ if (ActivityCompat.isInMultiWindowMode(this)
+ && !getResources().getBoolean(R.bool.incall_dialpad_allowed)) {
+ // Hide the dialpad because there may not be enough room
+ showDialpadFragment(false, false);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ LogUtil.i("InCallActivity.onResume", "");
+ super.onResume();
+ common.onResume();
+ PseudoScreenState pseudoScreenState = InCallPresenter.getInstance().getPseudoScreenState();
+ pseudoScreenState.addListener(this);
+ onPseudoScreenStateChanged(pseudoScreenState.isOn());
+ }
+
+ /** onPause is guaranteed to be called when the InCallActivity goes in the background. */
+ @Override
+ protected void onPause() {
+ LogUtil.i("InCallActivity.onPause", "");
+ super.onPause();
+ common.onPause();
+ InCallPresenter.getInstance().getPseudoScreenState().removeListener(this);
+ }
+
+ @Override
+ protected void onStop() {
+ LogUtil.i("InCallActivity.onStop", "");
+ super.onStop();
+ common.onStop();
+ isVisible = false;
+ }
+
+ @Override
+ protected void onDestroy() {
+ LogUtil.i("InCallActivity.onDestroy", "");
+ super.onDestroy();
+ common.onDestroy();
+ }
+
+ @Override
+ public void finish() {
+ if (shouldCloseActivityOnFinish()) {
+ super.finish();
+ }
+ }
+
+ private boolean shouldCloseActivityOnFinish() {
+ if (!isVisible()) {
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish",
+ "allowing activity to be closed because it's not visible");
+ return true;
+ }
+
+ if (common.hasPendingDialogs()) {
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish", "dialog is visible, not closing activity");
+ return false;
+ }
+
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen != null && answerScreen.hasPendingDialogs()) {
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish",
+ "answer screen dialog is visible, not closing activity");
+ return false;
+ }
+
+ LogUtil.i(
+ "InCallActivity.shouldCloseActivityOnFinish",
+ "activity is visible and has no dialogs, allowing activity to close");
+ return true;
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ LogUtil.i("InCallActivity.onNewIntent", "");
+ common.onNewIntent(intent);
+
+ // If the screen is off, we need to make sure it gets turned on for incoming calls.
+ // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works
+ // when the activity is first created. Therefore, to ensure the screen is turned on
+ // for the call waiting case, we recreate() the current activity. There should be no jank from
+ // this since the screen is already off and will remain so until our new activity is up.
+ if (!isVisible()) {
+ LogUtil.i("InCallActivity.onNewIntent", "Restarting InCallActivity to force screen on.");
+ recreate();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ LogUtil.i("InCallActivity.onBackPressed", "");
+ if (!common.onBackPressed(didShowInCallScreen || didShowVideoCallScreen)) {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ LogUtil.i("InCallActivity.onOptionsItemSelected", "item: " + item);
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (common.onKeyUp(keyCode, event)) {
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (common.onKeyDown(keyCode, event)) {
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public boolean isInCallScreenAnimating() {
+ return false;
+ }
+
+ public void showConferenceFragment(boolean show) {
+ if (show) {
+ startActivity(new Intent(this, ManageConferenceActivity.class));
+ }
+ }
+
+ public boolean showDialpadFragment(boolean show, boolean animate) {
+ boolean didChange = common.showDialpadFragment(show, animate);
+ if (didChange) {
+ // Note: onInCallScreenDialpadVisibilityChange is called here to ensure that the dialpad FAB
+ // repositions itself.
+ getInCallScreen().onInCallScreenDialpadVisibilityChange(show);
+ }
+ return didChange;
+ }
+
+ public boolean isDialpadVisible() {
+ return common.isDialpadVisible();
+ }
+
+ public void onForegroundCallChanged(DialerCall newForegroundCall) {
+ common.updateTaskDescription();
+ if (didShowAnswerScreen && newForegroundCall != null) {
+ if (newForegroundCall.getState() == State.DISCONNECTED
+ || newForegroundCall.getState() == State.IDLE) {
+ LogUtil.i(
+ "InCallActivity.onForegroundCallChanged",
+ "rejecting incoming call, not updating " + "window background color");
+ }
+ } else {
+ LogUtil.v("InCallActivity.onForegroundCallChanged", "resetting background color");
+ updateWindowBackgroundColor(0);
+ }
+ }
+
+ public void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress) {
+ ThemeColorManager themeColorManager = InCallPresenter.getInstance().getThemeColorManager();
+ @ColorInt int top;
+ @ColorInt int middle;
+ @ColorInt int bottom;
+ @ColorInt int gray = 0x66000000;
+
+ if (ActivityCompat.isInMultiWindowMode(this)) {
+ top = themeColorManager.getBackgroundColorSolid();
+ middle = themeColorManager.getBackgroundColorSolid();
+ bottom = themeColorManager.getBackgroundColorSolid();
+ } else {
+ top = themeColorManager.getBackgroundColorTop();
+ middle = themeColorManager.getBackgroundColorMiddle();
+ bottom = themeColorManager.getBackgroundColorBottom();
+ }
+
+ if (progress < 0) {
+ float correctedProgress = Math.abs(progress);
+ top = ColorUtils.blendARGB(top, gray, correctedProgress);
+ middle = ColorUtils.blendARGB(middle, gray, correctedProgress);
+ bottom = ColorUtils.blendARGB(bottom, gray, correctedProgress);
+ }
+
+ boolean backgroundDirty = false;
+ if (backgroundDrawable == null) {
+ backgroundDrawableColors = new int[] {top, middle, bottom};
+ backgroundDrawable = new GradientDrawable(Orientation.TOP_BOTTOM, backgroundDrawableColors);
+ backgroundDirty = true;
+ } else {
+ if (backgroundDrawableColors[0] != top) {
+ backgroundDrawableColors[0] = top;
+ backgroundDirty = true;
+ }
+ if (backgroundDrawableColors[1] != middle) {
+ backgroundDrawableColors[1] = middle;
+ backgroundDirty = true;
+ }
+ if (backgroundDrawableColors[2] != bottom) {
+ backgroundDrawableColors[2] = bottom;
+ backgroundDirty = true;
+ }
+ if (backgroundDirty) {
+ backgroundDrawable.setColors(backgroundDrawableColors);
+ }
+ }
+
+ if (backgroundDirty) {
+ getWindow().setBackgroundDrawable(backgroundDrawable);
+ }
+ }
+
+ public boolean isVisible() {
+ return isVisible;
+ }
+
+ public boolean getCallCardFragmentVisible() {
+ return didShowInCallScreen || didShowVideoCallScreen;
+ }
+
+ public void dismissKeyguard(boolean dismiss) {
+ common.dismissKeyguard(dismiss);
+ }
+
+ public void showPostCharWaitDialog(String callId, String chars) {
+ common.showPostCharWaitDialog(callId, chars);
+ }
+
+ public void maybeShowErrorDialogOnDisconnect(DisconnectCause disconnectCause) {
+ common.maybeShowErrorDialogOnDisconnect(disconnectCause);
+ }
+
+ public void dismissPendingDialogs() {
+ if (isVisible) {
+ LogUtil.i("InCallActivity.dismissPendingDialogs", "");
+ common.dismissPendingDialogs();
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen != null) {
+ answerScreen.dismissPendingDialogs();
+ }
+ needDismissPendingDialogs = false;
+ } else {
+ // The activity is not visible and onSaveInstanceState may have been called so defer the
+ // dismissing action.
+ LogUtil.i(
+ "InCallActivity.dismissPendingDialogs", "defer actions since activity is not visible");
+ needDismissPendingDialogs = true;
+ }
+ }
+
+ private void enableInCallOrientationEventListener(boolean enable) {
+ common.enableInCallOrientationEventListener(enable);
+ }
+
+ public void setExcludeFromRecents(boolean exclude) {
+ common.setExcludeFromRecents(exclude);
+ }
+
+ public void onResolveIntent(
+ DialerCall outgoingCall, boolean isNewOutgoingCall, boolean didShowAccountSelectionDialog) {
+ if (didShowAccountSelectionDialog) {
+ hideMainInCallFragment();
+ }
+ }
+
+ @Nullable
+ public FragmentManager getDialpadFragmentManager() {
+ InCallScreen inCallScreen = getInCallScreen();
+ if (inCallScreen != null) {
+ return inCallScreen.getInCallScreenFragment().getChildFragmentManager();
+ }
+ return null;
+ }
+
+ public int getDialpadContainerId() {
+ return getInCallScreen().getAnswerAndDialpadContainerResourceId();
+ }
+
+ @Override
+ public AnswerScreenDelegate newAnswerScreenDelegate(AnswerScreen answerScreen) {
+ DialerCall call = CallList.getInstance().getCallById(answerScreen.getCallId());
+ if (call == null) {
+ // This is a work around for a bug where we attempt to create a new delegate after the call
+ // has already been removed. An example of when this can happen is:
+ // 1. incoming video call in landscape mode
+ // 2. remote party hangs up
+ // 3. activity switches from landscape to portrait
+ // At step #3 the answer fragment will try to create a new answer delegate but the call won't
+ // exist. In this case we'll simply return a stub delegate that does nothing. This is ok
+ // because this new state is transient and the activity will be destroyed soon.
+ LogUtil.i("InCallActivity.onPrimaryCallStateChanged", "call doesn't exist, using stub");
+ return new AnswerScreenPresenterStub();
+ } else {
+ return new AnswerScreenPresenter(
+ this, answerScreen, CallList.getInstance().getCallById(answerScreen.getCallId()));
+ }
+ }
+
+ @Override
+ public InCallScreenDelegate newInCallScreenDelegate() {
+ return new CallCardPresenter(this);
+ }
+
+ @Override
+ public InCallButtonUiDelegate newInCallButtonUiDelegate() {
+ return new CallButtonPresenter(this);
+ }
+
+ @Override
+ public VideoCallScreenDelegate newVideoCallScreenDelegate() {
+ return new VideoCallPresenter();
+ }
+
+ public void onPrimaryCallStateChanged() {
+ LogUtil.i("InCallActivity.onPrimaryCallStateChanged", "");
+ showMainInCallFragment();
+ }
+
+ public void onWiFiToLteHandover(DialerCall call) {
+ common.showWifiToLteHandoverToast(call);
+ }
+
+ public void onHandoverToWifiFailed(DialerCall call) {
+ common.showWifiFailedDialog(call);
+ }
+
+ public void setAllowOrientationChange(boolean allowOrientationChange) {
+ if (!allowOrientationChange) {
+ setRequestedOrientation(InCallOrientationEventListener.ACTIVITY_PREFERENCE_DISALLOW_ROTATION);
+ } else {
+ setRequestedOrientation(InCallOrientationEventListener.ACTIVITY_PREFERENCE_ALLOW_ROTATION);
+ }
+ enableInCallOrientationEventListener(allowOrientationChange);
+ }
+
+ private void hideMainInCallFragment() {
+ LogUtil.i("InCallActivity.hideMainInCallFragment", "");
+ if (didShowInCallScreen || didShowVideoCallScreen) {
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ hideInCallScreenFragment(transaction);
+ hideVideoCallScreenFragment(transaction);
+ transaction.commitAllowingStateLoss();
+ getSupportFragmentManager().executePendingTransactions();
+ }
+ }
+
+ private void showMainInCallFragment() {
+ // If the activity's onStart method hasn't been called yet then defer doing any work.
+ if (!isVisible) {
+ LogUtil.i("InCallActivity.showMainInCallFragment", "not visible yet/anymore");
+ return;
+ }
+
+ // Don't let this be reentrant.
+ if (isInShowMainInCallFragment) {
+ LogUtil.i("InCallActivity.showMainInCallFragment", "already in method, bailing");
+ return;
+ }
+
+ isInShowMainInCallFragment = true;
+ ShouldShowAnswerUiResult shouldShowAnswerUi = getShouldShowAnswerUi();
+ boolean shouldShowVideoUi = getShouldShowVideoUi();
+ LogUtil.i(
+ "InCallActivity.showMainInCallFragment",
+ "shouldShowAnswerUi: %b, shouldShowVideoUi: %b, "
+ + "didShowAnswerScreen: %b, didShowInCallScreen: %b, didShowVideoCallScreen: %b",
+ shouldShowAnswerUi.shouldShow,
+ shouldShowVideoUi,
+ didShowAnswerScreen,
+ didShowInCallScreen,
+ didShowVideoCallScreen);
+ // Only video call ui allows orientation change.
+ setAllowOrientationChange(shouldShowVideoUi);
+
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ boolean didChangeInCall;
+ boolean didChangeVideo;
+ boolean didChangeAnswer;
+ if (shouldShowAnswerUi.shouldShow) {
+ didChangeInCall = hideInCallScreenFragment(transaction);
+ didChangeVideo = hideVideoCallScreenFragment(transaction);
+ didChangeAnswer = showAnswerScreenFragment(transaction, shouldShowAnswerUi.call);
+ } else if (shouldShowVideoUi) {
+ didChangeInCall = hideInCallScreenFragment(transaction);
+ didChangeVideo = showVideoCallScreenFragment(transaction);
+ didChangeAnswer = hideAnswerScreenFragment(transaction);
+ } else {
+ didChangeInCall = showInCallScreenFragment(transaction);
+ didChangeVideo = hideVideoCallScreenFragment(transaction);
+ didChangeAnswer = hideAnswerScreenFragment(transaction);
+ }
+
+ if (didChangeInCall || didChangeVideo || didChangeAnswer) {
+ transaction.commitNow();
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
+ }
+ isInShowMainInCallFragment = false;
+ }
+
+ private ShouldShowAnswerUiResult getShouldShowAnswerUi() {
+ DialerCall call = CallList.getInstance().getIncomingCall();
+ if (call != null) {
+ LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found incoming call");
+ return new ShouldShowAnswerUiResult(true, call);
+ }
+
+ call = CallList.getInstance().getVideoUpgradeRequestCall();
+ if (call != null) {
+ LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found video upgrade request");
+ return new ShouldShowAnswerUiResult(true, call);
+ }
+
+ // Check if we're showing the answer screen and the call is disconnected. If this condition is
+ // true then we won't switch from the answer UI to the in call UI. This prevents flicker when
+ // the user rejects an incoming call.
+ call = CallList.getInstance().getFirstCall();
+ if (call == null) {
+ call = CallList.getInstance().getBackgroundCall();
+ }
+ if (didShowAnswerScreen && (call == null || call.getState() == State.DISCONNECTED)) {
+ LogUtil.i("InCallActivity.getShouldShowAnswerUi", "found disconnecting incoming call");
+ return new ShouldShowAnswerUiResult(true, call);
+ }
+
+ return new ShouldShowAnswerUiResult(false, null);
+ }
+
+ private boolean getShouldShowVideoUi() {
+ DialerCall call = CallList.getInstance().getFirstCall();
+ if (call == null) {
+ LogUtil.i("InCallActivity.getShouldShowVideoUi", "null call");
+ return false;
+ }
+
+ if (VideoUtils.isVideoCall(call)) {
+ LogUtil.i("InCallActivity.getShouldShowVideoUi", "found video call");
+ return true;
+ }
+
+ if (VideoUtils.hasSentVideoUpgradeRequest(call)) {
+ LogUtil.i("InCallActivity.getShouldShowVideoUi", "upgrading to video");
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean showAnswerScreenFragment(FragmentTransaction transaction, DialerCall call) {
+ // When rejecting a call the active call can become null in which case we should continue
+ // showing the answer screen.
+ if (didShowAnswerScreen && call == null) {
+ return false;
+ }
+
+ boolean isVideoUpgradeRequest = VideoUtils.hasReceivedVideoUpgradeRequest(call);
+ int videoState = isVideoUpgradeRequest ? call.getRequestedVideoState() : call.getVideoState();
+
+ // Check if we're already showing an answer screen for this call.
+ if (didShowAnswerScreen) {
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen.getCallId().equals(call.getId())
+ && answerScreen.getVideoState() == videoState
+ && answerScreen.isVideoUpgradeRequest() == isVideoUpgradeRequest) {
+ return false;
+ }
+ LogUtil.i(
+ "InCallActivity.showAnswerScreenFragment",
+ "answer fragment exists but arguments do not match");
+ hideAnswerScreenFragment(transaction);
+ }
+
+ // Show a new answer screen.
+ AnswerScreen answerScreen =
+ AnswerBindings.createAnswerScreen(call.getId(), videoState, isVideoUpgradeRequest);
+ transaction.add(R.id.main, answerScreen.getAnswerScreenFragment(), TAG_ANSWER_SCREEN);
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCOMING_CALL, this);
+ didShowAnswerScreen = true;
+ return true;
+ }
+
+ private boolean hideAnswerScreenFragment(FragmentTransaction transaction) {
+ if (!didShowAnswerScreen) {
+ return false;
+ }
+ AnswerScreen answerScreen = getAnswerScreen();
+ if (answerScreen != null) {
+ transaction.remove(answerScreen.getAnswerScreenFragment());
+ }
+
+ didShowAnswerScreen = false;
+ return true;
+ }
+
+ private boolean showInCallScreenFragment(FragmentTransaction transaction) {
+ if (didShowInCallScreen) {
+ return false;
+ }
+ InCallScreen inCallScreen = getInCallScreen();
+ if (inCallScreen == null) {
+ inCallScreen = InCallBindings.createInCallScreen();
+ transaction.add(R.id.main, inCallScreen.getInCallScreenFragment(), TAG_IN_CALL_SCREEN);
+ } else {
+ transaction.show(inCallScreen.getInCallScreenFragment());
+ }
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
+ didShowInCallScreen = true;
+ return true;
+ }
+
+ private boolean hideInCallScreenFragment(FragmentTransaction transaction) {
+ if (!didShowInCallScreen) {
+ return false;
+ }
+ InCallScreen inCallScreen = getInCallScreen();
+ if (inCallScreen != null) {
+ transaction.hide(inCallScreen.getInCallScreenFragment());
+ }
+ didShowInCallScreen = false;
+ return true;
+ }
+
+ private boolean showVideoCallScreenFragment(FragmentTransaction transaction) {
+ if (didShowVideoCallScreen) {
+ return false;
+ }
+
+ VideoCallScreen videoCallScreen = VideoBindings.createVideoCallScreen();
+ transaction.add(R.id.main, videoCallScreen.getVideoCallScreenFragment(), TAG_VIDEO_CALL_SCREEN);
+
+ Logger.get(this).logScreenView(ScreenEvent.Type.INCALL, this);
+ didShowVideoCallScreen = true;
+ return true;
+ }
+
+ private boolean hideVideoCallScreenFragment(FragmentTransaction transaction) {
+ if (!didShowVideoCallScreen) {
+ return false;
+ }
+ VideoCallScreen videoCallScreen = getVideoCallScreen();
+ if (videoCallScreen != null) {
+ transaction.remove(videoCallScreen.getVideoCallScreenFragment());
+ }
+ didShowVideoCallScreen = false;
+ return true;
+ }
+
+ AnswerScreen getAnswerScreen() {
+ return (AnswerScreen) getSupportFragmentManager().findFragmentByTag(TAG_ANSWER_SCREEN);
+ }
+
+ InCallScreen getInCallScreen() {
+ return (InCallScreen) getSupportFragmentManager().findFragmentByTag(TAG_IN_CALL_SCREEN);
+ }
+
+ VideoCallScreen getVideoCallScreen() {
+ return (VideoCallScreen) getSupportFragmentManager().findFragmentByTag(TAG_VIDEO_CALL_SCREEN);
+ }
+
+ @Override
+ public void onPseudoScreenStateChanged(boolean isOn) {
+ LogUtil.i("InCallActivity.onPseudoScreenStateChanged", "isOn: " + isOn);
+ pseudoBlackScreenOverlay.setVisibility(isOn ? View.GONE : View.VISIBLE);
+ }
+
+ /**
+ * For some touch related issue, turning off the screen can be faked by drawing a black view over
+ * the activity. All touch events started when the screen is "off" is rejected.
+ *
+ * @see PseudoScreenState
+ */
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent event) {
+ // Reject any gesture that started when the screen is in the fake off state.
+ if (touchDownWhenPseudoScreenOff) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ touchDownWhenPseudoScreenOff = false;
+ }
+ return true;
+ }
+ // Reject all touch event when the screen is in the fake off state.
+ if (!InCallPresenter.getInstance().getPseudoScreenState().isOn()) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ touchDownWhenPseudoScreenOff = true;
+ LogUtil.i("InCallActivity.dispatchTouchEvent", "touchDownWhenPseudoScreenOff");
+ }
+ return true;
+ }
+ return super.dispatchTouchEvent(event);
+ }
+
+ private static class ShouldShowAnswerUiResult {
+ public final boolean shouldShow;
+ public final DialerCall call;
+
+ ShouldShowAnswerUiResult(boolean shouldShow, DialerCall call) {
+ this.shouldShow = shouldShow;
+ this.call = call;
+ }
+ }
+}
diff --git a/java/com/android/incallui/InCallActivityCommon.java b/java/com/android/incallui/InCallActivityCommon.java
new file mode 100644
index 000000000..a2467dd72
--- /dev/null
+++ b/java/com/android/incallui/InCallActivityCommon.java
@@ -0,0 +1,820 @@
+/*
+ * 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.incallui;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.AppTask;
+import android.app.ActivityManager.TaskDescription;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.res.ResourcesCompat;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.CheckBox;
+import android.widget.Toast;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment.SelectPhoneAccountListener;
+import com.android.dialer.animation.AnimUtils;
+import com.android.dialer.animation.AnimationListenerAdapter;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ScreenEvent;
+import com.android.dialer.util.ViewUtil;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.wifi.EnableWifiCallingPrompt;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Shared functionality between the new and old in call activity. */
+public class InCallActivityCommon {
+
+ private static final String INTENT_EXTRA_SHOW_DIALPAD = "InCallActivity.show_dialpad";
+ private static final String INTENT_EXTRA_NEW_OUTGOING_CALL = "InCallActivity.new_outgoing_call";
+ private static final String INTENT_EXTRA_FOR_FULL_SCREEN =
+ "InCallActivity.for_full_screen_intent";
+
+ private static final String DIALPAD_TEXT_KEY = "InCallActivity.dialpad_text";
+
+ private static final String TAG_SELECT_ACCOUNT_FRAGMENT = "tag_select_account_fragment";
+ private static final String TAG_DIALPAD_FRAGMENT = "tag_dialpad_fragment";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ DIALPAD_REQUEST_NONE,
+ DIALPAD_REQUEST_SHOW,
+ DIALPAD_REQUEST_HIDE,
+ })
+ @interface DialpadRequestType {}
+
+ private static final int DIALPAD_REQUEST_NONE = 1;
+ private static final int DIALPAD_REQUEST_SHOW = 2;
+ private static final int DIALPAD_REQUEST_HIDE = 3;
+
+ private final InCallActivity inCallActivity;
+ private boolean dismissKeyguard;
+ private boolean showPostCharWaitDialogOnResume;
+ private String showPostCharWaitDialogCallId;
+ private String showPostCharWaitDialogChars;
+ private Dialog dialog;
+ private InCallOrientationEventListener inCallOrientationEventListener;
+ private Animation dialpadSlideInAnimation;
+ private Animation dialpadSlideOutAnimation;
+ private boolean animateDialpadOnShow;
+ private String dtmfTextToPreopulate;
+ @DialpadRequestType private int showDialpadRequest = DIALPAD_REQUEST_NONE;
+
+ private SelectPhoneAccountListener selectAccountListener =
+ new SelectPhoneAccountListener() {
+ @Override
+ public void onPhoneAccountSelected(
+ PhoneAccountHandle selectedAccountHandle, boolean setDefault, String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ LogUtil.i(
+ "InCallActivityCommon.SelectPhoneAccountListener.onPhoneAccountSelected",
+ "call: " + call);
+ if (call != null) {
+ call.phoneAccountSelected(selectedAccountHandle, setDefault);
+ }
+ }
+
+ @Override
+ public void onDialogDismissed(String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ LogUtil.i(
+ "InCallActivityCommon.SelectPhoneAccountListener.onDialogDismissed",
+ "disconnecting call: " + call);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ };
+
+ public static void setIntentExtras(
+ Intent intent, boolean showDialpad, boolean newOutgoingCall, boolean isForFullScreen) {
+ if (showDialpad) {
+ intent.putExtra(INTENT_EXTRA_SHOW_DIALPAD, true);
+ }
+ intent.putExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, newOutgoingCall);
+ intent.putExtra(INTENT_EXTRA_FOR_FULL_SCREEN, isForFullScreen);
+ }
+
+ public InCallActivityCommon(InCallActivity inCallActivity) {
+ this.inCallActivity = inCallActivity;
+ }
+
+ public void onCreate(Bundle icicle) {
+ // set this flag so this activity will stay in front of the keyguard
+ // Have the WindowManager filter out touch events that are "too fat".
+ int flags =
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
+
+ inCallActivity.getWindow().addFlags(flags);
+
+ inCallActivity.setContentView(R.layout.incall_screen);
+
+ internalResolveIntent(inCallActivity.getIntent());
+
+ boolean isLandscape =
+ inCallActivity.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ boolean isRtl = ViewUtil.isRtl();
+
+ if (isLandscape) {
+ dialpadSlideInAnimation =
+ AnimationUtils.loadAnimation(
+ inCallActivity, isRtl ? R.anim.dialpad_slide_in_left : R.anim.dialpad_slide_in_right);
+ dialpadSlideOutAnimation =
+ AnimationUtils.loadAnimation(
+ inCallActivity,
+ isRtl ? R.anim.dialpad_slide_out_left : R.anim.dialpad_slide_out_right);
+ } else {
+ dialpadSlideInAnimation =
+ AnimationUtils.loadAnimation(inCallActivity, R.anim.dialpad_slide_in_bottom);
+ dialpadSlideOutAnimation =
+ AnimationUtils.loadAnimation(inCallActivity, R.anim.dialpad_slide_out_bottom);
+ }
+
+ dialpadSlideInAnimation.setInterpolator(AnimUtils.EASE_IN);
+ dialpadSlideOutAnimation.setInterpolator(AnimUtils.EASE_OUT);
+
+ dialpadSlideOutAnimation.setAnimationListener(
+ new AnimationListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ performHideDialpadFragment();
+ }
+ });
+
+ if (icicle != null) {
+ // If the dialpad was shown before, set variables indicating it should be shown and
+ // populated with the previous DTMF text. The dialpad is actually shown and populated
+ // in onResume() to ensure the hosting fragment has been inflated and is ready to receive it.
+ if (icicle.containsKey(INTENT_EXTRA_SHOW_DIALPAD)) {
+ boolean showDialpad = icicle.getBoolean(INTENT_EXTRA_SHOW_DIALPAD);
+ showDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_HIDE;
+ animateDialpadOnShow = false;
+ }
+ dtmfTextToPreopulate = icicle.getString(DIALPAD_TEXT_KEY);
+
+ SelectPhoneAccountDialogFragment dialogFragment =
+ (SelectPhoneAccountDialogFragment)
+ inCallActivity.getFragmentManager().findFragmentByTag(TAG_SELECT_ACCOUNT_FRAGMENT);
+ if (dialogFragment != null) {
+ dialogFragment.setListener(selectAccountListener);
+ }
+ }
+
+ inCallOrientationEventListener = new InCallOrientationEventListener(inCallActivity);
+ }
+
+ public void onSaveInstanceState(Bundle out) {
+ // TODO: The dialpad fragment should handle this as part of its own state
+ out.putBoolean(INTENT_EXTRA_SHOW_DIALPAD, isDialpadVisible());
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null) {
+ out.putString(DIALPAD_TEXT_KEY, dialpadFragment.getDtmfText());
+ }
+ }
+
+ public void onStart() {
+ // setting activity should be last thing in setup process
+ InCallPresenter.getInstance().setActivity(inCallActivity);
+ enableInCallOrientationEventListener(
+ inCallActivity.getRequestedOrientation()
+ == InCallOrientationEventListener.ACTIVITY_PREFERENCE_ALLOW_ROTATION);
+
+ InCallPresenter.getInstance().onActivityStarted();
+ }
+
+ public void onResume() {
+ if (InCallPresenter.getInstance().isReadyForTearDown()) {
+ LogUtil.i(
+ "InCallActivityCommon.onResume",
+ "InCallPresenter is ready for tear down, not sending updates");
+ } else {
+ updateTaskDescription();
+ InCallPresenter.getInstance().onUiShowing(true);
+ }
+
+ // If there is a pending request to show or hide the dialpad, handle that now.
+ if (showDialpadRequest != DIALPAD_REQUEST_NONE) {
+ if (showDialpadRequest == DIALPAD_REQUEST_SHOW) {
+ // Exit fullscreen so that the user has access to the dialpad hide/show button and
+ // can hide the dialpad. Important when showing the dialpad from within dialer.
+ InCallPresenter.getInstance().setFullScreen(false, true /* force */);
+
+ inCallActivity.showDialpadFragment(true /* show */, animateDialpadOnShow /* animate */);
+ animateDialpadOnShow = false;
+
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null) {
+ dialpadFragment.setDtmfText(dtmfTextToPreopulate);
+ dtmfTextToPreopulate = null;
+ }
+ } else {
+ LogUtil.i("InCallActivityCommon.onResume", "force hide dialpad");
+ if (getDialpadFragment() != null) {
+ inCallActivity.showDialpadFragment(false /* show */, false /* animate */);
+ }
+ }
+ showDialpadRequest = DIALPAD_REQUEST_NONE;
+ }
+
+ if (showPostCharWaitDialogOnResume) {
+ showPostCharWaitDialog(showPostCharWaitDialogCallId, showPostCharWaitDialogChars);
+ }
+
+ CallList.getInstance()
+ .onInCallUiShown(
+ inCallActivity.getIntent().getBooleanExtra(INTENT_EXTRA_FOR_FULL_SCREEN, false));
+ }
+
+ // onPause is guaranteed to be called when the InCallActivity goes
+ // in the background.
+ public void onPause() {
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null) {
+ dialpadFragment.onDialerKeyUp(null);
+ }
+
+ InCallPresenter.getInstance().onUiShowing(false);
+ if (inCallActivity.isFinishing()) {
+ InCallPresenter.getInstance().unsetActivity(inCallActivity);
+ }
+ }
+
+ public void onStop() {
+ enableInCallOrientationEventListener(false);
+ InCallPresenter.getInstance().updateIsChangingConfigurations();
+ InCallPresenter.getInstance().onActivityStopped();
+ }
+
+ public void onDestroy() {
+ InCallPresenter.getInstance().unsetActivity(inCallActivity);
+ InCallPresenter.getInstance().updateIsChangingConfigurations();
+ }
+
+ public void onNewIntent(Intent intent) {
+ LogUtil.i("InCallActivityCommon.onNewIntent", "");
+
+ // We're being re-launched with a new Intent. Since it's possible for a
+ // single InCallActivity instance to persist indefinitely (even if we
+ // finish() ourselves), this sequence can potentially happen any time
+ // the InCallActivity needs to be displayed.
+
+ // Stash away the new intent so that we can get it in the future
+ // by calling getIntent(). (Otherwise getIntent() will return the
+ // original Intent from when we first got created!)
+ inCallActivity.setIntent(intent);
+
+ // Activities are always paused before receiving a new intent, so
+ // we can count on our onResume() method being called next.
+
+ // Just like in onCreate(), handle the intent.
+ internalResolveIntent(intent);
+ }
+
+ public boolean onBackPressed(boolean isInCallScreenVisible) {
+ LogUtil.i("InCallActivityCommon.onBackPressed", "");
+
+ // BACK is also used to exit out of any "special modes" of the
+ // in-call UI:
+ if (!inCallActivity.isVisible()) {
+ return true;
+ }
+
+ if (!isInCallScreenVisible) {
+ return true;
+ }
+
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null && dialpadFragment.isVisible()) {
+ inCallActivity.showDialpadFragment(false /* show */, true /* animate */);
+ return true;
+ }
+
+ // Always disable the Back key while an incoming call is ringing
+ DialerCall call = CallList.getInstance().getIncomingCall();
+ if (call != null) {
+ LogUtil.i("InCallActivityCommon.onBackPressed", "consume Back press for an incoming call");
+ return true;
+ }
+
+ // Nothing special to do. Fall back to the default behavior.
+ return false;
+ }
+
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ // push input to the dialer.
+ if (dialpadFragment != null
+ && (dialpadFragment.isVisible())
+ && (dialpadFragment.onDialerKeyUp(event))) {
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_CALL) {
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL:
+ boolean handled = InCallPresenter.getInstance().handleCallKey();
+ if (!handled) {
+ LogUtil.e(
+ "InCallActivityCommon.onKeyDown",
+ "InCallPresenter should always handle KEYCODE_CALL in onKeyDown");
+ }
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+
+ // Note there's no KeyEvent.KEYCODE_ENDCALL case here.
+ // The standard system-wide handling of the ENDCALL key
+ // (see PhoneWindowManager's handling of KEYCODE_ENDCALL)
+ // already implements exactly what the UI spec wants,
+ // namely (1) "hang up" if there's a current active call,
+ // or (2) "don't answer" if there's a current ringing call.
+
+ case KeyEvent.KEYCODE_CAMERA:
+ // Disable the CAMERA button while in-call since it's too
+ // easy to press accidentally.
+ return true;
+
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ // Ringer silencing handled by PhoneWindowManager.
+ break;
+
+ case KeyEvent.KEYCODE_MUTE:
+ TelecomAdapter.getInstance()
+ .mute(!AudioModeProvider.getInstance().getAudioState().isMuted());
+ return true;
+
+ // Various testing/debugging features, enabled ONLY when VERBOSE == true.
+ case KeyEvent.KEYCODE_SLASH:
+ if (LogUtil.isVerboseEnabled()) {
+ LogUtil.v(
+ "InCallActivityCommon.onKeyDown",
+ "----------- InCallActivity View dump --------------");
+ // Dump starting from the top-level view of the entire activity:
+ Window w = inCallActivity.getWindow();
+ View decorView = w.getDecorView();
+ LogUtil.v("InCallActivityCommon.onKeyDown", "View dump:" + decorView);
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_EQUALS:
+ break;
+ }
+
+ return event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event);
+ }
+
+ private boolean handleDialerKeyDown(int keyCode, KeyEvent event) {
+ LogUtil.v("InCallActivityCommon.handleDialerKeyDown", "keyCode %d, event: %s", keyCode, event);
+
+ // As soon as the user starts typing valid dialable keys on the
+ // keyboard (presumably to type DTMF tones) we start passing the
+ // key events to the DTMFDialer's onDialerKeyDown.
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment != null && dialpadFragment.isVisible()) {
+ return dialpadFragment.onDialerKeyDown(event);
+ }
+
+ return false;
+ }
+
+ public void dismissKeyguard(boolean dismiss) {
+ if (dismissKeyguard == dismiss) {
+ return;
+ }
+ dismissKeyguard = dismiss;
+ if (dismiss) {
+ inCallActivity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ } else {
+ inCallActivity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+ }
+
+ public void showPostCharWaitDialog(String callId, String chars) {
+ if (inCallActivity.isVisible()) {
+ PostCharDialogFragment fragment = new PostCharDialogFragment(callId, chars);
+ fragment.show(inCallActivity.getSupportFragmentManager(), "postCharWait");
+
+ showPostCharWaitDialogOnResume = false;
+ showPostCharWaitDialogCallId = null;
+ showPostCharWaitDialogChars = null;
+ } else {
+ showPostCharWaitDialogOnResume = true;
+ showPostCharWaitDialogCallId = callId;
+ showPostCharWaitDialogChars = chars;
+ }
+ }
+
+ public void maybeShowErrorDialogOnDisconnect(DisconnectCause cause) {
+ LogUtil.i(
+ "InCallActivityCommon.maybeShowErrorDialogOnDisconnect", "disconnect cause: %s", cause);
+
+ if (!inCallActivity.isFinishing()) {
+ if (EnableWifiCallingPrompt.shouldShowPrompt(cause)) {
+ Pair<Dialog, CharSequence> pair =
+ EnableWifiCallingPrompt.createDialog(inCallActivity, cause);
+ showErrorDialog(pair.first, pair.second);
+ } else if (shouldShowDisconnectErrorDialog(cause)) {
+ Pair<Dialog, CharSequence> pair = getDisconnectErrorDialog(inCallActivity, cause);
+ showErrorDialog(pair.first, pair.second);
+ }
+ }
+ }
+
+ /**
+ * When relaunching from the dialer app, {@code showDialpad} indicates whether the dialpad should
+ * be shown on launch.
+ *
+ * @param showDialpad {@code true} to indicate the dialpad should be shown on launch, and {@code
+ * false} to indicate no change should be made to the dialpad visibility.
+ */
+ private void relaunchedFromDialer(boolean showDialpad) {
+ showDialpadRequest = showDialpad ? DIALPAD_REQUEST_SHOW : DIALPAD_REQUEST_NONE;
+ animateDialpadOnShow = true;
+
+ if (showDialpadRequest == DIALPAD_REQUEST_SHOW) {
+ // If there's only one line in use, AND it's on hold, then we're sure the user
+ // wants to use the dialpad toward the exact line, so un-hold the holding line.
+ DialerCall call = CallList.getInstance().getActiveOrBackgroundCall();
+ if (call != null && call.getState() == State.ONHOLD) {
+ call.unhold();
+ }
+ }
+ }
+
+ public void dismissPendingDialogs() {
+ if (dialog != null) {
+ dialog.dismiss();
+ dialog = null;
+ }
+ }
+
+ private static boolean shouldShowDisconnectErrorDialog(@NonNull DisconnectCause cause) {
+ return !TextUtils.isEmpty(cause.getDescription())
+ && (cause.getCode() == DisconnectCause.ERROR
+ || cause.getCode() == DisconnectCause.RESTRICTED);
+ }
+
+ private static Pair<Dialog, CharSequence> getDisconnectErrorDialog(
+ @NonNull Context context, @NonNull DisconnectCause cause) {
+ CharSequence message = cause.getDescription();
+ Dialog dialog =
+ new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null)
+ .create();
+ return new Pair<>(dialog, message);
+ }
+
+ private void showErrorDialog(Dialog dialog, CharSequence message) {
+ LogUtil.i("InCallActivityCommon.showErrorDialog", "message: %s", message);
+ inCallActivity.dismissPendingDialogs();
+
+ // Show toast if apps is in background when dialog won't be visible.
+ if (!inCallActivity.isVisible()) {
+ Toast.makeText(inCallActivity.getApplicationContext(), message, Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ this.dialog = dialog;
+ dialog.setOnDismissListener(
+ new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ LogUtil.i("InCallActivityCommon.showErrorDialog", "dialog dismissed");
+ onDialogDismissed();
+ }
+ });
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ dialog.show();
+ }
+
+ private void onDialogDismissed() {
+ dialog = null;
+ CallList.getInstance().onErrorDialogDismissed();
+ InCallPresenter.getInstance().onDismissDialog();
+ }
+
+ public void enableInCallOrientationEventListener(boolean enable) {
+ if (enable) {
+ inCallOrientationEventListener.enable(true);
+ } else {
+ inCallOrientationEventListener.disable();
+ }
+ }
+
+ public void setExcludeFromRecents(boolean exclude) {
+ List<AppTask> tasks = inCallActivity.getSystemService(ActivityManager.class).getAppTasks();
+ int taskId = inCallActivity.getTaskId();
+ for (int i = 0; i < tasks.size(); i++) {
+ ActivityManager.AppTask task = tasks.get(i);
+ try {
+ if (task.getTaskInfo().id == taskId) {
+ task.setExcludeFromRecents(exclude);
+ }
+ } catch (RuntimeException e) {
+ LogUtil.e(
+ "InCallActivityCommon.setExcludeFromRecents",
+ "RuntimeException when excluding task from recents.",
+ e);
+ }
+ }
+ }
+
+ public void showWifiToLteHandoverToast(DialerCall call) {
+ if (call.hasShownWiFiToLteHandoverToast()) {
+ return;
+ }
+ Toast.makeText(
+ inCallActivity, R.string.video_call_wifi_to_lte_handover_toast, Toast.LENGTH_LONG)
+ .show();
+ call.setHasShownWiFiToLteHandoverToast();
+ }
+
+ public void showWifiFailedDialog(final DialerCall call) {
+ if (call.showWifiHandoverAlertAsToast()) {
+ LogUtil.i("InCallActivityCommon.showWifiFailedDialog", "as toast");
+ Toast.makeText(
+ inCallActivity, R.string.video_call_lte_to_wifi_failed_message, Toast.LENGTH_SHORT)
+ .show();
+ return;
+ }
+
+ dismissPendingDialogs();
+
+ AlertDialog.Builder builder =
+ new AlertDialog.Builder(inCallActivity)
+ .setTitle(R.string.video_call_lte_to_wifi_failed_title);
+
+ // This allows us to use the theme of the dialog instead of the activity
+ View dialogCheckBoxView =
+ View.inflate(builder.getContext(), R.layout.video_call_lte_to_wifi_failed, null);
+ final CheckBox wifiHandoverFailureCheckbox =
+ (CheckBox) dialogCheckBoxView.findViewById(R.id.video_call_lte_to_wifi_failed_checkbox);
+ wifiHandoverFailureCheckbox.setChecked(false);
+
+ dialog =
+ builder
+ .setView(dialogCheckBoxView)
+ .setMessage(R.string.video_call_lte_to_wifi_failed_message)
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ onDialogDismissed();
+ }
+ })
+ .setPositiveButton(
+ android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ call.setDoNotShowDialogForHandoffToWifiFailure(
+ wifiHandoverFailureCheckbox.isChecked());
+ dialog.cancel();
+ onDialogDismissed();
+ }
+ })
+ .create();
+
+ LogUtil.i("InCallActivityCommon.showWifiFailedDialog", "as dialog");
+ dialog.show();
+ }
+
+ public boolean showDialpadFragment(boolean show, boolean animate) {
+ // If the dialpad is already visible, don't animate in. If it's gone, don't animate out.
+ boolean isDialpadVisible = isDialpadVisible();
+ LogUtil.i(
+ "InCallActivityCommon.showDialpadFragment",
+ "show: %b, animate: %b, " + "isDialpadVisible: %b",
+ show,
+ animate,
+ isDialpadVisible);
+ if (show == isDialpadVisible) {
+ return false;
+ }
+
+ FragmentManager dialpadFragmentManager = inCallActivity.getDialpadFragmentManager();
+ if (dialpadFragmentManager == null) {
+ LogUtil.i(
+ "InCallActivityCommon.showDialpadFragment", "unable to show or hide dialpad fragment");
+ return false;
+ }
+
+ // We don't do a FragmentTransaction on the hide case because it will be dealt with when
+ // the listener is fired after an animation finishes.
+ if (!animate) {
+ if (show) {
+ performShowDialpadFragment(dialpadFragmentManager);
+ } else {
+ performHideDialpadFragment();
+ }
+ } else {
+ if (show) {
+ performShowDialpadFragment(dialpadFragmentManager);
+ getDialpadFragment().animateShowDialpad();
+ }
+ getDialpadFragment()
+ .getView()
+ .startAnimation(show ? dialpadSlideInAnimation : dialpadSlideOutAnimation);
+ }
+
+ ProximitySensor sensor = InCallPresenter.getInstance().getProximitySensor();
+ if (sensor != null) {
+ sensor.onDialpadVisible(show);
+ }
+ showDialpadRequest = DIALPAD_REQUEST_NONE;
+ return true;
+ }
+
+ private void performShowDialpadFragment(@NonNull FragmentManager dialpadFragmentManager) {
+ FragmentTransaction transaction = dialpadFragmentManager.beginTransaction();
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ if (dialpadFragment == null) {
+ transaction.add(
+ inCallActivity.getDialpadContainerId(), new DialpadFragment(), TAG_DIALPAD_FRAGMENT);
+ } else {
+ transaction.show(dialpadFragment);
+ }
+
+ transaction.commitAllowingStateLoss();
+ dialpadFragmentManager.executePendingTransactions();
+
+ Logger.get(inCallActivity).logScreenView(ScreenEvent.Type.INCALL_DIALPAD, inCallActivity);
+ }
+
+ private void performHideDialpadFragment() {
+ FragmentManager fragmentManager = inCallActivity.getDialpadFragmentManager();
+ if (fragmentManager == null) {
+ LogUtil.e(
+ "InCallActivityCommon.performHideDialpadFragment", "child fragment manager is null");
+ return;
+ }
+
+ Fragment fragment = fragmentManager.findFragmentByTag(TAG_DIALPAD_FRAGMENT);
+ if (fragment != null) {
+ FragmentTransaction transaction = fragmentManager.beginTransaction();
+ transaction.hide(fragment);
+ transaction.commitAllowingStateLoss();
+ fragmentManager.executePendingTransactions();
+ }
+ }
+
+ public boolean isDialpadVisible() {
+ DialpadFragment dialpadFragment = getDialpadFragment();
+ return dialpadFragment != null && dialpadFragment.isVisible();
+ }
+
+ /** Returns the {@link DialpadFragment} that's shown by this activity, or {@code null} */
+ @Nullable
+ private DialpadFragment getDialpadFragment() {
+ FragmentManager fragmentManager = inCallActivity.getDialpadFragmentManager();
+ if (fragmentManager == null) {
+ return null;
+ }
+ return (DialpadFragment) fragmentManager.findFragmentByTag(TAG_DIALPAD_FRAGMENT);
+ }
+
+ public void updateTaskDescription() {
+ Resources resources = inCallActivity.getResources();
+ int color;
+ if (resources.getBoolean(R.bool.is_layout_landscape)) {
+ color =
+ ResourcesCompat.getColor(
+ resources, R.color.statusbar_background_color, inCallActivity.getTheme());
+ } else {
+ color = InCallPresenter.getInstance().getThemeColorManager().getSecondaryColor();
+ }
+
+ TaskDescription td =
+ new TaskDescription(resources.getString(R.string.notification_ongoing_call), null, color);
+ inCallActivity.setTaskDescription(td);
+ }
+
+ public boolean hasPendingDialogs() {
+ return dialog != null;
+ }
+
+ private void internalResolveIntent(Intent intent) {
+ if (!intent.getAction().equals(Intent.ACTION_MAIN)) {
+ return;
+ }
+
+ if (intent.hasExtra(INTENT_EXTRA_SHOW_DIALPAD)) {
+ // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF
+ // dialpad should be initially visible. If the extra isn't
+ // present at all, we just leave the dialpad in its previous state.
+ boolean showDialpad = intent.getBooleanExtra(INTENT_EXTRA_SHOW_DIALPAD, false);
+ LogUtil.i("InCallActivityCommon.internalResolveIntent", "SHOW_DIALPAD_EXTRA: " + showDialpad);
+
+ relaunchedFromDialer(showDialpad);
+ }
+
+ DialerCall outgoingCall = CallList.getInstance().getOutgoingCall();
+ if (outgoingCall == null) {
+ outgoingCall = CallList.getInstance().getPendingOutgoingCall();
+ }
+
+ boolean isNewOutgoingCall = false;
+ if (intent.getBooleanExtra(INTENT_EXTRA_NEW_OUTGOING_CALL, false)) {
+ isNewOutgoingCall = true;
+ intent.removeExtra(INTENT_EXTRA_NEW_OUTGOING_CALL);
+
+ // InCallActivity is responsible for disconnecting a new outgoing call if there
+ // is no way of making it (i.e. no valid call capable accounts).
+ // If the version is not MSIM compatible, then ignore this code.
+ if (CompatUtils.isMSIMCompatible()
+ && InCallPresenter.isCallWithNoValidAccounts(outgoingCall)) {
+ LogUtil.i(
+ "InCallActivityCommon.internalResolveIntent",
+ "call with no valid accounts, disconnecting");
+ outgoingCall.disconnect();
+ }
+
+ dismissKeyguard(true);
+ }
+
+ boolean didShowAccountSelectionDialog = maybeShowAccountSelectionDialog();
+ inCallActivity.onResolveIntent(outgoingCall, isNewOutgoingCall, didShowAccountSelectionDialog);
+ }
+
+ private boolean maybeShowAccountSelectionDialog() {
+ DialerCall call = CallList.getInstance().getWaitingForAccountCall();
+ if (call == null) {
+ return false;
+ }
+
+ Bundle extras = call.getIntentExtras();
+ List<PhoneAccountHandle> phoneAccountHandles;
+ if (extras != null) {
+ phoneAccountHandles =
+ extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
+ } else {
+ phoneAccountHandles = new ArrayList<>();
+ }
+
+ DialogFragment dialogFragment =
+ SelectPhoneAccountDialogFragment.newInstance(
+ R.string.select_phone_account_for_calls,
+ true,
+ phoneAccountHandles,
+ selectAccountListener,
+ call.getId());
+ dialogFragment.show(inCallActivity.getFragmentManager(), TAG_SELECT_ACCOUNT_FRAGMENT);
+ return true;
+ }
+}
diff --git a/java/com/android/incallui/InCallCameraManager.java b/java/com/android/incallui/InCallCameraManager.java
new file mode 100644
index 000000000..fdb422643
--- /dev/null
+++ b/java/com/android/incallui/InCallCameraManager.java
@@ -0,0 +1,173 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/** Used to track which camera is used for outgoing video. */
+public class InCallCameraManager {
+
+ private final Set<Listener> mCameraSelectionListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+ /** The camera ID for the front facing camera. */
+ private String mFrontFacingCameraId;
+ /** The camera ID for the rear facing camera. */
+ private String mRearFacingCameraId;
+ /** The currently active camera. */
+ private boolean mUseFrontFacingCamera;
+ /**
+ * Indicates whether the list of cameras has been initialized yet. Initialization is delayed until
+ * a video call is present.
+ */
+ private boolean mIsInitialized = false;
+ /** The context. */
+ private Context mContext;
+
+ /**
+ * Initializes the InCall CameraManager.
+ *
+ * @param context The current context.
+ */
+ public InCallCameraManager(Context context) {
+ mUseFrontFacingCamera = true;
+ mContext = context;
+ }
+
+ /**
+ * Sets whether the front facing camera should be used or not.
+ *
+ * @param useFrontFacingCamera {@code True} if the front facing camera is to be used.
+ */
+ public void setUseFrontFacingCamera(boolean useFrontFacingCamera) {
+ mUseFrontFacingCamera = useFrontFacingCamera;
+ for (Listener listener : mCameraSelectionListeners) {
+ listener.onActiveCameraSelectionChanged(mUseFrontFacingCamera);
+ }
+ }
+
+ /**
+ * Determines whether the front facing camera is currently in use.
+ *
+ * @return {@code True} if the front facing camera is in use.
+ */
+ public boolean isUsingFrontFacingCamera() {
+ return mUseFrontFacingCamera;
+ }
+
+ /**
+ * Determines the active camera ID.
+ *
+ * @return The active camera ID.
+ */
+ public String getActiveCameraId() {
+ maybeInitializeCameraList(mContext);
+
+ if (mUseFrontFacingCamera) {
+ return mFrontFacingCameraId;
+ } else {
+ return mRearFacingCameraId;
+ }
+ }
+
+ /** Calls when camera permission is granted by user. */
+ public void onCameraPermissionGranted() {
+ for (Listener listener : mCameraSelectionListeners) {
+ listener.onCameraPermissionGranted();
+ }
+ }
+
+ /**
+ * Get the list of cameras available for use.
+ *
+ * @param context The context.
+ */
+ private void maybeInitializeCameraList(Context context) {
+ if (mIsInitialized || context == null) {
+ return;
+ }
+
+ Log.v(this, "initializeCameraList");
+
+ CameraManager cameraManager = null;
+ try {
+ cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
+ } catch (Exception e) {
+ Log.e(this, "Could not get camera service.");
+ return;
+ }
+
+ if (cameraManager == null) {
+ return;
+ }
+
+ String[] cameraIds = {};
+ try {
+ cameraIds = cameraManager.getCameraIdList();
+ } catch (CameraAccessException e) {
+ Log.d(this, "Could not access camera: " + e);
+ // Camera disabled by device policy.
+ return;
+ }
+
+ for (int i = 0; i < cameraIds.length; i++) {
+ CameraCharacteristics c = null;
+ try {
+ c = cameraManager.getCameraCharacteristics(cameraIds[i]);
+ } catch (IllegalArgumentException e) {
+ // Device Id is unknown.
+ } catch (CameraAccessException e) {
+ // Camera disabled by device policy.
+ }
+ if (c != null) {
+ int facingCharacteristic = c.get(CameraCharacteristics.LENS_FACING);
+ if (facingCharacteristic == CameraCharacteristics.LENS_FACING_FRONT) {
+ mFrontFacingCameraId = cameraIds[i];
+ } else if (facingCharacteristic == CameraCharacteristics.LENS_FACING_BACK) {
+ mRearFacingCameraId = cameraIds[i];
+ }
+ }
+ }
+
+ mIsInitialized = true;
+ Log.v(this, "initializeCameraList : done");
+ }
+
+ public void addCameraSelectionListener(Listener listener) {
+ if (listener != null) {
+ mCameraSelectionListeners.add(listener);
+ }
+ }
+
+ public void removeCameraSelectionListener(Listener listener) {
+ if (listener != null) {
+ mCameraSelectionListeners.remove(listener);
+ }
+ }
+
+ public interface Listener {
+
+ void onActiveCameraSelectionChanged(boolean isUsingFrontFacingCamera);
+
+ void onCameraPermissionGranted();
+ }
+}
diff --git a/java/com/android/incallui/InCallOrientationEventListener.java b/java/com/android/incallui/InCallOrientationEventListener.java
new file mode 100644
index 000000000..e6b0bc027
--- /dev/null
+++ b/java/com/android/incallui/InCallOrientationEventListener.java
@@ -0,0 +1,194 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.support.annotation.IntDef;
+import android.view.OrientationEventListener;
+import com.android.dialer.common.LogUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class listens to Orientation events and overrides onOrientationChanged which gets invoked
+ * when an orientation change occurs. When that happens, we notify InCallUI registrants of the
+ * change.
+ */
+public class InCallOrientationEventListener extends OrientationEventListener {
+
+ public static final int SCREEN_ORIENTATION_0 = 0;
+ public static final int SCREEN_ORIENTATION_90 = 90;
+ public static final int SCREEN_ORIENTATION_180 = 180;
+ public static final int SCREEN_ORIENTATION_270 = 270;
+ public static final int SCREEN_ORIENTATION_360 = 360;
+
+ /** Screen orientation angles one of 0, 90, 180, 270, 360 in degrees. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SCREEN_ORIENTATION_0,
+ SCREEN_ORIENTATION_90,
+ SCREEN_ORIENTATION_180,
+ SCREEN_ORIENTATION_270,
+ SCREEN_ORIENTATION_360,
+ SCREEN_ORIENTATION_UNKNOWN
+ })
+ public @interface ScreenOrientation {}
+
+ // We use SCREEN_ORIENTATION_USER so that reverse-portrait is not allowed.
+ public static final int ACTIVITY_PREFERENCE_ALLOW_ROTATION = ActivityInfo.SCREEN_ORIENTATION_USER;
+
+ public static final int ACTIVITY_PREFERENCE_DISALLOW_ROTATION =
+ ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+
+ /**
+ * This is to identify dead zones where we won't notify others of orientation changed. Say for e.g
+ * our threshold is x degrees. We will only notify UI when our current rotation is within x
+ * degrees right or left of the screen orientation angles. If it's not within those ranges, we
+ * return SCREEN_ORIENTATION_UNKNOWN and ignore it.
+ */
+ public static final int SCREEN_ORIENTATION_UNKNOWN = -1;
+
+ // Rotation threshold is 10 degrees. So if the rotation angle is within 10 degrees of any of
+ // the above angles, we will notify orientation changed.
+ private static final int ROTATION_THRESHOLD = 10;
+
+ /** Cache the current rotation of the device. */
+ @ScreenOrientation private static int sCurrentOrientation = SCREEN_ORIENTATION_0;
+
+ private boolean mEnabled = false;
+
+ public InCallOrientationEventListener(Context context) {
+ super(context);
+ }
+
+ private static boolean isWithinRange(int value, int begin, int end) {
+ return value >= begin && value < end;
+ }
+
+ private static boolean isWithinThreshold(int value, int center, int threshold) {
+ return isWithinRange(value, center - threshold, center + threshold);
+ }
+
+ private static boolean isInLeftRange(int value, int center, int threshold) {
+ return isWithinRange(value, center - threshold, center);
+ }
+
+ private static boolean isInRightRange(int value, int center, int threshold) {
+ return isWithinRange(value, center, center + threshold);
+ }
+
+ @ScreenOrientation
+ public static int getCurrentOrientation() {
+ return sCurrentOrientation;
+ }
+
+ /**
+ * Handles changes in device orientation. Notifies InCallPresenter of orientation changes.
+ *
+ * <p>Note that this API receives sensor rotation in degrees as a param and we convert that to one
+ * of our screen orientation constants - (one of: {@link #SCREEN_ORIENTATION_0}, {@link
+ * #SCREEN_ORIENTATION_90}, {@link #SCREEN_ORIENTATION_180}, {@link #SCREEN_ORIENTATION_270}).
+ *
+ * @param rotation The new device sensor rotation in degrees
+ */
+ @Override
+ public void onOrientationChanged(int rotation) {
+ if (rotation == OrientationEventListener.ORIENTATION_UNKNOWN) {
+ return;
+ }
+
+ final int orientation = toScreenOrientation(rotation);
+
+ if (orientation != SCREEN_ORIENTATION_UNKNOWN && sCurrentOrientation != orientation) {
+ LogUtil.i(
+ "InCallOrientationEventListener.onOrientationChanged",
+ "orientation: %d -> %d",
+ sCurrentOrientation,
+ orientation);
+ sCurrentOrientation = orientation;
+ InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation);
+ }
+ }
+
+ /**
+ * Enables the OrientationEventListener and notifies listeners of current orientation if notify
+ * flag is true
+ *
+ * @param notify true or false. Notify device orientation changed if true.
+ */
+ public void enable(boolean notify) {
+ if (mEnabled) {
+ Log.v(this, "enable: Orientation listener is already enabled. Ignoring...");
+ return;
+ }
+
+ super.enable();
+ mEnabled = true;
+ if (notify) {
+ InCallPresenter.getInstance().onDeviceOrientationChange(sCurrentOrientation);
+ }
+ }
+
+ /** Enables the OrientationEventListener with notify flag defaulting to false. */
+ @Override
+ public void enable() {
+ enable(false);
+ }
+
+ /** Disables the OrientationEventListener. */
+ @Override
+ public void disable() {
+ if (!mEnabled) {
+ Log.v(this, "enable: Orientation listener is already disabled. Ignoring...");
+ return;
+ }
+
+ mEnabled = false;
+ super.disable();
+ }
+
+ /** Returns true the OrientationEventListener is enabled, false otherwise. */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Converts sensor rotation in degrees to screen orientation constants.
+ *
+ * @param rotation sensor rotation angle in degrees
+ * @return Screen orientation angle in degrees (0, 90, 180, 270). Returns -1 for degrees not
+ * within threshold to identify zones where orientation change should not be trigerred.
+ */
+ @ScreenOrientation
+ private int toScreenOrientation(int rotation) {
+ // Sensor orientation 90 is equivalent to screen orientation 270 and vice versa. This
+ // function returns the screen orientation. So we convert sensor rotation 90 to 270 and
+ // vice versa here.
+ if (isInLeftRange(rotation, SCREEN_ORIENTATION_360, ROTATION_THRESHOLD)
+ || isInRightRange(rotation, SCREEN_ORIENTATION_0, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_0;
+ } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_90, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_270;
+ } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_180, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_180;
+ } else if (isWithinThreshold(rotation, SCREEN_ORIENTATION_270, ROTATION_THRESHOLD)) {
+ return SCREEN_ORIENTATION_90;
+ }
+ return SCREEN_ORIENTATION_UNKNOWN;
+ }
+}
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
new file mode 100644
index 000000000..97105fb78
--- /dev/null
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -0,0 +1,1679 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.Call.Details;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.view.Window;
+import android.view.WindowManager;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.InteractionEvent;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.TouchPointManager;
+import com.android.incallui.InCallOrientationEventListener.ScreenOrientation;
+import com.android.incallui.answerproximitysensor.PseudoScreenState;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.ExternalCallList;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier;
+import com.android.incallui.call.TelecomAdapter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.legacyblocking.BlockedNumberContentObserver;
+import com.android.incallui.spam.SpamCallListListener;
+import com.android.incallui.util.TelecomCallUtil;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Takes updates from the CallList and notifies the InCallActivity (UI) of the changes. Responsible
+ * for starting the activity for a new call and finishing the activity when all calls are
+ * disconnected. Creates and manages the in-call state and provides a listener pattern for the
+ * presenters that want to listen in on the in-call state changes. TODO: This class has become more
+ * of a state machine at this point. Consider renaming.
+ */
+public class InCallPresenter
+ implements CallList.Listener, InCallVideoCallCallbackNotifier.SessionModificationListener {
+
+ private static final String EXTRA_FIRST_TIME_SHOWN =
+ "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
+
+ private static final long BLOCK_QUERY_TIMEOUT_MS = 1000;
+
+ private static final Bundle EMPTY_EXTRAS = new Bundle();
+
+ private static InCallPresenter sInCallPresenter;
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<InCallStateListener> mListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1));
+
+ private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>();
+ private final Set<InCallDetailsListener> mDetailsListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1));
+ private final Set<CanAddCallListener> mCanAddCallListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<CanAddCallListener, Boolean>(8, 0.9f, 1));
+ private final Set<InCallUiListener> mInCallUiListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallUiListener, Boolean>(8, 0.9f, 1));
+ private final Set<InCallOrientationListener> mOrientationListeners =
+ Collections.newSetFromMap(
+ new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1));
+ private final Set<InCallEventListener> mInCallEventListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1));
+
+ private StatusBarNotifier mStatusBarNotifier;
+ private ExternalCallNotifier mExternalCallNotifier;
+ private ContactInfoCache mContactInfoCache;
+ private Context mContext;
+ private final OnCheckBlockedListener mOnCheckBlockedListener =
+ new OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(final Integer id) {
+ if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ // Silence the ringer now to prevent ringing and vibration before the call is
+ // terminated when Telecom attempts to add it.
+ TelecomUtil.silenceRinger(mContext);
+ }
+ }
+ };
+ private CallList mCallList;
+ private ExternalCallList mExternalCallList;
+ private InCallActivity mInCallActivity;
+ private ManageConferenceActivity mManageConferenceActivity;
+ private final android.telecom.Call.Callback mCallCallback =
+ new android.telecom.Call.Callback() {
+ @Override
+ public void onPostDialWait(
+ android.telecom.Call telecomCall, String remainingPostDialSequence) {
+ final DialerCall call = mCallList.getDialerCallFromTelecomCall(telecomCall);
+ if (call == null) {
+ Log.w(this, "DialerCall not found in call list: " + telecomCall);
+ return;
+ }
+ onPostDialCharWait(call.getId(), remainingPostDialSequence);
+ }
+
+ @Override
+ public void onDetailsChanged(
+ android.telecom.Call telecomCall, android.telecom.Call.Details details) {
+ final DialerCall call = mCallList.getDialerCallFromTelecomCall(telecomCall);
+ if (call == null) {
+ Log.w(this, "DialerCall not found in call list: " + telecomCall);
+ return;
+ }
+
+ if (details.hasProperty(Details.PROPERTY_IS_EXTERNAL_CALL)
+ && !mExternalCallList.isCallTracked(telecomCall)) {
+
+ // A regular call became an external call so swap call lists.
+ Log.i(this, "Call became external: " + telecomCall);
+ mCallList.onInternalCallMadeExternal(mContext, telecomCall);
+ mExternalCallList.onCallAdded(telecomCall);
+ return;
+ }
+
+ for (InCallDetailsListener listener : mDetailsListeners) {
+ listener.onDetailsChanged(call, details);
+ }
+ }
+
+ @Override
+ public void onConferenceableCallsChanged(
+ android.telecom.Call telecomCall, List<android.telecom.Call> conferenceableCalls) {
+ Log.i(this, "onConferenceableCallsChanged: " + telecomCall);
+ onDetailsChanged(telecomCall, telecomCall.getDetails());
+ }
+ };
+ private InCallState mInCallState = InCallState.NO_CALLS;
+ private ProximitySensor mProximitySensor;
+ private final PseudoScreenState mPseudoScreenState = new PseudoScreenState();
+ private boolean mServiceConnected;
+ private boolean mAccountSelectionCancelled;
+ private InCallCameraManager mInCallCameraManager;
+ private FilteredNumberAsyncQueryHandler mFilteredQueryHandler;
+ private CallList.Listener mSpamCallListListener;
+ /** Whether or not we are currently bound and waiting for Telecom to send us a new call. */
+ private boolean mBoundAndWaitingForOutgoingCall;
+ /** Determines if the InCall UI is in fullscreen mode or not. */
+ private boolean mIsFullScreen = false;
+
+ private PhoneStateListener mPhoneStateListener =
+ new PhoneStateListener() {
+ @Override
+ public void onCallStateChanged(int state, String incomingNumber) {
+ if (state == TelephonyManager.CALL_STATE_RINGING) {
+ if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) {
+ return;
+ }
+ // Check if the number is blocked, to silence the ringer.
+ String countryIso = GeoUtil.getCurrentCountryIso(mContext);
+ mFilteredQueryHandler.isBlockedNumber(
+ mOnCheckBlockedListener, incomingNumber, countryIso);
+ }
+ }
+ };
+ /**
+ * Is true when the activity has been previously started. Some code needs to know not just if the
+ * activity is currently up, but if it had been previously shown in foreground for this in-call
+ * session (e.g., StatusBarNotifier). This gets reset when the session ends in the tear-down
+ * method.
+ */
+ private boolean mIsActivityPreviouslyStarted = false;
+
+ /** Whether or not InCallService is bound to Telecom. */
+ private boolean mServiceBound = false;
+
+ /**
+ * When configuration changes Android kills the current activity and starts a new one. The flag is
+ * used to check if full clean up is necessary (activity is stopped and new activity won't be
+ * started), or if a new activity will be started right after the current one is destroyed, and
+ * therefore no need in release all resources.
+ */
+ private boolean mIsChangingConfigurations = false;
+
+ private boolean mAwaitingCallListUpdate = false;
+
+ private ExternalCallList.ExternalCallListener mExternalCallListener =
+ new ExternalCallList.ExternalCallListener() {
+
+ @Override
+ public void onExternalCallPulled(android.telecom.Call call) {
+ // Note: keep this code in sync with InCallPresenter#onCallAdded
+ LatencyReport latencyReport = new LatencyReport(call);
+ latencyReport.onCallBlockingDone();
+ // Note: External calls do not require spam checking.
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ call.registerCallback(mCallCallback);
+ }
+
+ @Override
+ public void onExternalCallAdded(android.telecom.Call call) {
+ // No-op
+ }
+
+ @Override
+ public void onExternalCallRemoved(android.telecom.Call call) {
+ // No-op
+ }
+
+ @Override
+ public void onExternalCallUpdated(android.telecom.Call call) {
+ // No-op
+ }
+ };
+
+ private ThemeColorManager mThemeColorManager;
+ private VideoSurfaceTexture mLocalVideoSurfaceTexture;
+ private VideoSurfaceTexture mRemoteVideoSurfaceTexture;
+
+ /** Inaccessible constructor. Must use getInstance() to get this singleton. */
+ @VisibleForTesting
+ InCallPresenter() {}
+
+ public static synchronized InCallPresenter getInstance() {
+ if (sInCallPresenter == null) {
+ sInCallPresenter = new InCallPresenter();
+ }
+ return sInCallPresenter;
+ }
+
+ /**
+ * Determines whether or not a call has no valid phone accounts that can be used to make the call
+ * with. Emergency calls do not require a phone account.
+ *
+ * @param call to check accounts for.
+ * @return {@code true} if the call has no call capable phone accounts set, {@code false} if the
+ * call contains a phone account that could be used to initiate it with, or is an emergency
+ * call.
+ */
+ public static boolean isCallWithNoValidAccounts(DialerCall call) {
+ if (call != null && !call.isEmergencyCall()) {
+ Bundle extras = call.getIntentExtras();
+
+ if (extras == null) {
+ extras = EMPTY_EXTRAS;
+ }
+
+ final List<PhoneAccountHandle> phoneAccountHandles =
+ extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
+
+ if ((call.getAccountHandle() == null
+ && (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) {
+ Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public InCallState getInCallState() {
+ return mInCallState;
+ }
+
+ public CallList getCallList() {
+ return mCallList;
+ }
+
+ public void setUp(
+ @NonNull Context context,
+ CallList callList,
+ ExternalCallList externalCallList,
+ StatusBarNotifier statusBarNotifier,
+ ExternalCallNotifier externalCallNotifier,
+ ContactInfoCache contactInfoCache,
+ ProximitySensor proximitySensor) {
+ if (mServiceConnected) {
+ Log.i(this, "New service connection replacing existing one.");
+ if (context != mContext || callList != mCallList) {
+ throw new IllegalStateException();
+ }
+ return;
+ }
+
+ Objects.requireNonNull(context);
+ mContext = context;
+
+ mContactInfoCache = contactInfoCache;
+
+ mStatusBarNotifier = statusBarNotifier;
+ mExternalCallNotifier = externalCallNotifier;
+ addListener(mStatusBarNotifier);
+
+ mProximitySensor = proximitySensor;
+ addListener(mProximitySensor);
+
+ mThemeColorManager =
+ new ThemeColorManager(new InCallUIMaterialColorMapUtils(mContext.getResources()));
+
+ mCallList = callList;
+ mExternalCallList = externalCallList;
+ externalCallList.addExternalCallListener(mExternalCallNotifier);
+ externalCallList.addExternalCallListener(mExternalCallListener);
+
+ // This only gets called by the service so this is okay.
+ mServiceConnected = true;
+
+ // The final thing we do in this set up is add ourselves as a listener to CallList. This
+ // will kick off an update and the whole process can start.
+ mCallList.addListener(this);
+
+ // Create spam call list listener and add it to the list of listeners
+ mSpamCallListListener = new SpamCallListListener(context);
+ mCallList.addListener(mSpamCallListListener);
+
+ VideoPauseController.getInstance().setUp(this);
+ InCallVideoCallCallbackNotifier.getInstance().addSessionModificationListener(this);
+
+ mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context);
+ mContext
+ .getSystemService(TelephonyManager.class)
+ .listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
+
+ Log.d(this, "Finished InCallPresenter.setUp");
+ }
+
+ /**
+ * Called when the telephony service has disconnected from us. This will happen when there are no
+ * more active calls. However, we may still want to continue showing the UI for certain cases like
+ * showing "Call Ended". What we really want is to wait for the activity and the service to both
+ * disconnect before we tear things down. This method sets a serviceConnected boolean and calls a
+ * secondary method that performs the aforementioned logic.
+ */
+ public void tearDown() {
+ Log.d(this, "tearDown");
+ mCallList.clearOnDisconnect();
+
+ mServiceConnected = false;
+
+ mContext
+ .getSystemService(TelephonyManager.class)
+ .listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
+
+ attemptCleanup();
+ VideoPauseController.getInstance().tearDown();
+ InCallVideoCallCallbackNotifier.getInstance().removeSessionModificationListener(this);
+ }
+
+ private void attemptFinishActivity() {
+ final boolean doFinish = (mInCallActivity != null && isActivityStarted());
+ Log.i(this, "Hide in call UI: " + doFinish);
+ if (doFinish) {
+ mInCallActivity.setExcludeFromRecents(true);
+ mInCallActivity.finish();
+
+ if (mAccountSelectionCancelled) {
+ // This finish is a result of account selection cancellation
+ // do not include activity ending transition
+ mInCallActivity.overridePendingTransition(0, 0);
+ }
+ }
+ }
+
+ /**
+ * Called when the UI ends. Attempts to tear down everything if necessary. See {@link #tearDown()}
+ * for more insight on the tear-down process.
+ */
+ public void unsetActivity(InCallActivity inCallActivity) {
+ if (inCallActivity == null) {
+ throw new IllegalArgumentException("unregisterActivity cannot be called with null");
+ }
+ if (mInCallActivity == null) {
+ Log.i(this, "No InCallActivity currently set, no need to unset.");
+ return;
+ }
+ if (mInCallActivity != inCallActivity) {
+ Log.w(
+ this,
+ "Second instance of InCallActivity is trying to unregister when another"
+ + " instance is active. Ignoring.");
+ return;
+ }
+ updateActivity(null);
+ }
+
+ /**
+ * Updates the current instance of {@link InCallActivity} with the provided one. If a {@code null}
+ * activity is provided, it means that the activity was finished and we should attempt to cleanup.
+ */
+ private void updateActivity(InCallActivity inCallActivity) {
+ boolean updateListeners = false;
+ boolean doAttemptCleanup = false;
+
+ if (inCallActivity != null) {
+ if (mInCallActivity == null) {
+ updateListeners = true;
+ Log.i(this, "UI Initialized");
+ } else {
+ // since setActivity is called onStart(), it can be called multiple times.
+ // This is fine and ignorable, but we do not want to update the world every time
+ // this happens (like going to/from background) so we do not set updateListeners.
+ }
+
+ mInCallActivity = inCallActivity;
+ mInCallActivity.setExcludeFromRecents(false);
+
+ // By the time the UI finally comes up, the call may already be disconnected.
+ // If that's the case, we may need to show an error dialog.
+ if (mCallList != null && mCallList.getDisconnectedCall() != null) {
+ maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall());
+ }
+
+ // When the UI comes up, we need to first check the in-call state.
+ // If we are showing NO_CALLS, that means that a call probably connected and
+ // then immediately disconnected before the UI was able to come up.
+ // If we dont have any calls, start tearing down the UI instead.
+ // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after
+ // it has been set.
+ if (mInCallState == InCallState.NO_CALLS) {
+ Log.i(this, "UI Initialized, but no calls left. shut down.");
+ attemptFinishActivity();
+ return;
+ }
+ } else {
+ Log.i(this, "UI Destroyed");
+ updateListeners = true;
+ mInCallActivity = null;
+
+ // We attempt cleanup for the destroy case but only after we recalculate the state
+ // to see if we need to come back up or stay shut down. This is why we do the
+ // cleanup after the call to onCallListChange() instead of directly here.
+ doAttemptCleanup = true;
+ }
+
+ // Messages can come from the telephony layer while the activity is coming up
+ // and while the activity is going down. So in both cases we need to recalculate what
+ // state we should be in after they complete.
+ // Examples: (1) A new incoming call could come in and then get disconnected before
+ // the activity is created.
+ // (2) All calls could disconnect and then get a new incoming call before the
+ // activity is destroyed.
+ //
+ // b/1122139 - We previously had a check for mServiceConnected here as well, but there are
+ // cases where we need to recalculate the current state even if the service in not
+ // connected. In particular the case where startOrFinish() is called while the app is
+ // already finish()ing. In that case, we skip updating the state with the knowledge that
+ // we will check again once the activity has finished. That means we have to recalculate the
+ // state here even if the service is disconnected since we may not have finished a state
+ // transition while finish()ing.
+ if (updateListeners) {
+ onCallListChange(mCallList);
+ }
+
+ if (doAttemptCleanup) {
+ attemptCleanup();
+ }
+ }
+
+ public void setManageConferenceActivity(
+ @Nullable ManageConferenceActivity manageConferenceActivity) {
+ mManageConferenceActivity = manageConferenceActivity;
+ }
+
+ public void onBringToForeground(boolean showDialpad) {
+ Log.i(this, "Bringing UI to foreground.");
+ bringToForeground(showDialpad);
+ }
+
+ public void onCallAdded(final android.telecom.Call call) {
+ LatencyReport latencyReport = new LatencyReport(call);
+ if (shouldAttemptBlocking(call)) {
+ maybeBlockCall(call, latencyReport);
+ } else {
+ if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ mExternalCallList.onCallAdded(call);
+ } else {
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ }
+
+ // Since a call has been added we are no longer waiting for Telecom to send us a call.
+ setBoundAndWaitingForOutgoingCall(false, null);
+ call.registerCallback(mCallCallback);
+ }
+
+ private boolean shouldAttemptBlocking(android.telecom.Call call) {
+ if (call.getState() != android.telecom.Call.STATE_RINGING) {
+ return false;
+ }
+ if (TelecomCallUtil.isEmergencyCall(call)) {
+ Log.i(this, "Not attempting to block incoming emergency call");
+ return false;
+ }
+ if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) {
+ Log.i(this, "Not attempting to block incoming call due to recent emergency call");
+ return false;
+ }
+ if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether a call should be blocked, and blocks it if so. Otherwise, it adds the call to
+ * the CallList so it can proceed as normal. There is a timeout, so if the function for checking
+ * whether a function is blocked does not return in a reasonable time, we proceed with adding the
+ * call anyways.
+ */
+ private void maybeBlockCall(final android.telecom.Call call, final LatencyReport latencyReport) {
+ final String countryIso = GeoUtil.getCurrentCountryIso(mContext);
+ final String number = TelecomCallUtil.getNumber(call);
+ final long timeAdded = System.currentTimeMillis();
+
+ // Though AtomicBoolean's can be scary, don't fear, as in this case it is only used on the
+ // main UI thread. It is needed so we can change its value within different scopes, since
+ // that cannot be done with a final boolean.
+ final AtomicBoolean hasTimedOut = new AtomicBoolean(false);
+
+ final Handler handler = new Handler();
+
+ // Proceed if the query is slow; the call may still be blocked after the query returns.
+ final Runnable runnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ hasTimedOut.set(true);
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ };
+ handler.postDelayed(runnable, BLOCK_QUERY_TIMEOUT_MS);
+
+ OnCheckBlockedListener onCheckBlockedListener =
+ new OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(final Integer id) {
+ if (isReadyForTearDown()) {
+ Log.i(this, "InCallPresenter is torn down, not adding call");
+ return;
+ }
+ if (!hasTimedOut.get()) {
+ handler.removeCallbacks(runnable);
+ }
+ if (id == null) {
+ if (!hasTimedOut.get()) {
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ Log.d(this, "checkForBlockedCall: invalid number, skipping block checking");
+ if (!hasTimedOut.get()) {
+ handler.removeCallbacks(runnable);
+
+ latencyReport.onCallBlockingDone();
+ mCallList.onCallAdded(mContext, call, latencyReport);
+ }
+ } else {
+ Log.i(this, "Rejecting incoming call from blocked number");
+ call.reject(false, null);
+ Logger.get(mContext).logInteraction(InteractionEvent.Type.CALL_BLOCKED);
+
+ /*
+ * If mContext is null, then the InCallPresenter was torn down before the
+ * block check had a chance to complete. The context is no longer valid, so
+ * don't attempt to remove the call log entry.
+ */
+ if (mContext == null) {
+ return;
+ }
+ // Register observer to update the call log.
+ // BlockedNumberContentObserver will unregister after successful log or timeout.
+ BlockedNumberContentObserver contentObserver =
+ new BlockedNumberContentObserver(mContext, new Handler(), number, timeAdded);
+ contentObserver.register();
+ }
+ }
+ };
+
+ mFilteredQueryHandler.isBlockedNumber(onCheckBlockedListener, number, countryIso);
+ }
+
+ public void onCallRemoved(android.telecom.Call call) {
+ if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ mExternalCallList.onCallRemoved(call);
+ } else {
+ mCallList.onCallRemoved(mContext, call);
+ call.unregisterCallback(mCallCallback);
+ }
+ }
+
+ public void onCanAddCallChanged(boolean canAddCall) {
+ for (CanAddCallListener listener : mCanAddCallListeners) {
+ listener.onCanAddCallChanged(canAddCall);
+ }
+ }
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) {
+ if (mInCallActivity != null) {
+ mInCallActivity.onWiFiToLteHandover(call);
+ }
+ }
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) {
+ if (mInCallActivity != null) {
+ mInCallActivity.onHandoverToWifiFailed(call);
+ }
+ }
+
+ /**
+ * Called when there is a change to the call list. Sets the In-Call state for the entire in-call
+ * app based on the information it gets from CallList. Dispatches the in-call state to all
+ * listeners. Can trigger the creation or destruction of the UI based on the states that is
+ * calculates.
+ */
+ @Override
+ public void onCallListChange(CallList callList) {
+ if (mInCallActivity != null && mInCallActivity.isInCallScreenAnimating()) {
+ mAwaitingCallListUpdate = true;
+ return;
+ }
+ if (callList == null) {
+ return;
+ }
+
+ mAwaitingCallListUpdate = false;
+
+ InCallState newState = getPotentialStateFromCallList(callList);
+ InCallState oldState = mInCallState;
+ Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState);
+ newState = startOrFinishUi(newState);
+ Log.d(this, "onCallListChange newState changed to " + newState);
+
+ // Set the new state before announcing it to the world
+ Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
+ mInCallState = newState;
+
+ // notify listeners of new state
+ for (InCallStateListener listener : mListeners) {
+ Log.d(this, "Notify " + listener + " of state " + mInCallState.toString());
+ listener.onStateChange(oldState, mInCallState, callList);
+ }
+
+ if (isActivityStarted()) {
+ final boolean hasCall =
+ callList.getActiveOrBackgroundCall() != null || callList.getOutgoingCall() != null;
+ mInCallActivity.dismissKeyguard(hasCall);
+ }
+ }
+
+ /** Called when there is a new incoming call. */
+ @Override
+ public void onIncomingCall(DialerCall call) {
+ InCallState newState = startOrFinishUi(InCallState.INCOMING);
+ InCallState oldState = mInCallState;
+
+ Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
+ mInCallState = newState;
+
+ for (IncomingCallListener listener : mIncomingCallListeners) {
+ listener.onIncomingCall(oldState, mInCallState, call);
+ }
+
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) {
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ && mInCallState == InCallPresenter.InCallState.INCOMING) {
+ LogUtil.i(
+ "InCallPresenter.onUpgradeToVideo",
+ "rejecting upgrade request due to existing incoming call");
+ call.declineUpgradeRequest();
+ }
+
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ @Override
+ public void onSessionModificationStateChange(@SessionModificationState int newState) {
+ LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState);
+ if (mProximitySensor == null) {
+ LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null");
+ return;
+ }
+ mProximitySensor.setIsAttemptingVideoCall(
+ VideoUtils.hasSentVideoUpgradeRequest(newState)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(newState));
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ /**
+ * Called when a call becomes disconnected. Called everytime an existing call changes from being
+ * connected (incoming/outgoing/active) to disconnected.
+ */
+ @Override
+ public void onDisconnect(DialerCall call) {
+ maybeShowErrorDialogOnDisconnect(call);
+
+ // We need to do the run the same code as onCallListChange.
+ onCallListChange(mCallList);
+
+ if (isActivityStarted()) {
+ mInCallActivity.dismissKeyguard(false);
+ }
+
+ if (call.isEmergencyCall()) {
+ FilteredNumbersUtil.recordLastEmergencyCallTime(mContext);
+ }
+ }
+
+ @Override
+ public void onUpgradeToVideoRequest(DialerCall call, int videoState) {
+ LogUtil.d(
+ "InCallPresenter.onUpgradeToVideoRequest",
+ "call = " + call + " video state = " + videoState);
+
+ if (call == null) {
+ return;
+ }
+
+ call.setRequestedVideoState(videoState);
+ }
+
+ /** Given the call list, return the state in which the in-call screen should be. */
+ public InCallState getPotentialStateFromCallList(CallList callList) {
+
+ InCallState newState = InCallState.NO_CALLS;
+
+ if (callList == null) {
+ return newState;
+ }
+ if (callList.getIncomingCall() != null) {
+ newState = InCallState.INCOMING;
+ } else if (callList.getWaitingForAccountCall() != null) {
+ newState = InCallState.WAITING_FOR_ACCOUNT;
+ } else if (callList.getPendingOutgoingCall() != null) {
+ newState = InCallState.PENDING_OUTGOING;
+ } else if (callList.getOutgoingCall() != null) {
+ newState = InCallState.OUTGOING;
+ } else if (callList.getActiveCall() != null
+ || callList.getBackgroundCall() != null
+ || callList.getDisconnectedCall() != null
+ || callList.getDisconnectingCall() != null) {
+ newState = InCallState.INCALL;
+ }
+
+ if (newState == InCallState.NO_CALLS) {
+ if (mBoundAndWaitingForOutgoingCall) {
+ return InCallState.OUTGOING;
+ }
+ }
+
+ return newState;
+ }
+
+ public boolean isBoundAndWaitingForOutgoingCall() {
+ return mBoundAndWaitingForOutgoingCall;
+ }
+
+ public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) {
+ Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound);
+ mBoundAndWaitingForOutgoingCall = isBound;
+ mThemeColorManager.setPendingPhoneAccountHandle(handle);
+ if (isBound && mInCallState == InCallState.NO_CALLS) {
+ mInCallState = InCallState.OUTGOING;
+ }
+ }
+
+ public void onShrinkAnimationComplete() {
+ if (mAwaitingCallListUpdate) {
+ onCallListChange(mCallList);
+ }
+ }
+
+ public void addIncomingCallListener(IncomingCallListener listener) {
+ Objects.requireNonNull(listener);
+ mIncomingCallListeners.add(listener);
+ }
+
+ public void removeIncomingCallListener(IncomingCallListener listener) {
+ if (listener != null) {
+ mIncomingCallListeners.remove(listener);
+ }
+ }
+
+ public void addListener(InCallStateListener listener) {
+ Objects.requireNonNull(listener);
+ mListeners.add(listener);
+ }
+
+ public void removeListener(InCallStateListener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ public void addDetailsListener(InCallDetailsListener listener) {
+ Objects.requireNonNull(listener);
+ mDetailsListeners.add(listener);
+ }
+
+ public void removeDetailsListener(InCallDetailsListener listener) {
+ if (listener != null) {
+ mDetailsListeners.remove(listener);
+ }
+ }
+
+ public void addCanAddCallListener(CanAddCallListener listener) {
+ Objects.requireNonNull(listener);
+ mCanAddCallListeners.add(listener);
+ }
+
+ public void removeCanAddCallListener(CanAddCallListener listener) {
+ if (listener != null) {
+ mCanAddCallListeners.remove(listener);
+ }
+ }
+
+ public void addOrientationListener(InCallOrientationListener listener) {
+ Objects.requireNonNull(listener);
+ mOrientationListeners.add(listener);
+ }
+
+ public void removeOrientationListener(InCallOrientationListener listener) {
+ if (listener != null) {
+ mOrientationListeners.remove(listener);
+ }
+ }
+
+ public void addInCallEventListener(InCallEventListener listener) {
+ Objects.requireNonNull(listener);
+ mInCallEventListeners.add(listener);
+ }
+
+ public void removeInCallEventListener(InCallEventListener listener) {
+ if (listener != null) {
+ mInCallEventListeners.remove(listener);
+ }
+ }
+
+ public ProximitySensor getProximitySensor() {
+ return mProximitySensor;
+ }
+
+ public PseudoScreenState getPseudoScreenState() {
+ return mPseudoScreenState;
+ }
+
+ /** Returns true if the incall app is the foreground application. */
+ public boolean isShowingInCallUi() {
+ if (!isActivityStarted()) {
+ return false;
+ }
+ if (mManageConferenceActivity != null && mManageConferenceActivity.isVisible()) {
+ return true;
+ }
+ return mInCallActivity.isVisible();
+ }
+
+ /**
+ * Returns true if the activity has been created and is running. Returns true as long as activity
+ * is not destroyed or finishing. This ensures that we return true even if the activity is paused
+ * (not in foreground).
+ */
+ public boolean isActivityStarted() {
+ return (mInCallActivity != null
+ && !mInCallActivity.isDestroyed()
+ && !mInCallActivity.isFinishing());
+ }
+
+ /**
+ * Determines if the In-Call app is currently changing configuration.
+ *
+ * @return {@code true} if the In-Call app is changing configuration.
+ */
+ public boolean isChangingConfigurations() {
+ return mIsChangingConfigurations;
+ }
+
+ /**
+ * Tracks whether the In-Call app is currently in the process of changing configuration (i.e.
+ * screen orientation).
+ */
+ /*package*/
+ void updateIsChangingConfigurations() {
+ mIsChangingConfigurations = false;
+ if (mInCallActivity != null) {
+ mIsChangingConfigurations = mInCallActivity.isChangingConfigurations();
+ }
+ Log.v(this, "updateIsChangingConfigurations = " + mIsChangingConfigurations);
+ }
+
+ /** Called when the activity goes in/out of the foreground. */
+ public void onUiShowing(boolean showing) {
+ // We need to update the notification bar when we leave the UI because that
+ // could trigger it to show again.
+ if (mStatusBarNotifier != null) {
+ mStatusBarNotifier.updateNotification(mCallList);
+ }
+
+ if (mProximitySensor != null) {
+ mProximitySensor.onInCallShowing(showing);
+ }
+
+ Intent broadcastIntent = Bindings.get(mContext).getUiReadyBroadcastIntent(mContext);
+ if (broadcastIntent != null) {
+ broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted);
+
+ if (showing) {
+ Log.d(this, "Sending sticky broadcast: ", broadcastIntent);
+ mContext.sendStickyBroadcast(broadcastIntent);
+ } else {
+ Log.d(this, "Removing sticky broadcast: ", broadcastIntent);
+ mContext.removeStickyBroadcast(broadcastIntent);
+ }
+ }
+
+ if (showing) {
+ mIsActivityPreviouslyStarted = true;
+ } else {
+ updateIsChangingConfigurations();
+ }
+
+ for (InCallUiListener listener : mInCallUiListeners) {
+ listener.onUiShowing(showing);
+ }
+
+ if (mInCallActivity != null) {
+ // Re-evaluate which fragment is being shown.
+ mInCallActivity.onPrimaryCallStateChanged();
+ }
+ }
+
+ public void addInCallUiListener(InCallUiListener listener) {
+ mInCallUiListeners.add(listener);
+ }
+
+ public boolean removeInCallUiListener(InCallUiListener listener) {
+ return mInCallUiListeners.remove(listener);
+ }
+
+ /*package*/
+ void onActivityStarted() {
+ Log.d(this, "onActivityStarted");
+ notifyVideoPauseController(true);
+ mStatusBarNotifier.updateNotification(mCallList);
+ }
+
+ /*package*/
+ void onActivityStopped() {
+ Log.d(this, "onActivityStopped");
+ notifyVideoPauseController(false);
+ }
+
+ private void notifyVideoPauseController(boolean showing) {
+ Log.d(
+ this, "notifyVideoPauseController: mIsChangingConfigurations=" + mIsChangingConfigurations);
+ if (!mIsChangingConfigurations) {
+ VideoPauseController.getInstance().onUiShowing(showing);
+ }
+ }
+
+ /** Brings the app into the foreground if possible. */
+ public void bringToForeground(boolean showDialpad) {
+ // Before we bring the incall UI to the foreground, we check to see if:
+ // 1. It is not currently in the foreground
+ // 2. We are in a state where we want to show the incall ui (i.e. there are calls to
+ // be displayed)
+ // If the activity hadn't actually been started previously, yet there are still calls
+ // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
+ // bring it up the UI regardless.
+ if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
+ showInCall(showDialpad, false /* newOutgoingCall */, false /* isVideoCall */);
+ }
+ }
+
+ public void onPostDialCharWait(String callId, String chars) {
+ if (isActivityStarted()) {
+ mInCallActivity.showPostCharWaitDialog(callId, chars);
+ }
+ }
+
+ /**
+ * Handles the green CALL key while in-call.
+ *
+ * @return true if we consumed the event.
+ */
+ public boolean handleCallKey() {
+ LogUtil.v("InCallPresenter.handleCallKey", null);
+
+ // The green CALL button means either "Answer", "Unhold", or
+ // "Swap calls", or can be a no-op, depending on the current state
+ // of the Phone.
+
+ /** INCOMING CALL */
+ final CallList calls = mCallList;
+ final DialerCall incomingCall = calls.getIncomingCall();
+ LogUtil.v("InCallPresenter.handleCallKey", "incomingCall: " + incomingCall);
+
+ // (1) Attempt to answer a call
+ if (incomingCall != null) {
+ incomingCall.answer(VideoProfile.STATE_AUDIO_ONLY);
+ return true;
+ }
+
+ /** STATE_ACTIVE CALL */
+ final DialerCall activeCall = calls.getActiveCall();
+ if (activeCall != null) {
+ // TODO: This logic is repeated from CallButtonPresenter.java. We should
+ // consolidate this logic.
+ final boolean canMerge =
+ activeCall.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE);
+ final boolean canSwap =
+ activeCall.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE);
+
+ Log.v(
+ this, "activeCall: " + activeCall + ", canMerge: " + canMerge + ", canSwap: " + canSwap);
+
+ // (2) Attempt actions on conference calls
+ if (canMerge) {
+ TelecomAdapter.getInstance().merge(activeCall.getId());
+ return true;
+ } else if (canSwap) {
+ TelecomAdapter.getInstance().swap(activeCall.getId());
+ return true;
+ }
+ }
+
+ /** BACKGROUND CALL */
+ final DialerCall heldCall = calls.getBackgroundCall();
+ if (heldCall != null) {
+ // We have a hold call so presumeable it will always support HOLD...but
+ // there is no harm in double checking.
+ final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD);
+
+ Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold);
+
+ // (4) unhold call
+ if (heldCall.getState() == DialerCall.State.ONHOLD && canHold) {
+ heldCall.unhold();
+ return true;
+ }
+ }
+
+ // Always consume hard keys
+ return true;
+ }
+
+ /**
+ * A dialog could have prevented in-call screen from being previously finished. This function
+ * checks to see if there should be any UI left and if not attempts to tear down the UI.
+ */
+ public void onDismissDialog() {
+ Log.i(this, "Dialog dismissed");
+ if (mInCallState == InCallState.NO_CALLS) {
+ attemptFinishActivity();
+ attemptCleanup();
+ }
+ }
+
+ /** Clears the previous fullscreen state. */
+ public void clearFullscreen() {
+ mIsFullScreen = false;
+ }
+
+ /**
+ * Changes the fullscreen mode of the in-call UI.
+ *
+ * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false}
+ * otherwise.
+ */
+ public void setFullScreen(boolean isFullScreen) {
+ setFullScreen(isFullScreen, false /* force */);
+ }
+
+ /**
+ * Changes the fullscreen mode of the in-call UI.
+ *
+ * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false}
+ * otherwise.
+ * @param force {@code true} if fullscreen mode should be set regardless of its current state.
+ */
+ public void setFullScreen(boolean isFullScreen, boolean force) {
+ Log.i(this, "setFullScreen = " + isFullScreen);
+
+ // As a safeguard, ensure we cannot enter fullscreen if the dialpad is shown.
+ if (isDialpadVisible()) {
+ isFullScreen = false;
+ Log.v(this, "setFullScreen overridden as dialpad is shown = " + isFullScreen);
+ }
+
+ if (mIsFullScreen == isFullScreen && !force) {
+ Log.v(this, "setFullScreen ignored as already in that state.");
+ return;
+ }
+ mIsFullScreen = isFullScreen;
+ notifyFullscreenModeChange(mIsFullScreen);
+ }
+
+ /**
+ * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false}
+ * otherwise.
+ */
+ public boolean isFullscreen() {
+ return mIsFullScreen;
+ }
+
+ /**
+ * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status.
+ *
+ * @param isFullscreenMode {@code True} if entering full screen mode.
+ */
+ public void notifyFullscreenModeChange(boolean isFullscreenMode) {
+ for (InCallEventListener listener : mInCallEventListeners) {
+ listener.onFullscreenModeChanged(isFullscreenMode);
+ }
+ }
+
+ /**
+ * For some disconnected causes, we show a dialog. This calls into the activity to show the dialog
+ * if appropriate for the call.
+ */
+ private void maybeShowErrorDialogOnDisconnect(DialerCall call) {
+ // For newly disconnected calls, we may want to show a dialog on specific error conditions
+ if (isActivityStarted() && call.getState() == DialerCall.State.DISCONNECTED) {
+ if (call.getAccountHandle() == null && !call.isConferenceCall()) {
+ setDisconnectCauseForMissingAccounts(call);
+ }
+ mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause());
+ }
+ }
+
+ /**
+ * When the state of in-call changes, this is the first method to get called. It determines if the
+ * UI needs to be started or finished depending on the new state and does it.
+ */
+ private InCallState startOrFinishUi(InCallState newState) {
+ Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState);
+
+ // TODO: Consider a proper state machine implementation
+
+ // If the state isn't changing we have already done any starting/stopping of activities in
+ // a previous pass...so lets cut out early
+ if (newState == mInCallState) {
+ return newState;
+ }
+
+ // A new Incoming call means that the user needs to be notified of the the call (since
+ // it wasn't them who initiated it). We do this through full screen notifications and
+ // happens indirectly through {@link StatusBarNotifier}.
+ //
+ // The process for incoming calls is as follows:
+ //
+ // 1) CallList - Announces existence of new INCOMING call
+ // 2) InCallPresenter - Gets announcement and calculates that the new InCallState
+ // - should be set to INCOMING.
+ // 3) InCallPresenter - This method is called to see if we need to start or finish
+ // the app given the new state.
+ // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls
+ // StatusBarNotifier explicitly to issue a FullScreen Notification
+ // that will either start the InCallActivity or show the user a
+ // top-level notification dialog if the user is in an immersive app.
+ // That notification can also start the InCallActivity.
+ // 5) InCallActivity - Main activity starts up and at the end of its onCreate will
+ // call InCallPresenter::setActivity() to let the presenter
+ // know that start-up is complete.
+ //
+ // [ AND NOW YOU'RE IN THE CALL. voila! ]
+ //
+ // Our app is started using a fullScreen notification. We need to do this whenever
+ // we get an incoming call. Depending on the current context of the device, either a
+ // incoming call HUN or the actual InCallActivity will be shown.
+ final boolean startIncomingCallSequence = (InCallState.INCOMING == newState);
+
+ // A dialog to show on top of the InCallUI to select a PhoneAccount
+ final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState);
+
+ // A new outgoing call indicates that the user just now dialed a number and when that
+ // happens we need to display the screen immediately or show an account picker dialog if
+ // no default is set. However, if the main InCallUI is already visible, we do not want to
+ // re-initiate the start-up animation, so we do not need to do anything here.
+ //
+ // It is also possible to go into an intermediate state where the call has been initiated
+ // but Telecom has not yet returned with the details of the call (handle, gateway, etc.).
+ // This pending outgoing state can also launch the call screen.
+ //
+ // This is different from the incoming call sequence because we do not need to shock the
+ // user with a top-level notification. Just show the call UI normally.
+ boolean callCardFragmentVisible =
+ mInCallActivity != null && mInCallActivity.getCallCardFragmentVisible();
+ final boolean mainUiNotVisible = !isShowingInCallUi() || !callCardFragmentVisible;
+ boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible;
+
+ // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the
+ // outgoing call process, so the UI should be brought up to show an error dialog.
+ showCallUi |=
+ (InCallState.PENDING_OUTGOING == mInCallState
+ && InCallState.INCALL == newState
+ && !isShowingInCallUi());
+
+ // Another exception - InCallActivity is in charge of disconnecting a call with no
+ // valid accounts set. Bring the UI up if this is true for the current pending outgoing
+ // call so that:
+ // 1) The call can be disconnected correctly
+ // 2) The UI comes up and correctly displays the error dialog.
+ // TODO: Remove these special case conditions by making InCallPresenter a true state
+ // machine. Telecom should also be the component responsible for disconnecting a call
+ // with no valid accounts.
+ showCallUi |=
+ InCallState.PENDING_OUTGOING == newState
+ && mainUiNotVisible
+ && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall());
+
+ // The only time that we have an instance of mInCallActivity and it isn't started is
+ // when it is being destroyed. In that case, lets avoid bringing up another instance of
+ // the activity. When it is finally destroyed, we double check if we should bring it back
+ // up so we aren't going to lose anything by avoiding a second startup here.
+ boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted();
+ if (activityIsFinishing) {
+ Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState);
+ return mInCallState;
+ }
+
+ // We're about the bring up the in-call UI for outgoing and incoming call. If we still have
+ // dialogs up, we need to clear them out before showing in-call screen. This is necessary
+ // to fix the bug that dialog will show up when data reaches limit even after makeing new
+ // outgoing call after user ignore it by pressing home button.
+ if ((newState == InCallState.INCOMING || newState == InCallState.PENDING_OUTGOING)
+ && !showCallUi
+ && isActivityStarted()) {
+ mInCallActivity.dismissPendingDialogs();
+ }
+
+ if (showCallUi || showAccountPicker) {
+ Log.i(this, "Start in call UI");
+ showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */, false);
+ } else if (startIncomingCallSequence) {
+ Log.i(this, "Start Full Screen in call UI");
+
+ if (!startUi()) {
+ // startUI refused to start the UI. This indicates that it needed to restart the
+ // activity. When it finally restarts, it will call us back, so we do not actually
+ // change the state yet (we return mInCallState instead of newState).
+ return mInCallState;
+ }
+ } else if (newState == InCallState.NO_CALLS) {
+ // The new state is the no calls state. Tear everything down.
+ attemptFinishActivity();
+ attemptCleanup();
+ }
+
+ return newState;
+ }
+
+ /**
+ * Sets the DisconnectCause for a call that was disconnected because it was missing a PhoneAccount
+ * or PhoneAccounts to select from.
+ */
+ private void setDisconnectCauseForMissingAccounts(DialerCall call) {
+
+ Bundle extras = call.getIntentExtras();
+ // Initialize the extras bundle to avoid NPE
+ if (extras == null) {
+ extras = new Bundle();
+ }
+
+ final List<PhoneAccountHandle> phoneAccountHandles =
+ extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
+
+ if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) {
+ String scheme = call.getHandle().getScheme();
+ final String errorMsg =
+ PhoneAccount.SCHEME_TEL.equals(scheme)
+ ? mContext.getString(R.string.callFailed_simError)
+ : mContext.getString(R.string.incall_error_supp_service_unknown);
+ DisconnectCause disconnectCause =
+ new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg);
+ call.setDisconnectCause(disconnectCause);
+ }
+ }
+
+ private boolean startUi() {
+ boolean isCallWaiting =
+ mCallList.getActiveCall() != null && mCallList.getIncomingCall() != null;
+
+ if (isCallWaiting) {
+ showInCall(false, false, false /* isVideoCall */);
+ } else {
+ mStatusBarNotifier.updateNotification(mCallList);
+ }
+ return true;
+ }
+
+ /**
+ * @return {@code true} if the InCallPresenter is ready to be torn down, {@code false} otherwise.
+ * Calling classes should use this as an indication whether to interact with the
+ * InCallPresenter or not.
+ */
+ public boolean isReadyForTearDown() {
+ return mInCallActivity == null && !mServiceConnected && mInCallState == InCallState.NO_CALLS;
+ }
+
+ /**
+ * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all down.
+ */
+ private void attemptCleanup() {
+ if (isReadyForTearDown()) {
+ Log.i(this, "Cleaning up");
+
+ cleanupSurfaces();
+
+ mIsActivityPreviouslyStarted = false;
+ mIsChangingConfigurations = false;
+
+ // blow away stale contact info so that we get fresh data on
+ // the next set of calls
+ if (mContactInfoCache != null) {
+ mContactInfoCache.clearCache();
+ }
+ mContactInfoCache = null;
+
+ if (mProximitySensor != null) {
+ removeListener(mProximitySensor);
+ mProximitySensor.tearDown();
+ }
+ mProximitySensor = null;
+
+ if (mStatusBarNotifier != null) {
+ removeListener(mStatusBarNotifier);
+ }
+ if (mExternalCallNotifier != null && mExternalCallList != null) {
+ mExternalCallList.removeExternalCallListener(mExternalCallNotifier);
+ }
+ mStatusBarNotifier = null;
+
+ if (mCallList != null) {
+ mCallList.removeListener(this);
+ mCallList.removeListener(mSpamCallListListener);
+ }
+ mCallList = null;
+
+ mContext = null;
+ mInCallActivity = null;
+ mManageConferenceActivity = null;
+
+ mListeners.clear();
+ mIncomingCallListeners.clear();
+ mDetailsListeners.clear();
+ mCanAddCallListeners.clear();
+ mOrientationListeners.clear();
+ mInCallEventListeners.clear();
+ mInCallUiListeners.clear();
+
+ Log.d(this, "Finished InCallPresenter.CleanUp");
+ }
+ }
+
+ public void showInCall(boolean showDialpad, boolean newOutgoingCall, boolean isVideoCall) {
+ Log.i(this, "Showing InCallActivity");
+ mContext.startActivity(
+ InCallActivity.getIntent(
+ mContext, showDialpad, newOutgoingCall, isVideoCall, false /* forFullScreen */));
+ }
+
+ public void onServiceBind() {
+ mServiceBound = true;
+ }
+
+ public void onServiceUnbind() {
+ InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null);
+ mServiceBound = false;
+ }
+
+ public boolean isServiceBound() {
+ return mServiceBound;
+ }
+
+ public void maybeStartRevealAnimation(Intent intent) {
+ if (intent == null || mInCallActivity != null) {
+ return;
+ }
+ final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS);
+ if (extras == null) {
+ // Incoming call, just show the in-call UI directly.
+ return;
+ }
+
+ if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) {
+ // Account selection dialog will show up so don't show the animation.
+ return;
+ }
+
+ final PhoneAccountHandle accountHandle =
+ intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT);
+ int videoState =
+ extras.getInt(
+ TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_AUDIO_ONLY);
+
+ InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle);
+
+ final Intent activityIntent =
+ InCallActivity.getIntent(
+ mContext, false, true, VideoUtils.isVideoCall(videoState), false /* forFullScreen */);
+ activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint);
+ mContext.startActivity(activityIntent);
+ }
+
+ /**
+ * Retrieves the current in-call camera manager instance, creating if necessary.
+ *
+ * @return The {@link InCallCameraManager}.
+ */
+ public InCallCameraManager getInCallCameraManager() {
+ synchronized (this) {
+ if (mInCallCameraManager == null) {
+ mInCallCameraManager = new InCallCameraManager(mContext);
+ }
+
+ return mInCallCameraManager;
+ }
+ }
+
+ /**
+ * Notifies listeners of changes in orientation and notify calls of rotation angle change.
+ *
+ * @param orientation The screen orientation of the device (one of: {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_0}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_90}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_180}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_270}).
+ */
+ public void onDeviceOrientationChange(@ScreenOrientation int orientation) {
+ Log.d(this, "onDeviceOrientationChange: orientation= " + orientation);
+
+ if (mCallList != null) {
+ mCallList.notifyCallsOfDeviceRotation(orientation);
+ } else {
+ Log.w(this, "onDeviceOrientationChange: CallList is null.");
+ }
+
+ // Notify listeners of device orientation changed.
+ for (InCallOrientationListener listener : mOrientationListeners) {
+ listener.onDeviceOrientationChanged(orientation);
+ }
+ }
+
+ /**
+ * Configures the in-call UI activity so it can change orientations or not. Enables the
+ * orientation event listener if allowOrientationChange is true, disables it if false.
+ *
+ * @param allowOrientationChange {@code true} if the in-call UI can change between portrait and
+ * landscape. {@code false} if the in-call UI should be locked in portrait.
+ */
+ public void setInCallAllowsOrientationChange(boolean allowOrientationChange) {
+ if (mInCallActivity == null) {
+ Log.e(this, "InCallActivity is null. Can't set requested orientation.");
+ return;
+ }
+ mInCallActivity.setAllowOrientationChange(allowOrientationChange);
+ }
+
+ public void enableScreenTimeout(boolean enable) {
+ Log.v(this, "enableScreenTimeout: value=" + enable);
+ if (mInCallActivity == null) {
+ Log.e(this, "enableScreenTimeout: InCallActivity is null.");
+ return;
+ }
+
+ final Window window = mInCallActivity.getWindow();
+ if (enable) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ } else {
+ window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+ }
+
+ /**
+ * Hides or shows the conference manager fragment.
+ *
+ * @param show {@code true} if the conference manager should be shown, {@code false} if it should
+ * be hidden.
+ */
+ public void showConferenceCallManager(boolean show) {
+ if (mInCallActivity != null) {
+ mInCallActivity.showConferenceFragment(show);
+ }
+ if (!show && mManageConferenceActivity != null) {
+ mManageConferenceActivity.finish();
+ }
+ }
+
+ /**
+ * Determines if the dialpad is visible.
+ *
+ * @return {@code true} if the dialpad is visible, {@code false} otherwise.
+ */
+ public boolean isDialpadVisible() {
+ if (mInCallActivity == null) {
+ return false;
+ }
+ return mInCallActivity.isDialpadVisible();
+ }
+
+ public ThemeColorManager getThemeColorManager() {
+ return mThemeColorManager;
+ }
+
+ /** Called when the foreground call changes. */
+ public void onForegroundCallChanged(DialerCall newForegroundCall) {
+ mThemeColorManager.onForegroundCallChanged(mContext, newForegroundCall);
+ if (mInCallActivity != null) {
+ mInCallActivity.onForegroundCallChanged(newForegroundCall);
+ }
+ }
+
+ public InCallActivity getActivity() {
+ return mInCallActivity;
+ }
+
+ /** Called when the UI begins, and starts the callstate callbacks if necessary. */
+ public void setActivity(InCallActivity inCallActivity) {
+ if (inCallActivity == null) {
+ throw new IllegalArgumentException("registerActivity cannot be called with null");
+ }
+ if (mInCallActivity != null && mInCallActivity != inCallActivity) {
+ Log.w(this, "Setting a second activity before destroying the first.");
+ }
+ updateActivity(inCallActivity);
+ }
+
+ ExternalCallNotifier getExternalCallNotifier() {
+ return mExternalCallNotifier;
+ }
+
+ VideoSurfaceTexture getLocalVideoSurfaceTexture() {
+ if (mLocalVideoSurfaceTexture == null) {
+ mLocalVideoSurfaceTexture = VideoSurfaceBindings.createLocalVideoSurfaceTexture();
+ }
+ return mLocalVideoSurfaceTexture;
+ }
+
+ VideoSurfaceTexture getRemoteVideoSurfaceTexture() {
+ if (mRemoteVideoSurfaceTexture == null) {
+ mRemoteVideoSurfaceTexture = VideoSurfaceBindings.createRemoteVideoSurfaceTexture();
+ }
+ return mRemoteVideoSurfaceTexture;
+ }
+
+ void cleanupSurfaces() {
+ if (mRemoteVideoSurfaceTexture != null) {
+ mRemoteVideoSurfaceTexture.setDoneWithSurface();
+ mRemoteVideoSurfaceTexture = null;
+ }
+ if (mLocalVideoSurfaceTexture != null) {
+ mLocalVideoSurfaceTexture.setDoneWithSurface();
+ mLocalVideoSurfaceTexture = null;
+ }
+ }
+
+ /** All the main states of InCallActivity. */
+ public enum InCallState {
+ // InCall Screen is off and there are no calls
+ NO_CALLS,
+
+ // Incoming-call screen is up
+ INCOMING,
+
+ // In-call experience is showing
+ INCALL,
+
+ // Waiting for user input before placing outgoing call
+ WAITING_FOR_ACCOUNT,
+
+ // UI is starting up but no call has been initiated yet.
+ // The UI is waiting for Telecom to respond.
+ PENDING_OUTGOING,
+
+ // User is dialing out
+ OUTGOING;
+
+ public boolean isIncoming() {
+ return (this == INCOMING);
+ }
+
+ public boolean isConnectingOrConnected() {
+ return (this == INCOMING || this == OUTGOING || this == INCALL);
+ }
+ }
+
+ /** Interface implemented by classes that need to know about the InCall State. */
+ public interface InCallStateListener {
+
+ // TODO: Enhance state to contain the call objects instead of passing CallList
+ void onStateChange(InCallState oldState, InCallState newState, CallList callList);
+ }
+
+ public interface IncomingCallListener {
+
+ void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call);
+ }
+
+ public interface CanAddCallListener {
+
+ void onCanAddCallChanged(boolean canAddCall);
+ }
+
+ public interface InCallDetailsListener {
+
+ void onDetailsChanged(DialerCall call, android.telecom.Call.Details details);
+ }
+
+ public interface InCallOrientationListener {
+
+ void onDeviceOrientationChanged(@ScreenOrientation int orientation);
+ }
+
+ /**
+ * Interface implemented by classes that need to know about events which occur within the In-Call
+ * UI. Used as a means of communicating between fragments that make up the UI.
+ */
+ public interface InCallEventListener {
+
+ void onFullscreenModeChanged(boolean isFullscreenMode);
+ }
+
+ public interface InCallUiListener {
+
+ void onUiShowing(boolean showing);
+ }
+}
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
new file mode 100644
index 000000000..33e8393ae
--- /dev/null
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -0,0 +1,99 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.telecom.Call;
+import android.telecom.CallAudioState;
+import android.telecom.InCallService;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.ExternalCallList;
+import com.android.incallui.call.TelecomAdapter;
+
+/**
+ * Used to receive updates about calls from the Telecom component. This service is bound to Telecom
+ * while there exist calls which potentially require UI. This includes ringing (incoming), dialing
+ * (outgoing), and active calls. When the last call is disconnected, Telecom will unbind to the
+ * service triggering InCallActivity (via CallList) to finish soon after.
+ */
+public class InCallServiceImpl extends InCallService {
+
+ @Override
+ public void onCallAudioStateChanged(CallAudioState audioState) {
+ AudioModeProvider.getInstance().onAudioStateChanged(audioState);
+ }
+
+ @Override
+ public void onBringToForeground(boolean showDialpad) {
+ InCallPresenter.getInstance().onBringToForeground(showDialpad);
+ }
+
+ @Override
+ public void onCallAdded(Call call) {
+ InCallPresenter.getInstance().onCallAdded(call);
+ }
+
+ @Override
+ public void onCallRemoved(Call call) {
+ InCallPresenter.getInstance().onCallRemoved(call);
+ }
+
+ @Override
+ public void onCanAddCallChanged(boolean canAddCall) {
+ InCallPresenter.getInstance().onCanAddCallChanged(canAddCall);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ final Context context = getApplicationContext();
+ final ContactInfoCache contactInfoCache = ContactInfoCache.getInstance(context);
+ InCallPresenter.getInstance()
+ .setUp(
+ getApplicationContext(),
+ CallList.getInstance(),
+ new ExternalCallList(),
+ new StatusBarNotifier(context, contactInfoCache),
+ new ExternalCallNotifier(context, contactInfoCache),
+ contactInfoCache,
+ new ProximitySensor(
+ context, AudioModeProvider.getInstance(), new AccelerometerListener(context)));
+ InCallPresenter.getInstance().onServiceBind();
+ InCallPresenter.getInstance().maybeStartRevealAnimation(intent);
+ TelecomAdapter.getInstance().setInCallService(this);
+
+ return super.onBind(intent);
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ super.onUnbind(intent);
+
+ InCallPresenter.getInstance().onServiceUnbind();
+ tearDown();
+
+ return false;
+ }
+
+ private void tearDown() {
+ Log.v(this, "tearDown");
+ // Tear down the InCall system
+ TelecomAdapter.getInstance().clearInCallService();
+ InCallPresenter.getInstance().tearDown();
+ }
+}
diff --git a/java/com/android/incallui/InCallUIMaterialColorMapUtils.java b/java/com/android/incallui/InCallUIMaterialColorMapUtils.java
new file mode 100644
index 000000000..7b06a5e39
--- /dev/null
+++ b/java/com/android/incallui/InCallUIMaterialColorMapUtils.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.incallui;
+
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.telecom.PhoneAccount;
+import com.android.contacts.common.util.MaterialColorMapUtils;
+
+public class InCallUIMaterialColorMapUtils extends MaterialColorMapUtils {
+
+ private final TypedArray mPrimaryColors;
+ private final TypedArray mSecondaryColors;
+ private final Resources mResources;
+
+ public InCallUIMaterialColorMapUtils(Resources resources) {
+ super(resources);
+ mPrimaryColors = resources.obtainTypedArray(R.array.background_colors);
+ mSecondaryColors = resources.obtainTypedArray(R.array.background_colors_dark);
+ mResources = resources;
+ }
+
+ /**
+ * {@link Resources#getColor(int) used for compatibility
+ */
+ @SuppressWarnings("deprecation")
+ public static MaterialPalette getDefaultPrimaryAndSecondaryColors(Resources resources) {
+ final int primaryColor = resources.getColor(R.color.dialer_theme_color);
+ final int secondaryColor = resources.getColor(R.color.dialer_theme_color_dark);
+ return new MaterialPalette(primaryColor, secondaryColor);
+ }
+
+ /**
+ * Currently the InCallUI color will only vary by SIM color which is a list of colors defined in
+ * the background_colors array, so first search the list for the matching color and fall back to
+ * the closest matching color if an exact match does not exist.
+ */
+ @Override
+ public MaterialPalette calculatePrimaryAndSecondaryColor(int color) {
+ if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
+ return getDefaultPrimaryAndSecondaryColors(mResources);
+ }
+
+ for (int i = 0; i < mPrimaryColors.length(); i++) {
+ if (mPrimaryColors.getColor(i, 0) == color) {
+ return new MaterialPalette(mPrimaryColors.getColor(i, 0), mSecondaryColors.getColor(i, 0));
+ }
+ }
+
+ // The color isn't in the list, so use the superclass to find an approximate color.
+ return super.calculatePrimaryAndSecondaryColor(color);
+ }
+}
diff --git a/java/com/android/incallui/Log.java b/java/com/android/incallui/Log.java
new file mode 100644
index 000000000..c63eccbd4
--- /dev/null
+++ b/java/com/android/incallui/Log.java
@@ -0,0 +1,145 @@
+/*
+ * 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.incallui;
+
+import android.net.Uri;
+import android.telecom.PhoneAccount;
+import android.telephony.PhoneNumberUtils;
+import com.android.dialer.common.LogUtil;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Manages logging for the entire class. */
+public class Log {
+
+ public static void d(String tag, String msg) {
+ LogUtil.d(tag, msg);
+ }
+
+ public static void d(Object obj, String msg) {
+ LogUtil.d(getPrefix(obj), msg);
+ }
+
+ public static void d(Object obj, String str1, Object str2) {
+ LogUtil.d(getPrefix(obj), str1 + str2);
+ }
+
+ public static void v(Object obj, String msg) {
+ LogUtil.v(getPrefix(obj), msg);
+ }
+
+ public static void v(Object obj, String str1, Object str2) {
+ LogUtil.v(getPrefix(obj), str1 + str2);
+ }
+
+ public static void e(String tag, String msg, Exception e) {
+ LogUtil.e(tag, msg, e);
+ }
+
+ public static void e(String tag, String msg) {
+ LogUtil.e(tag, msg);
+ }
+
+ public static void e(Object obj, String msg, Exception e) {
+ LogUtil.e(getPrefix(obj), msg, e);
+ }
+
+ public static void e(Object obj, String msg) {
+ LogUtil.e(getPrefix(obj), msg);
+ }
+
+ public static void i(String tag, String msg) {
+ LogUtil.i(tag, msg);
+ }
+
+ public static void i(Object obj, String msg) {
+ LogUtil.i(getPrefix(obj), msg);
+ }
+
+ public static void w(Object obj, String msg) {
+ LogUtil.w(getPrefix(obj), msg);
+ }
+
+ public static String piiHandle(Object pii) {
+ if (pii == null || LogUtil.isVerboseEnabled()) {
+ return String.valueOf(pii);
+ }
+
+ if (pii instanceof Uri) {
+ Uri uri = (Uri) pii;
+
+ // All Uri's which are not "tel" go through normal pii() method.
+ if (!PhoneAccount.SCHEME_TEL.equals(uri.getScheme())) {
+ return pii(pii);
+ } else {
+ pii = uri.getSchemeSpecificPart();
+ }
+ }
+
+ String originalString = String.valueOf(pii);
+ StringBuilder stringBuilder = new StringBuilder(originalString.length());
+ for (char c : originalString.toCharArray()) {
+ if (PhoneNumberUtils.isDialable(c)) {
+ stringBuilder.append('*');
+ } else {
+ stringBuilder.append(c);
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ /**
+ * Redact personally identifiable information for production users. If we are running in verbose
+ * mode, return the original string, otherwise return a SHA-1 hash of the input string.
+ */
+ public static String pii(Object pii) {
+ if (pii == null || LogUtil.isVerboseEnabled()) {
+ return String.valueOf(pii);
+ }
+ return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
+ }
+
+ private static String secureHash(byte[] input) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+ messageDigest.update(input);
+ byte[] result = messageDigest.digest();
+ return encodeHex(result);
+ }
+
+ private static String encodeHex(byte[] bytes) {
+ StringBuffer hex = new StringBuffer(bytes.length * 2);
+
+ for (int i = 0; i < bytes.length; i++) {
+ int byteIntValue = bytes[i] & 0xff;
+ if (byteIntValue < 0x10) {
+ hex.append("0");
+ }
+ hex.append(Integer.toString(byteIntValue, 16));
+ }
+
+ return hex.toString();
+ }
+
+ private static String getPrefix(Object obj) {
+ return (obj == null ? "" : (obj.getClass().getSimpleName()));
+ }
+}
diff --git a/java/com/android/incallui/ManageConferenceActivity.java b/java/com/android/incallui/ManageConferenceActivity.java
new file mode 100644
index 000000000..6584e4f67
--- /dev/null
+++ b/java/com/android/incallui/ManageConferenceActivity.java
@@ -0,0 +1,86 @@
+/*
+ * 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.incallui;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+
+/** Shows the {@link ConferenceManagerFragment} */
+public class ManageConferenceActivity extends AppCompatActivity {
+
+ private boolean isVisible;
+
+ public boolean isVisible() {
+ return isVisible;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ InCallPresenter.getInstance().setManageConferenceActivity(this);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setContentView(R.layout.activity_manage_conference);
+ Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.manageConferencePanel);
+ if (fragment == null) {
+ fragment = new ConferenceManagerFragment();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.manageConferencePanel, fragment)
+ .commit();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (isFinishing()) {
+ InCallPresenter.getInstance().setManageConferenceActivity(null);
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ InCallPresenter.getInstance().bringToForeground(false);
+ finish();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ isVisible = true;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ isVisible = false;
+ }
+}
diff --git a/java/com/android/incallui/NotificationBroadcastReceiver.java b/java/com/android/incallui/NotificationBroadcastReceiver.java
new file mode 100644
index 000000000..5c5d255cc
--- /dev/null
+++ b/java/com/android/incallui/NotificationBroadcastReceiver.java
@@ -0,0 +1,165 @@
+/*
+ * 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.incallui;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.RequiresApi;
+import android.telecom.VideoProfile;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.VideoUtils;
+
+/**
+ * Accepts broadcast Intents which will be prepared by {@link StatusBarNotifier} and thus sent from
+ * the notification manager. This should be visible from outside, but shouldn't be exported.
+ */
+public class NotificationBroadcastReceiver extends BroadcastReceiver {
+
+ /**
+ * Intent Action used for hanging up the current call from Notification bar. This will choose
+ * first ringing call, first active call, or first background call (typically in STATE_HOLDING
+ * state).
+ */
+ public static final String ACTION_DECLINE_INCOMING_CALL =
+ "com.android.incallui.ACTION_DECLINE_INCOMING_CALL";
+
+ public static final String ACTION_HANG_UP_ONGOING_CALL =
+ "com.android.incallui.ACTION_HANG_UP_ONGOING_CALL";
+ public static final String ACTION_ANSWER_VIDEO_INCOMING_CALL =
+ "com.android.incallui.ACTION_ANSWER_VIDEO_INCOMING_CALL";
+ public static final String ACTION_ANSWER_VOICE_INCOMING_CALL =
+ "com.android.incallui.ACTION_ANSWER_VOICE_INCOMING_CALL";
+ public static final String ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST =
+ "com.android.incallui.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST";
+ public static final String ACTION_DECLINE_VIDEO_UPGRADE_REQUEST =
+ "com.android.incallui.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST";
+
+ @RequiresApi(VERSION_CODES.N_MR1)
+ public static final String ACTION_PULL_EXTERNAL_CALL =
+ "com.android.incallui.ACTION_PULL_EXTERNAL_CALL";
+
+ public static final String EXTRA_NOTIFICATION_ID =
+ "com.android.incallui.extra.EXTRA_NOTIFICATION_ID";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ final String action = intent.getAction();
+ LogUtil.i("NotificationBroadcastReceiver.onReceive", "Broadcast from Notification: " + action);
+
+ // TODO: Commands of this nature should exist in the CallList.
+ if (action.equals(ACTION_ANSWER_VIDEO_INCOMING_CALL)) {
+ answerIncomingCall(context, VideoProfile.STATE_BIDIRECTIONAL);
+ } else if (action.equals(ACTION_ANSWER_VOICE_INCOMING_CALL)) {
+ answerIncomingCall(context, VideoProfile.STATE_AUDIO_ONLY);
+ } else if (action.equals(ACTION_DECLINE_INCOMING_CALL)) {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_NOTIFICATION);
+ declineIncomingCall(context);
+ } else if (action.equals(ACTION_HANG_UP_ONGOING_CALL)) {
+ hangUpOngoingCall(context);
+ } else if (action.equals(ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST)) {
+ acceptUpgradeRequest(context);
+ } else if (action.equals(ACTION_DECLINE_VIDEO_UPGRADE_REQUEST)) {
+ declineUpgradeRequest(context);
+ } else if (action.equals(ACTION_PULL_EXTERNAL_CALL)) {
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
+ InCallPresenter.getInstance().getExternalCallNotifier().pullExternalCall(notificationId);
+ }
+ }
+
+ private void acceptUpgradeRequest(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.acceptUpgradeRequest", "call list is empty");
+ } else {
+ DialerCall call = callList.getVideoUpgradeRequestCall();
+ if (call != null) {
+ call.acceptUpgradeRequest(call.getRequestedVideoState());
+ }
+ }
+ }
+
+ private void declineUpgradeRequest(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.declineUpgradeRequest", "call list is empty");
+ } else {
+ DialerCall call = callList.getVideoUpgradeRequestCall();
+ if (call != null) {
+ call.declineUpgradeRequest();
+ }
+ }
+ }
+
+ private void hangUpOngoingCall(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.hangUpOngoingCall", "call list is empty");
+ } else {
+ DialerCall call = callList.getOutgoingCall();
+ if (call == null) {
+ call = callList.getActiveOrBackgroundCall();
+ }
+ LogUtil.i(
+ "NotificationBroadcastReceiver.hangUpOngoingCall", "disconnecting call, call: " + call);
+ if (call != null) {
+ call.disconnect();
+ }
+ }
+ }
+
+ private void answerIncomingCall(Context context, int videoState) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.answerIncomingCall", "call list is empty");
+ } else {
+ DialerCall call = callList.getIncomingCall();
+ if (call != null) {
+ call.answer(videoState);
+ InCallPresenter.getInstance()
+ .showInCall(
+ false /* showDialpad */,
+ false /* newOutgoingCall */,
+ VideoUtils.isVideoCall(videoState));
+ }
+ }
+ }
+
+ private void declineIncomingCall(Context context) {
+ CallList callList = InCallPresenter.getInstance().getCallList();
+ if (callList == null) {
+ StatusBarNotifier.clearAllCallNotifications(context);
+ LogUtil.e("NotificationBroadcastReceiver.declineIncomingCall", "call list is empty");
+ } else {
+ DialerCall call = callList.getIncomingCall();
+ if (call != null) {
+ call.reject(false /* rejectWithMessage */, null);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/PostCharDialogFragment.java b/java/com/android/incallui/PostCharDialogFragment.java
new file mode 100644
index 000000000..a852f7683
--- /dev/null
+++ b/java/com/android/incallui/PostCharDialogFragment.java
@@ -0,0 +1,96 @@
+/*
+ * 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.incallui;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+import com.android.incallui.call.TelecomAdapter;
+
+/**
+ * Pop up an alert dialog with OK and Cancel buttons to allow user to Accept or Reject the WAIT
+ * inserted as part of the Dial string.
+ */
+public class PostCharDialogFragment extends DialogFragment {
+
+ private static final String STATE_CALL_ID = "CALL_ID";
+ private static final String STATE_POST_CHARS = "POST_CHARS";
+
+ private String mCallId;
+ private String mPostDialStr;
+
+ public PostCharDialogFragment() {}
+
+ public PostCharDialogFragment(String callId, String postDialStr) {
+ mCallId = callId;
+ mPostDialStr = postDialStr;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+
+ if (mPostDialStr == null && savedInstanceState != null) {
+ mCallId = savedInstanceState.getString(STATE_CALL_ID);
+ mPostDialStr = savedInstanceState.getString(STATE_POST_CHARS);
+ }
+
+ final StringBuilder buf = new StringBuilder();
+ buf.append(getResources().getText(R.string.wait_prompt_str));
+ buf.append(mPostDialStr);
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(buf.toString());
+
+ builder.setPositiveButton(
+ R.string.pause_prompt_yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ TelecomAdapter.getInstance().postDialContinue(mCallId, true);
+ }
+ });
+ builder.setNegativeButton(
+ R.string.pause_prompt_no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ dialog.cancel();
+ }
+ });
+
+ final AlertDialog dialog = builder.create();
+ return dialog;
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onCancel(dialog);
+
+ TelecomAdapter.getInstance().postDialContinue(mCallId, false);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putString(STATE_CALL_ID, mCallId);
+ outState.putString(STATE_POST_CHARS, mPostDialStr);
+ }
+}
diff --git a/java/com/android/incallui/ProximitySensor.java b/java/com/android/incallui/ProximitySensor.java
new file mode 100644
index 000000000..91220627c
--- /dev/null
+++ b/java/com/android/incallui/ProximitySensor.java
@@ -0,0 +1,292 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.PowerManager;
+import android.support.annotation.NonNull;
+import android.telecom.CallAudioState;
+import android.view.Display;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.AudioModeProvider.AudioModeListener;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.VideoUtils;
+
+/**
+ * Class manages the proximity sensor for the in-call UI. We enable the proximity sensor while the
+ * user in a phone call. The Proximity sensor turns off the touchscreen and display when the user is
+ * close to the screen to prevent user's cheek from causing touch events. The class requires special
+ * knowledge of the activity and device state to know when the proximity sensor should be enabled
+ * and disabled. Most of that state is fed into this class through public methods.
+ */
+public class ProximitySensor
+ implements AccelerometerListener.OrientationListener, InCallStateListener, AudioModeListener {
+
+ private static final String TAG = ProximitySensor.class.getSimpleName();
+
+ private final PowerManager mPowerManager;
+ private final PowerManager.WakeLock mProximityWakeLock;
+ private final AudioModeProvider mAudioModeProvider;
+ private final AccelerometerListener mAccelerometerListener;
+ private final ProximityDisplayListener mDisplayListener;
+ private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
+ private boolean mUiShowing = false;
+ private boolean mIsPhoneOffhook = false;
+ private boolean mDialpadVisible;
+ private boolean mIsAttemptingVideoCall;
+ private boolean mIsVideoCall;
+
+ public ProximitySensor(
+ @NonNull Context context,
+ @NonNull AudioModeProvider audioModeProvider,
+ @NonNull AccelerometerListener accelerometerListener) {
+ mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
+ if (mPowerManager.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock =
+ mPowerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+ } else {
+ LogUtil.i("ProximitySensor.constructor", "Device does not support proximity wake lock.");
+ mProximityWakeLock = null;
+ }
+ mAccelerometerListener = accelerometerListener;
+ mAccelerometerListener.setListener(this);
+
+ mDisplayListener =
+ new ProximityDisplayListener(
+ (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE));
+ mDisplayListener.register();
+
+ mAudioModeProvider = audioModeProvider;
+ mAudioModeProvider.addListener(this);
+ }
+
+ public void tearDown() {
+ mAudioModeProvider.removeListener(this);
+
+ mAccelerometerListener.enable(false);
+ mDisplayListener.unregister();
+
+ turnOffProximitySensor(true);
+ }
+
+ /** Called to identify when the device is laid down flat. */
+ @Override
+ public void orientationChanged(int orientation) {
+ mOrientation = orientation;
+ updateProximitySensorMode();
+ }
+
+ /** Called to keep track of the overall UI state. */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ // We ignore incoming state because we do not want to enable proximity
+ // sensor during incoming call screen. We check hasLiveCall() because a disconnected call
+ // can also put the in-call screen in the INCALL state.
+ boolean hasOngoingCall = InCallState.INCALL == newState && callList.hasLiveCall();
+ boolean isOffhook = (InCallState.OUTGOING == newState) || hasOngoingCall;
+
+ boolean isVideoCall = VideoUtils.isVideoCall(callList.getActiveCall());
+
+ if (isOffhook != mIsPhoneOffhook || mIsVideoCall != isVideoCall) {
+ mIsPhoneOffhook = isOffhook;
+ mIsVideoCall = isVideoCall;
+
+ mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
+ mAccelerometerListener.enable(mIsPhoneOffhook);
+
+ updateProximitySensorMode();
+ }
+ }
+
+ @Override
+ public void onAudioStateChanged(CallAudioState audioState) {
+ updateProximitySensorMode();
+ }
+
+ public void onDialpadVisible(boolean visible) {
+ mDialpadVisible = visible;
+ updateProximitySensorMode();
+ }
+
+ public void setIsAttemptingVideoCall(boolean isAttemptingVideoCall) {
+ LogUtil.i(
+ "ProximitySensor.setIsAttemptingVideoCall",
+ "isAttemptingVideoCall: %b",
+ isAttemptingVideoCall);
+ mIsAttemptingVideoCall = isAttemptingVideoCall;
+ updateProximitySensorMode();
+ }
+ /** Used to save when the UI goes in and out of the foreground. */
+ public void onInCallShowing(boolean showing) {
+ if (showing) {
+ mUiShowing = true;
+
+ // We only consider the UI not showing for instances where another app took the foreground.
+ // If we stopped showing because the screen is off, we still consider that showing.
+ } else if (mPowerManager.isScreenOn()) {
+ mUiShowing = false;
+ }
+ updateProximitySensorMode();
+ }
+
+ void onDisplayStateChanged(boolean isDisplayOn) {
+ LogUtil.i("ProximitySensor.onDisplayStateChanged", "isDisplayOn: %b", isDisplayOn);
+ mAccelerometerListener.enable(isDisplayOn);
+ }
+
+ /**
+ * TODO: There is no way to determine if a screen is off due to proximity or if it is legitimately
+ * off, but if ever we can do that in the future, it would be useful here. Until then, this
+ * function will simply return true of the screen is off. TODO: Investigate whether this can be
+ * replaced with the ProximityDisplayListener.
+ */
+ public boolean isScreenReallyOff() {
+ return !mPowerManager.isScreenOn();
+ }
+
+ private void turnOnProximitySensor() {
+ if (mProximityWakeLock != null) {
+ if (!mProximityWakeLock.isHeld()) {
+ LogUtil.i("ProximitySensor.turnOnProximitySensor", "acquiring wake lock");
+ mProximityWakeLock.acquire();
+ } else {
+ LogUtil.i("ProximitySensor.turnOnProximitySensor", "wake lock already acquired");
+ }
+ }
+ }
+
+ private void turnOffProximitySensor(boolean screenOnImmediately) {
+ if (mProximityWakeLock != null) {
+ if (mProximityWakeLock.isHeld()) {
+ LogUtil.i("ProximitySensor.turnOffProximitySensor", "releasing wake lock");
+ int flags = (screenOnImmediately ? 0 : PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
+ mProximityWakeLock.release(flags);
+ } else {
+ LogUtil.i("ProximitySensor.turnOffProximitySensor", "wake lock already released");
+ }
+ }
+ }
+
+ /**
+ * Updates the wake lock used to control proximity sensor behavior, based on the current state of
+ * the phone.
+ *
+ * <p>On devices that have a proximity sensor, to avoid false touches during a call, we hold a
+ * PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock whenever the phone is off hook. (When held, that wake
+ * lock causes the screen to turn off automatically when the sensor detects an object close to the
+ * screen.)
+ *
+ * <p>This method is a no-op for devices that don't have a proximity sensor.
+ *
+ * <p>Proximity wake lock will be released if any of the following conditions are true: the audio
+ * is routed through bluetooth, a wired headset, or the speaker; the user requested, received a
+ * request for, or is in a video call; or the phone is horizontal while in a call.
+ */
+ private synchronized void updateProximitySensorMode() {
+ final int audioRoute = mAudioModeProvider.getAudioState().getRoute();
+
+ boolean screenOnImmediately =
+ (CallAudioState.ROUTE_WIRED_HEADSET == audioRoute
+ || CallAudioState.ROUTE_SPEAKER == audioRoute
+ || CallAudioState.ROUTE_BLUETOOTH == audioRoute
+ || mIsAttemptingVideoCall
+ || mIsVideoCall);
+
+ // We do not keep the screen off when the user is outside in-call screen and we are
+ // horizontal, but we do not force it on when we become horizontal until the
+ // proximity sensor goes negative.
+ final boolean horizontal = (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL);
+ screenOnImmediately |= !mUiShowing && horizontal;
+
+ // We do not keep the screen off when dialpad is visible, we are horizontal, and
+ // the in-call screen is being shown.
+ // At that moment we're pretty sure users want to use it, instead of letting the
+ // proximity sensor turn off the screen by their hands.
+ screenOnImmediately |= mDialpadVisible && horizontal;
+
+ LogUtil.i(
+ "ProximitySensor.updateProximitySensorMode",
+ "screenOnImmediately: %b, dialPadVisible: %b, "
+ + "offHook: %b, horizontal: %b, uiShowing: %b, audioRoute: %s",
+ screenOnImmediately,
+ mDialpadVisible,
+ mIsPhoneOffhook,
+ mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL,
+ mUiShowing,
+ CallAudioState.audioRouteToString(audioRoute));
+
+ if (mIsPhoneOffhook && !screenOnImmediately) {
+ LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning on proximity sensor");
+ // Phone is in use! Arrange for the screen to turn off
+ // automatically when the sensor detects a close object.
+ turnOnProximitySensor();
+ } else {
+ LogUtil.v("ProximitySensor.updateProximitySensorMode", "turning off proximity sensor");
+ // Phone is either idle, or ringing. We don't want any special proximity sensor
+ // behavior in either case.
+ turnOffProximitySensor(screenOnImmediately);
+ }
+ }
+
+ /**
+ * Implementation of a {@link DisplayListener} that maintains a binary state: Screen on vs screen
+ * off. Used by the proximity sensor manager to decide whether or not it needs to listen to
+ * accelerometer events.
+ */
+ public class ProximityDisplayListener implements DisplayListener {
+
+ private DisplayManager mDisplayManager;
+ private boolean mIsDisplayOn = true;
+
+ ProximityDisplayListener(DisplayManager displayManager) {
+ mDisplayManager = displayManager;
+ }
+
+ void register() {
+ mDisplayManager.registerDisplayListener(this, null);
+ }
+
+ void unregister() {
+ mDisplayManager.unregisterDisplayListener(this);
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ final Display display = mDisplayManager.getDisplay(displayId);
+
+ final boolean isDisplayOn = display.getState() != Display.STATE_OFF;
+ // For call purposes, we assume that as long as the screen is not truly off, it is
+ // considered on, even if it is in an unknown or low power idle state.
+ if (isDisplayOn != mIsDisplayOn) {
+ mIsDisplayOn = isDisplayOn;
+ onDisplayStateChanged(mIsDisplayOn);
+ }
+ }
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+ }
+}
diff --git a/java/com/android/incallui/StatusBarNotifier.java b/java/com/android/incallui/StatusBarNotifier.java
new file mode 100644
index 000000000..c7226753f
--- /dev/null
+++ b/java/com/android/incallui/StatusBarNotifier.java
@@ -0,0 +1,842 @@
+/*
+ * 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.incallui;
+
+import static com.android.contacts.common.compat.CallCompat.Details.PROPERTY_ENTERPRISE_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VIDEO_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_ANSWER_VOICE_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_INCOMING_CALL;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST;
+import static com.android.incallui.NotificationBroadcastReceiver.ACTION_HANG_UP_ONGOING_CALL;
+
+import android.app.ActivityManager;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.ColorRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.Call.Details;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.text.BidiFormatter;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import android.text.style.ForegroundColorSpan;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.ContactsUtils.UserType;
+import com.android.contacts.common.preference.ContactsPreferences;
+import com.android.contacts.common.util.BitmapUtil;
+import com.android.contacts.common.util.ContactDisplayUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.util.DrawableConverter;
+import com.android.incallui.ContactInfoCache.ContactCacheEntry;
+import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.async.PausableExecutorImpl;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCallListener;
+import com.android.incallui.ringtone.DialerRingtoneManager;
+import com.android.incallui.ringtone.InCallTonePlayer;
+import com.android.incallui.ringtone.ToneGeneratorFactory;
+import java.util.Objects;
+
+/** This class adds Notifications to the status bar for the in-call experience. */
+public class StatusBarNotifier implements InCallPresenter.InCallStateListener {
+
+ // Notification types
+ // Indicates that no notification is currently showing.
+ private static final int NOTIFICATION_NONE = 0;
+ // Notification for an active call. This is non-interruptive, but cannot be dismissed.
+ private static final int NOTIFICATION_IN_CALL = 1;
+ // Notification for incoming calls. This is interruptive and will show up as a HUN.
+ private static final int NOTIFICATION_INCOMING_CALL = 2;
+
+ private static final int PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN = 0;
+ private static final int PENDING_INTENT_REQUEST_CODE_FULL_SCREEN = 1;
+
+ private static final long[] VIBRATE_PATTERN = new long[] {0, 1000, 1000};
+
+ private final Context mContext;
+ private final ContactInfoCache mContactInfoCache;
+ private final NotificationManager mNotificationManager;
+ private final DialerRingtoneManager mDialerRingtoneManager;
+ @Nullable private ContactsPreferences mContactsPreferences;
+ private int mCurrentNotification = NOTIFICATION_NONE;
+ private int mCallState = DialerCall.State.INVALID;
+ private int mSavedIcon = 0;
+ private String mSavedContent = null;
+ private Bitmap mSavedLargeIcon;
+ private String mSavedContentTitle;
+ private Uri mRingtone;
+ private StatusBarCallListener mStatusBarCallListener;
+
+ public StatusBarNotifier(@NonNull Context context, @NonNull ContactInfoCache contactInfoCache) {
+ Objects.requireNonNull(context);
+ mContext = context;
+ mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext);
+ mContactInfoCache = contactInfoCache;
+ mNotificationManager = context.getSystemService(NotificationManager.class);
+ mDialerRingtoneManager =
+ new DialerRingtoneManager(
+ new InCallTonePlayer(new ToneGeneratorFactory(), new PausableExecutorImpl()),
+ CallList.getInstance());
+ mCurrentNotification = NOTIFICATION_NONE;
+ }
+
+ /**
+ * Should only be called from a irrecoverable state where it is necessary to dismiss all
+ * notifications.
+ */
+ static void clearAllCallNotifications(Context backupContext) {
+ Log.i(
+ StatusBarNotifier.class.getSimpleName(),
+ "Something terrible happened. Clear all InCall notifications");
+
+ NotificationManager notificationManager =
+ backupContext.getSystemService(NotificationManager.class);
+ notificationManager.cancel(NOTIFICATION_IN_CALL);
+ notificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ }
+
+ private static int getWorkStringFromPersonalString(int resId) {
+ if (resId == R.string.notification_ongoing_call) {
+ return R.string.notification_ongoing_work_call;
+ } else if (resId == R.string.notification_ongoing_call_wifi) {
+ return R.string.notification_ongoing_work_call_wifi;
+ } else if (resId == R.string.notification_incoming_call_wifi) {
+ return R.string.notification_incoming_work_call_wifi;
+ } else if (resId == R.string.notification_incoming_call) {
+ return R.string.notification_incoming_work_call;
+ } else {
+ return resId;
+ }
+ }
+
+ /**
+ * Returns PendingIntent for answering a phone call. This will typically be used from Notification
+ * context.
+ */
+ private static PendingIntent createNotificationPendingIntent(Context context, String action) {
+ final Intent intent = new Intent(action, null, context, NotificationBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /** Creates notifications according to the state we receive from {@link InCallPresenter}. */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ Log.d(this, "onStateChange");
+ updateNotification(callList);
+ }
+
+ /**
+ * Updates the phone app's status bar notification *and* launches the incoming call UI in response
+ * to a new incoming call.
+ *
+ * <p>If an incoming call is ringing (or call-waiting), the notification will also include a
+ * "fullScreenIntent" that will cause the InCallScreen to be launched, unless the current
+ * foreground activity is marked as "immersive".
+ *
+ * <p>(This is the mechanism that actually brings up the incoming call UI when we receive a "new
+ * ringing connection" event from the telephony layer.)
+ *
+ * <p>Also note that this method is safe to call even if the phone isn't actually ringing (or,
+ * more likely, if an incoming call *was* ringing briefly but then disconnected). In that case,
+ * we'll simply update or cancel the in-call notification based on the current phone state.
+ *
+ * @see #updateInCallNotification(CallList)
+ */
+ public void updateNotification(CallList callList) {
+ updateInCallNotification(callList);
+ }
+
+ /**
+ * Take down the in-call notification.
+ *
+ * @see #updateInCallNotification(CallList)
+ */
+ private void cancelNotification() {
+ if (mStatusBarCallListener != null) {
+ setStatusBarCallListener(null);
+ }
+ if (mCurrentNotification != NOTIFICATION_NONE) {
+ Log.d(this, "cancelInCall()...");
+ mNotificationManager.cancel(mCurrentNotification);
+ }
+ mCurrentNotification = NOTIFICATION_NONE;
+ }
+
+ /**
+ * Helper method for updateInCallNotification() and updateNotification(): Update the phone app's
+ * status bar notification based on the current telephony state, or cancels the notification if
+ * the phone is totally idle.
+ */
+ private void updateInCallNotification(CallList callList) {
+ Log.d(this, "updateInCallNotification...");
+
+ final DialerCall call = getCallToShow(callList);
+
+ if (call != null) {
+ showNotification(callList, call);
+ } else {
+ cancelNotification();
+ }
+ }
+
+ private void showNotification(final CallList callList, final DialerCall call) {
+ final boolean isIncoming =
+ (call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING);
+ setStatusBarCallListener(new StatusBarCallListener(call));
+
+ // we make a call to the contact info cache to query for supplemental data to what the
+ // call provides. This includes the contact name and photo.
+ // This callback will always get called immediately and synchronously with whatever data
+ // it has available, and may make a subsequent call later (same thread) if it had to
+ // call into the contacts provider for more data.
+ mContactInfoCache.findInfo(
+ call,
+ isIncoming,
+ new ContactInfoCacheCallback() {
+ @Override
+ public void onContactInfoComplete(String callId, ContactCacheEntry entry) {
+ DialerCall call = callList.getCallById(callId);
+ if (call != null) {
+ call.getLogState().contactLookupResult = entry.contactLookupResult;
+ buildAndSendNotification(callList, call, entry);
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(String callId, ContactCacheEntry entry) {
+ DialerCall call = callList.getCallById(callId);
+ if (call != null) {
+ buildAndSendNotification(callList, call, entry);
+ }
+ }
+ });
+ }
+
+ /** Sets up the main Ui for the notification */
+ private void buildAndSendNotification(
+ CallList callList, DialerCall originalCall, ContactCacheEntry contactInfo) {
+ // This can get called to update an existing notification after contact information has come
+ // back. However, it can happen much later. Before we continue, we need to make sure that
+ // the call being passed in is still the one we want to show in the notification.
+ final DialerCall call = getCallToShow(callList);
+ if (call == null || !call.getId().equals(originalCall.getId())) {
+ return;
+ }
+
+ final int callState = call.getState();
+
+ // Check if data has changed; if nothing is different, don't issue another notification.
+ final int iconResId = getIconToDisplay(call);
+ Bitmap largeIcon = getLargeIconToDisplay(contactInfo, call);
+ final String content = getContentString(call, contactInfo.userType);
+ final String contentTitle = getContentTitle(contactInfo, call);
+
+ final boolean isVideoUpgradeRequest =
+ call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ final int notificationType;
+ if (callState == DialerCall.State.INCOMING
+ || callState == DialerCall.State.CALL_WAITING
+ || isVideoUpgradeRequest) {
+ notificationType = NOTIFICATION_INCOMING_CALL;
+ } else {
+ notificationType = NOTIFICATION_IN_CALL;
+ }
+
+ if (!checkForChangeAndSaveData(
+ iconResId,
+ content,
+ largeIcon,
+ contentTitle,
+ callState,
+ notificationType,
+ contactInfo.contactRingtoneUri)) {
+ return;
+ }
+
+ if (largeIcon != null) {
+ largeIcon = getRoundedIcon(largeIcon);
+ }
+
+ // This builder is used for the notification shown when the device is locked and the user
+ // has set their notification settings to 'hide sensitive content'
+ // {@see Notification.Builder#setPublicVersion}.
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder
+ .setSmallIcon(iconResId)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ // Hide work call state for the lock screen notification
+ .setContentTitle(getContentString(call, ContactsUtils.USER_TYPE_CURRENT));
+ setNotificationWhen(call, callState, publicBuilder);
+
+ // Builder for the notification shown when the device is unlocked or the user has set their
+ // notification settings to 'show all notification content'.
+ final Notification.Builder builder = getNotificationBuilder();
+ builder.setPublicVersion(publicBuilder.build());
+
+ // Set up the main intent to send the user to the in-call screen
+ builder.setContentIntent(
+ createLaunchPendingIntent(false /* isFullScreen */, call.isVideoCall()));
+
+ // Set the intent as a full screen intent as well if a call is incoming
+ if (notificationType == NOTIFICATION_INCOMING_CALL) {
+ if (!InCallPresenter.getInstance().isActivityStarted()) {
+ configureFullScreenIntent(
+ builder,
+ createLaunchPendingIntent(true /* isFullScreen */, call.isVideoCall()),
+ callList,
+ call);
+ } else {
+ // If the incall screen is already up, we don't want to show HUN but regular notification
+ // should still be shown. In order to do that the previous one with full screen intent
+ // needs to be cancelled.
+ LogUtil.d(
+ "StatusBarNotifier.buildAndSendNotification",
+ "cancel previous incoming call notification");
+ mNotificationManager.cancel(NOTIFICATION_INCOMING_CALL);
+ }
+ // Set the notification category for incoming calls
+ builder.setCategory(Notification.CATEGORY_CALL);
+ }
+
+ // Set the content
+ builder.setContentText(content);
+ builder.setSmallIcon(iconResId);
+ builder.setContentTitle(contentTitle);
+ builder.setLargeIcon(largeIcon);
+ builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color));
+
+ if (isVideoUpgradeRequest) {
+ builder.setUsesChronometer(false);
+ addDismissUpgradeRequestAction(builder);
+ addAcceptUpgradeRequestAction(builder);
+ } else {
+ createIncomingCallNotification(call, callState, builder);
+ }
+
+ addPersonReference(builder, contactInfo, call);
+
+ // Fire off the notification
+ Notification notification = builder.build();
+
+ if (mDialerRingtoneManager.shouldPlayRingtone(callState, contactInfo.contactRingtoneUri)) {
+ notification.flags |= Notification.FLAG_INSISTENT;
+ notification.sound = contactInfo.contactRingtoneUri;
+ AudioAttributes.Builder audioAttributes = new AudioAttributes.Builder();
+ audioAttributes.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
+ audioAttributes.setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE);
+ notification.audioAttributes = audioAttributes.build();
+ if (mDialerRingtoneManager.shouldVibrate(mContext.getContentResolver())) {
+ notification.vibrate = VIBRATE_PATTERN;
+ }
+ }
+ if (mDialerRingtoneManager.shouldPlayCallWaitingTone(callState)) {
+ Log.v(this, "Playing call waiting tone");
+ mDialerRingtoneManager.playCallWaitingTone();
+ }
+ if (mCurrentNotification != notificationType && mCurrentNotification != NOTIFICATION_NONE) {
+ Log.i(this, "Previous notification already showing - cancelling " + mCurrentNotification);
+ mNotificationManager.cancel(mCurrentNotification);
+ }
+
+ Log.i(this, "Displaying notification for " + notificationType);
+ try {
+ mNotificationManager.notify(notificationType, notification);
+ } catch (RuntimeException e) {
+ // TODO(b/34744003): Move the memory stats into silent feedback PSD.
+ ActivityManager activityManager = mContext.getSystemService(ActivityManager.class);
+ ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+ activityManager.getMemoryInfo(memoryInfo);
+ throw new RuntimeException(
+ String.format(
+ "Error displaying notification with photo type: %d (low memory? %b, availMem: %d)",
+ contactInfo.photoType, memoryInfo.lowMemory, memoryInfo.availMem),
+ e);
+ }
+ call.getLatencyReport().onNotificationShown();
+ mCurrentNotification = notificationType;
+ }
+
+ private void createIncomingCallNotification(
+ DialerCall call, int state, Notification.Builder builder) {
+ setNotificationWhen(call, state, builder);
+
+ // Add hang up option for any active calls (active | onhold), outgoing calls (dialing).
+ if (state == DialerCall.State.ACTIVE
+ || state == DialerCall.State.ONHOLD
+ || DialerCall.State.isDialing(state)) {
+ addHangupAction(builder);
+ } else if (state == DialerCall.State.INCOMING || state == DialerCall.State.CALL_WAITING) {
+ addDismissAction(builder);
+ if (call.isVideoCall()) {
+ addVideoCallAction(builder);
+ } else {
+ addAnswerAction(builder);
+ }
+ }
+ }
+
+ /**
+ * Sets the notification's when section as needed. For active calls, this is explicitly set as the
+ * duration of the call. For all other states, the notification will automatically show the time
+ * at which the notification was created.
+ */
+ private void setNotificationWhen(DialerCall call, int state, Notification.Builder builder) {
+ if (state == DialerCall.State.ACTIVE) {
+ builder.setUsesChronometer(true);
+ builder.setWhen(call.getConnectTimeMillis());
+ } else {
+ builder.setUsesChronometer(false);
+ }
+ }
+
+ /**
+ * Checks the new notification data and compares it against any notification that we are already
+ * displaying. If the data is exactly the same, we return false so that we do not issue a new
+ * notification for the exact same data.
+ */
+ private boolean checkForChangeAndSaveData(
+ int icon,
+ String content,
+ Bitmap largeIcon,
+ String contentTitle,
+ int state,
+ int notificationType,
+ Uri ringtone) {
+
+ // The two are different:
+ // if new title is not null, it should be different from saved version OR
+ // if new title is null, the saved version should not be null
+ final boolean contentTitleChanged =
+ (contentTitle != null && !contentTitle.equals(mSavedContentTitle))
+ || (contentTitle == null && mSavedContentTitle != null);
+
+ // any change means we are definitely updating
+ boolean retval =
+ (mSavedIcon != icon)
+ || !Objects.equals(mSavedContent, content)
+ || (mCallState != state)
+ || (mSavedLargeIcon != largeIcon)
+ || contentTitleChanged
+ || !Objects.equals(mRingtone, ringtone);
+
+ // If we aren't showing a notification right now or the notification type is changing,
+ // definitely do an update.
+ if (mCurrentNotification != notificationType) {
+ if (mCurrentNotification == NOTIFICATION_NONE) {
+ Log.d(this, "Showing notification for first time.");
+ }
+ retval = true;
+ }
+
+ mSavedIcon = icon;
+ mSavedContent = content;
+ mCallState = state;
+ mSavedLargeIcon = largeIcon;
+ mSavedContentTitle = contentTitle;
+ mRingtone = ringtone;
+
+ if (retval) {
+ Log.d(this, "Data changed. Showing notification");
+ }
+
+ return retval;
+ }
+
+ /** Returns the main string to use in the notification. */
+ @VisibleForTesting
+ @Nullable
+ String getContentTitle(ContactCacheEntry contactInfo, DialerCall call) {
+ if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
+ return mContext.getResources().getString(R.string.conference_call_name);
+ }
+
+ String preferredName =
+ ContactDisplayUtils.getPreferredDisplayName(
+ contactInfo.namePrimary, contactInfo.nameAlternative, mContactsPreferences);
+ if (TextUtils.isEmpty(preferredName)) {
+ return TextUtils.isEmpty(contactInfo.number)
+ ? null
+ : BidiFormatter.getInstance()
+ .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR);
+ }
+ return preferredName;
+ }
+
+ private void addPersonReference(
+ Notification.Builder builder, ContactCacheEntry contactInfo, DialerCall call) {
+ // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed.
+ // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid
+ // NotificationManager using it.
+ if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) {
+ builder.addPerson(contactInfo.lookupUri.toString());
+ } else if (!TextUtils.isEmpty(call.getNumber())) {
+ builder.addPerson(Uri.fromParts(PhoneAccount.SCHEME_TEL, call.getNumber(), null).toString());
+ }
+ }
+
+ /** Gets a large icon from the contact info object to display in the notification. */
+ private Bitmap getLargeIconToDisplay(ContactCacheEntry contactInfo, DialerCall call) {
+ Bitmap largeIcon = null;
+ if (call.isConferenceCall() && !call.hasProperty(Details.PROPERTY_GENERIC_CONFERENCE)) {
+ largeIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.img_conference);
+ }
+ if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) {
+ largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap();
+ }
+ if (call.isSpam()) {
+ Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact);
+ largeIcon = DrawableConverter.drawableToBitmap(drawable);
+ }
+ return largeIcon;
+ }
+
+ private Bitmap getRoundedIcon(Bitmap bitmap) {
+ if (bitmap == null) {
+ return null;
+ }
+ final int height =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_height);
+ final int width =
+ (int) mContext.getResources().getDimension(android.R.dimen.notification_large_icon_width);
+ return BitmapUtil.getRoundedBitmap(bitmap, width, height);
+ }
+
+ /**
+ * Returns the appropriate icon res Id to display based on the call for which we want to display
+ * information.
+ */
+ private int getIconToDisplay(DialerCall call) {
+ // Even if both lines are in use, we only show a single item in
+ // the expanded Notifications UI. It's labeled "Ongoing call"
+ // (or "On hold" if there's only one call, and it's on hold.)
+ // Also, we don't have room to display caller-id info from two
+ // different calls. So if both lines are in use, display info
+ // from the foreground call. And if there's a ringing call,
+ // display that regardless of the state of the other calls.
+ if (call.getState() == DialerCall.State.ONHOLD) {
+ return R.drawable.ic_phone_paused_white_24dp;
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return R.drawable.ic_videocam;
+ }
+ return R.anim.on_going_call;
+ }
+
+ /** Returns the message to use with the notification. */
+ private String getContentString(DialerCall call, @UserType long userType) {
+ boolean isIncomingOrWaiting =
+ call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING;
+
+ if (isIncomingOrWaiting
+ && call.getNumberPresentation() == TelecomManager.PRESENTATION_ALLOWED) {
+
+ if (!TextUtils.isEmpty(call.getChildNumber())) {
+ return mContext.getString(R.string.child_number, call.getChildNumber());
+ } else if (!TextUtils.isEmpty(call.getCallSubject()) && call.isCallSubjectSupported()) {
+ return call.getCallSubject();
+ }
+ }
+
+ int resId = R.string.notification_ongoing_call;
+ if (call.hasProperty(Details.PROPERTY_WIFI)) {
+ resId = R.string.notification_ongoing_call_wifi;
+ }
+
+ if (isIncomingOrWaiting) {
+ if (call.hasProperty(Details.PROPERTY_WIFI)) {
+ resId = R.string.notification_incoming_call_wifi;
+ } else {
+ if (call.isSpam()) {
+ resId = R.string.notification_incoming_spam_call;
+ } else {
+ resId = R.string.notification_incoming_call;
+ }
+ }
+ } else if (call.getState() == DialerCall.State.ONHOLD) {
+ resId = R.string.notification_on_hold;
+ } else if (DialerCall.State.isDialing(call.getState())) {
+ resId = R.string.notification_dialing;
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ resId = R.string.notification_requesting_video_call;
+ }
+
+ // Is the call placed through work connection service.
+ boolean isWorkCall = call.hasProperty(PROPERTY_ENTERPRISE_CALL);
+ if (userType == ContactsUtils.USER_TYPE_WORK || isWorkCall) {
+ resId = getWorkStringFromPersonalString(resId);
+ }
+
+ return mContext.getString(resId);
+ }
+
+ /** Gets the most relevant call to display in the notification. */
+ private DialerCall getCallToShow(CallList callList) {
+ if (callList == null) {
+ return null;
+ }
+ DialerCall call = callList.getIncomingCall();
+ if (call == null) {
+ call = callList.getOutgoingCall();
+ }
+ if (call == null) {
+ call = callList.getVideoUpgradeRequestCall();
+ }
+ if (call == null) {
+ call = callList.getActiveOrBackgroundCall();
+ }
+ return call;
+ }
+
+ private Spannable getActionText(@StringRes int stringRes, @ColorRes int colorRes) {
+ Spannable spannable = new SpannableString(mContext.getText(stringRes));
+ if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
+ // This will only work for cases where the Notification.Builder has a fullscreen intent set
+ // Notification.Builder that does not have a full screen intent will take the color of the
+ // app and the following leads to a no-op.
+ spannable.setSpan(
+ new ForegroundColorSpan(mContext.getColor(colorRes)), 0, spannable.length(), 0);
+ }
+ return spannable;
+ }
+
+ private void addAnswerAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"answer\" action in the incoming call Notification");
+ PendingIntent answerVoicePendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ANSWER_VOICE_INCOMING_CALL);
+ builder.addAction(
+ R.anim.on_going_call,
+ getActionText(R.string.notification_action_answer, R.color.notification_action_accept),
+ answerVoicePendingIntent);
+ }
+
+ private void addDismissAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"decline\" action in the incoming call Notification");
+ PendingIntent declinePendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_DECLINE_INCOMING_CALL);
+ builder.addAction(
+ R.drawable.ic_close_dk,
+ getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declinePendingIntent);
+ }
+
+ private void addHangupAction(Notification.Builder builder) {
+ Log.d(this, "Will show \"hang-up\" action in the ongoing active call Notification");
+ PendingIntent hangupPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_HANG_UP_ONGOING_CALL);
+ builder.addAction(
+ R.drawable.ic_call_end_white_24dp,
+ getActionText(R.string.notification_action_end_call, R.color.notification_action_end_call),
+ hangupPendingIntent);
+ }
+
+ private void addVideoCallAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"video\" action in the incoming call Notification");
+ PendingIntent answerVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ANSWER_VIDEO_INCOMING_CALL);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(
+ R.string.notification_action_answer_video, R.color.notification_action_answer_video),
+ answerVideoPendingIntent);
+ }
+
+ private void addAcceptUpgradeRequestAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"accept upgrade\" action in the incoming call Notification");
+ PendingIntent acceptVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(R.string.notification_action_accept, R.color.notification_action_accept),
+ acceptVideoPendingIntent);
+ }
+
+ private void addDismissUpgradeRequestAction(Notification.Builder builder) {
+ Log.i(this, "Will show \"dismiss upgrade\" action in the incoming call Notification");
+ PendingIntent declineVideoPendingIntent =
+ createNotificationPendingIntent(mContext, ACTION_DECLINE_VIDEO_UPGRADE_REQUEST);
+ builder.addAction(
+ R.drawable.ic_videocam,
+ getActionText(R.string.notification_action_dismiss, R.color.notification_action_dismiss),
+ declineVideoPendingIntent);
+ }
+
+ /** Adds fullscreen intent to the builder. */
+ private void configureFullScreenIntent(
+ Notification.Builder builder, PendingIntent intent, CallList callList, DialerCall call) {
+ // Ok, we actually want to launch the incoming call
+ // UI at this point (in addition to simply posting a notification
+ // to the status bar). Setting fullScreenIntent will cause
+ // the InCallScreen to be launched immediately *unless* the
+ // current foreground activity is marked as "immersive".
+ Log.d(this, "- Setting fullScreenIntent: " + intent);
+ builder.setFullScreenIntent(intent, true);
+
+ // Ugly hack alert:
+ //
+ // The NotificationManager has the (undocumented) behavior
+ // that it will *ignore* the fullScreenIntent field if you
+ // post a new Notification that matches the ID of one that's
+ // already active. Unfortunately this is exactly what happens
+ // when you get an incoming call-waiting call: the
+ // "ongoing call" notification is already visible, so the
+ // InCallScreen won't get launched in this case!
+ // (The result: if you bail out of the in-call UI while on a
+ // call and then get a call-waiting call, the incoming call UI
+ // won't come up automatically.)
+ //
+ // The workaround is to just notice this exact case (this is a
+ // call-waiting call *and* the InCallScreen is not in the
+ // foreground) and manually cancel the in-call notification
+ // before (re)posting it.
+ //
+ // TODO: there should be a cleaner way of avoiding this
+ // problem (see discussion in bug 3184149.)
+
+ // If a call is onhold during an incoming call, the call actually comes in as
+ // INCOMING. For that case *and* traditional call-waiting, we want to
+ // cancel the notification.
+ boolean isCallWaiting =
+ (call.getState() == DialerCall.State.CALL_WAITING
+ || (call.getState() == DialerCall.State.INCOMING
+ && callList.getBackgroundCall() != null));
+
+ if (isCallWaiting) {
+ Log.i(this, "updateInCallNotification: call-waiting! force relaunch...");
+ // Cancel the IN_CALL_NOTIFICATION immediately before
+ // (re)posting it; this seems to force the
+ // NotificationManager to launch the fullScreenIntent.
+ mNotificationManager.cancel(NOTIFICATION_IN_CALL);
+ }
+ }
+
+ private Notification.Builder getNotificationBuilder() {
+ final Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setOngoing(true);
+
+ // Make the notification prioritized over the other normal notifications.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+
+ return builder;
+ }
+
+ private PendingIntent createLaunchPendingIntent(boolean isFullScreen, boolean isVideoCall) {
+ Intent intent =
+ InCallActivity.getIntent(
+ mContext,
+ false /* showDialpad */,
+ false /* newOutgoingCall */,
+ isVideoCall,
+ isFullScreen);
+
+ int requestCode = PENDING_INTENT_REQUEST_CODE_NON_FULL_SCREEN;
+ if (isFullScreen) {
+ // Use a unique request code so that the pending intent isn't clobbered by the
+ // non-full screen pending intent.
+ requestCode = PENDING_INTENT_REQUEST_CODE_FULL_SCREEN;
+ }
+
+ // PendingIntent that can be used to launch the InCallActivity. The
+ // system fires off this intent if the user pulls down the windowshade
+ // and clicks the notification's expanded view. It's also used to
+ // launch the InCallActivity immediately when when there's an incoming
+ // call (see the "fullScreenIntent" field below).
+ return PendingIntent.getActivity(mContext, requestCode, intent, 0);
+ }
+
+ private void setStatusBarCallListener(StatusBarCallListener listener) {
+ if (mStatusBarCallListener != null) {
+ mStatusBarCallListener.cleanup();
+ }
+ mStatusBarCallListener = listener;
+ }
+
+ private class StatusBarCallListener implements DialerCallListener {
+
+ private DialerCall mDialerCall;
+
+ StatusBarCallListener(DialerCall dialerCall) {
+ mDialerCall = dialerCall;
+ mDialerCall.addListener(this);
+ }
+
+ void cleanup() {
+ mDialerCall.removeListener(this);
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {}
+
+ @Override
+ public void onDialerCallUpdate() {
+ if (CallList.getInstance().getIncomingCall() == null) {
+ mDialerRingtoneManager.stopCallWaitingTone();
+ }
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ /**
+ * Responds to changes in the session modification state for the call by dismissing the status
+ * bar notification as required.
+ */
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ if (state == DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST) {
+ cleanup();
+ updateNotification(CallList.getInstance());
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/ThemeColorManager.java b/java/com/android/incallui/ThemeColorManager.java
new file mode 100644
index 000000000..a88ae33cd
--- /dev/null
+++ b/java/com/android/incallui/ThemeColorManager.java
@@ -0,0 +1,142 @@
+/*
+ * 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.incallui;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.support.annotation.ColorInt;
+import android.support.annotation.Nullable;
+import android.support.v4.graphics.ColorUtils;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.contacts.common.util.MaterialColorMapUtils;
+import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette;
+import com.android.incallui.call.DialerCall;
+
+/**
+ * Calculates the background color for the in call window. The background color is based on the SIM
+ * and spam status.
+ */
+public class ThemeColorManager {
+ private final MaterialColorMapUtils colorMap;
+ @ColorInt private int primaryColor;
+ @ColorInt private int secondaryColor;
+ @ColorInt private int backgroundColorTop;
+ @ColorInt private int backgroundColorMiddle;
+ @ColorInt private int backgroundColorBottom;
+ @ColorInt private int backgroundColorSolid;
+
+ /**
+ * If there is no actual call currently in the call list, this will be used as a fallback to
+ * determine the theme color for InCallUI.
+ */
+ @Nullable private PhoneAccountHandle pendingPhoneAccountHandle;
+
+ public ThemeColorManager(MaterialColorMapUtils colorMap) {
+ this.colorMap = colorMap;
+ }
+
+ public void setPendingPhoneAccountHandle(@Nullable PhoneAccountHandle pendingPhoneAccountHandle) {
+ this.pendingPhoneAccountHandle = pendingPhoneAccountHandle;
+ }
+
+ public void onForegroundCallChanged(Context context, @Nullable DialerCall newForegroundCall) {
+ if (newForegroundCall == null) {
+ updateThemeColors(context, pendingPhoneAccountHandle, false);
+ } else {
+ updateThemeColors(context, newForegroundCall.getAccountHandle(), newForegroundCall.isSpam());
+ }
+ }
+
+ private void updateThemeColors(
+ Context context, @Nullable PhoneAccountHandle handle, boolean isSpam) {
+ MaterialPalette palette;
+ if (isSpam) {
+ palette =
+ colorMap.calculatePrimaryAndSecondaryColor(R.color.incall_call_spam_background_color);
+ backgroundColorTop = context.getColor(R.color.incall_background_gradient_spam_top);
+ backgroundColorMiddle = context.getColor(R.color.incall_background_gradient_spam_middle);
+ backgroundColorBottom = context.getColor(R.color.incall_background_gradient_spam_bottom);
+ backgroundColorSolid = context.getColor(R.color.incall_background_multiwindow_spam);
+ } else {
+ @ColorInt int highlightColor = getHighlightColor(context, handle);
+ palette = colorMap.calculatePrimaryAndSecondaryColor(highlightColor);
+ backgroundColorTop = context.getColor(R.color.incall_background_gradient_top);
+ backgroundColorMiddle = context.getColor(R.color.incall_background_gradient_middle);
+ backgroundColorBottom = context.getColor(R.color.incall_background_gradient_bottom);
+ backgroundColorSolid = context.getColor(R.color.incall_background_multiwindow);
+ if (highlightColor != PhoneAccount.NO_HIGHLIGHT_COLOR) {
+ // The default background gradient has a subtle alpha. We grab that alpha and apply it to
+ // the phone account color.
+ backgroundColorTop = applyAlpha(palette.mPrimaryColor, backgroundColorTop);
+ backgroundColorMiddle = applyAlpha(palette.mPrimaryColor, backgroundColorMiddle);
+ backgroundColorBottom = applyAlpha(palette.mPrimaryColor, backgroundColorBottom);
+ backgroundColorSolid = applyAlpha(palette.mPrimaryColor, backgroundColorSolid);
+ }
+ }
+
+ primaryColor = palette.mPrimaryColor;
+ secondaryColor = palette.mSecondaryColor;
+ }
+
+ @ColorInt
+ private static int getHighlightColor(Context context, @Nullable PhoneAccountHandle handle) {
+ if (handle != null) {
+ PhoneAccount account = context.getSystemService(TelecomManager.class).getPhoneAccount(handle);
+ if (account != null) {
+ return account.getHighlightColor();
+ }
+ }
+ return PhoneAccount.NO_HIGHLIGHT_COLOR;
+ }
+
+ @ColorInt
+ public int getPrimaryColor() {
+ return primaryColor;
+ }
+
+ @ColorInt
+ public int getSecondaryColor() {
+ return secondaryColor;
+ }
+
+ @ColorInt
+ public int getBackgroundColorTop() {
+ return backgroundColorTop;
+ }
+
+ @ColorInt
+ public int getBackgroundColorMiddle() {
+ return backgroundColorMiddle;
+ }
+
+ @ColorInt
+ public int getBackgroundColorBottom() {
+ return backgroundColorBottom;
+ }
+
+ @ColorInt
+ public int getBackgroundColorSolid() {
+ return backgroundColorSolid;
+ }
+
+ @ColorInt
+ private static int applyAlpha(@ColorInt int color, @ColorInt int sourceColorWithAlpha) {
+ return ColorUtils.setAlphaComponent(color, Color.alpha(sourceColorWithAlpha));
+ }
+}
diff --git a/java/com/android/incallui/TransactionSafeFragmentActivity.java b/java/com/android/incallui/TransactionSafeFragmentActivity.java
new file mode 100644
index 000000000..a6b078cb4
--- /dev/null
+++ b/java/com/android/incallui/TransactionSafeFragmentActivity.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.incallui;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentActivity;
+
+/**
+ * A common superclass that keeps track of whether an {@link Activity} has saved its state yet or
+ * not.
+ */
+public abstract class TransactionSafeFragmentActivity extends FragmentActivity {
+
+ private boolean mIsSafeToCommitTransactions;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mIsSafeToCommitTransactions = true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mIsSafeToCommitTransactions = false;
+ }
+
+ /**
+ * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
+ * whether {@link Activity#onSaveInstanceState} has been called or not.
+ *
+ * <p>Make sure that the current activity calls into {@link super.onSaveInstanceState(Bundle
+ * outState)} (if that method is overridden), so the flag is properly set.
+ */
+ public boolean isSafeToCommitTransactions() {
+ return mIsSafeToCommitTransactions;
+ }
+}
diff --git a/java/com/android/incallui/VideoCallPresenter.java b/java/com/android/incallui/VideoCallPresenter.java
new file mode 100644
index 000000000..971b6957a
--- /dev/null
+++ b/java/com/android/incallui/VideoCallPresenter.java
@@ -0,0 +1,1289 @@
+/*
+ * 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.incallui;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Point;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import android.view.Surface;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.CompatUtils;
+import com.android.incallui.InCallPresenter.InCallDetailsListener;
+import com.android.incallui.InCallPresenter.InCallOrientationListener;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier.SurfaceChangeListener;
+import com.android.incallui.call.InCallVideoCallCallbackNotifier.VideoEventListener;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.util.AccessibilityUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import java.util.Objects;
+
+/**
+ * Logic related to the {@link VideoCallScreen} and for managing changes to the video calling
+ * surfaces based on other user interface events and incoming events from the {@class
+ * VideoCallListener}.
+ *
+ * <p>When a call's video state changes to bi-directional video, the {@link
+ * com.android.incallui.VideoCallPresenter} performs the following negotiation with the telephony
+ * layer:
+ *
+ * <ul>
+ * <li>{@code VideoCallPresenter} creates and informs telephony of the display surface.
+ * <li>{@code VideoCallPresenter} creates the preview surface.
+ * <li>{@code VideoCallPresenter} informs telephony of the currently selected camera.
+ * <li>Telephony layer sends {@link CameraCapabilities}, including the dimensions of the video for
+ * the current camera.
+ * <li>{@code VideoCallPresenter} adjusts size of the preview surface to match the aspect ratio of
+ * the camera.
+ * <li>{@code VideoCallPresenter} informs telephony of the new preview surface.
+ * </ul>
+ *
+ * <p>When downgrading to an audio-only video state, the {@code VideoCallPresenter} nulls both
+ * surfaces.
+ */
+public class VideoCallPresenter
+ implements IncomingCallListener,
+ InCallOrientationListener,
+ InCallStateListener,
+ InCallDetailsListener,
+ SurfaceChangeListener,
+ VideoEventListener,
+ InCallPresenter.InCallEventListener,
+ VideoCallScreenDelegate {
+
+ private static boolean mIsVideoMode = false;
+
+ private final Handler mHandler = new Handler();
+ private VideoCallScreen mVideoCallScreen;
+
+ /** The current context. */
+ private Context mContext;
+
+ @Override
+ public boolean shouldShowCameraPermissionDialog() {
+ if (mPrimaryCall == null) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "null call");
+ return false;
+ }
+ if (mPrimaryCall.didShowCameraPermission()) {
+ LogUtil.i(
+ "VideoCallPresenter.shouldShowCameraPermissionDialog", "already shown for this call");
+ return false;
+ }
+ if (!ConfigProviderBindings.get(mContext)
+ .getBoolean("camera_permission_dialog_allowed", true)) {
+ LogUtil.i("VideoCallPresenter.shouldShowCameraPermissionDialog", "disabled by config");
+ return false;
+ }
+ return !VideoUtils.hasCameraPermission(mContext) || !VideoUtils.isCameraAllowedByUser(mContext);
+ }
+
+ @Override
+ public void onCameraPermissionDialogShown() {
+ if (mPrimaryCall != null) {
+ mPrimaryCall.setDidShowCameraPermission(true);
+ }
+ }
+
+ /** The call the video surfaces are currently related to */
+ private DialerCall mPrimaryCall;
+ /**
+ * The {@link VideoCall} used to inform the video telephony layer of changes to the video
+ * surfaces.
+ */
+ private VideoCall mVideoCall;
+ /** Determines if the current UI state represents a video call. */
+ private int mCurrentVideoState;
+ /** DialerCall's current state */
+ private int mCurrentCallState = DialerCall.State.INVALID;
+ /** Determines the device orientation (portrait/lanscape). */
+ private int mDeviceOrientation = InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN;
+ /** Tracks the state of the preview surface negotiation with the telephony layer. */
+ private int mPreviewSurfaceState = PreviewSurfaceState.NONE;
+ /**
+ * Determines whether video calls should automatically enter full screen mode after {@link
+ * #mAutoFullscreenTimeoutMillis} milliseconds.
+ */
+ private boolean mIsAutoFullscreenEnabled = false;
+ /**
+ * Determines the number of milliseconds after which a video call will automatically enter
+ * fullscreen mode. Requires {@link #mIsAutoFullscreenEnabled} to be {@code true}.
+ */
+ private int mAutoFullscreenTimeoutMillis = 0;
+ /**
+ * Determines if the countdown is currently running to automatically enter full screen video mode.
+ */
+ private boolean mAutoFullScreenPending = false;
+ /** Whether if the call is remotely held. */
+ private boolean mIsRemotelyHeld = false;
+ /**
+ * Runnable which is posted to schedule automatically entering fullscreen mode. Will not auto
+ * enter fullscreen mode if the dialpad is visible (doing so would make it impossible to exit the
+ * dialpad).
+ */
+ private Runnable mAutoFullscreenRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mAutoFullScreenPending
+ && !InCallPresenter.getInstance().isDialpadVisible()
+ && mIsVideoMode) {
+
+ LogUtil.v("VideoCallPresenter.mAutoFullScreenRunnable", "entering fullscreen mode");
+ InCallPresenter.getInstance().setFullScreen(true);
+ mAutoFullScreenPending = false;
+ } else {
+ LogUtil.v(
+ "VideoCallPresenter.mAutoFullScreenRunnable",
+ "skipping scheduled fullscreen mode.");
+ }
+ }
+ };
+
+ private boolean isVideoCallScreenUiReady;
+
+ private static boolean isCameraRequired(int videoState, int sessionModificationState) {
+ return VideoProfile.isBidirectional(videoState)
+ || VideoProfile.isTransmissionEnabled(videoState)
+ || isVideoUpgrade(sessionModificationState);
+ }
+
+ /**
+ * Determines if the incoming video surface should be shown based on the current videoState and
+ * callState. The video surface is shown when incoming video is not paused, the call is active,
+ * and video reception is enabled.
+ *
+ * @param videoState The current video state.
+ * @param callState The current call state.
+ * @return {@code true} if the incoming video surface should be shown, {@code false} otherwise.
+ */
+ public static boolean showIncomingVideo(int videoState, int callState) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ boolean isPaused = VideoProfile.isPaused(videoState);
+ boolean isCallActive = callState == DialerCall.State.ACTIVE;
+
+ return !isPaused && isCallActive && VideoProfile.isReceptionEnabled(videoState);
+ }
+
+ /**
+ * Determines if the outgoing video surface should be shown based on the current videoState. The
+ * video surface is shown if video transmission is enabled.
+ *
+ * @return {@code true} if the the outgoing video surface should be shown, {@code false}
+ * otherwise.
+ */
+ public static boolean showOutgoingVideo(
+ Context context, int videoState, int sessionModificationState) {
+ if (!VideoUtils.hasCameraPermissionAndAllowedByUser(context)) {
+ LogUtil.i("VideoCallPresenter.showOutgoingVideo", "Camera permission is disabled by user.");
+ return false;
+ }
+
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ return VideoProfile.isTransmissionEnabled(videoState)
+ || isVideoUpgrade(sessionModificationState);
+ }
+
+ private static void updateCameraSelection(DialerCall call) {
+ LogUtil.v("VideoCallPresenter.updateCameraSelection", "call=" + call);
+ LogUtil.v("VideoCallPresenter.updateCameraSelection", "call=" + toSimpleString(call));
+
+ final DialerCall activeCall = CallList.getInstance().getActiveCall();
+ int cameraDir;
+
+ // this function should never be called with null call object, however if it happens we
+ // should handle it gracefully.
+ if (call == null) {
+ cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ LogUtil.e(
+ "VideoCallPresenter.updateCameraSelection",
+ "call is null. Setting camera direction to default value (CAMERA_DIRECTION_UNKNOWN)");
+ }
+
+ // Clear camera direction if this is not a video call.
+ else if (VideoUtils.isAudioCall(call) && !isVideoUpgrade(call)) {
+ cameraDir = DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ call.getVideoSettings().setCameraDir(cameraDir);
+ }
+
+ // If this is a waiting video call, default to active call's camera,
+ // since we don't want to change the current camera for waiting call
+ // without user's permission.
+ else if (VideoUtils.isVideoCall(activeCall) && VideoUtils.isIncomingVideoCall(call)) {
+ cameraDir = activeCall.getVideoSettings().getCameraDir();
+ }
+
+ // Infer the camera direction from the video state and store it,
+ // if this is an outgoing video call.
+ else if (VideoUtils.isOutgoingVideoCall(call) && !isCameraDirectionSet(call)) {
+ cameraDir = toCameraDirection(call.getVideoState());
+ call.getVideoSettings().setCameraDir(cameraDir);
+ }
+
+ // Use the stored camera dir if this is an outgoing video call for which camera direction
+ // is set.
+ else if (VideoUtils.isOutgoingVideoCall(call)) {
+ cameraDir = call.getVideoSettings().getCameraDir();
+ }
+
+ // Infer the camera direction from the video state and store it,
+ // if this is an active video call and camera direction is not set.
+ else if (VideoUtils.isActiveVideoCall(call) && !isCameraDirectionSet(call)) {
+ cameraDir = toCameraDirection(call.getVideoState());
+ call.getVideoSettings().setCameraDir(cameraDir);
+ }
+
+ // Use the stored camera dir if this is an active video call for which camera direction
+ // is set.
+ else if (VideoUtils.isActiveVideoCall(call)) {
+ cameraDir = call.getVideoSettings().getCameraDir();
+ }
+
+ // For all other cases infer the camera direction but don't store it in the call object.
+ else {
+ cameraDir = toCameraDirection(call.getVideoState());
+ }
+
+ LogUtil.i(
+ "VideoCallPresenter.updateCameraSelection",
+ "setting camera direction to %d, call: %s",
+ cameraDir,
+ call);
+ final InCallCameraManager cameraManager =
+ InCallPresenter.getInstance().getInCallCameraManager();
+ cameraManager.setUseFrontFacingCamera(
+ cameraDir == DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING);
+ }
+
+ private static int toCameraDirection(int videoState) {
+ return VideoProfile.isTransmissionEnabled(videoState)
+ && !VideoProfile.isBidirectional(videoState)
+ ? DialerCall.VideoSettings.CAMERA_DIRECTION_BACK_FACING
+ : DialerCall.VideoSettings.CAMERA_DIRECTION_FRONT_FACING;
+ }
+
+ private static boolean isCameraDirectionSet(DialerCall call) {
+ return VideoUtils.isVideoCall(call)
+ && call.getVideoSettings().getCameraDir()
+ != DialerCall.VideoSettings.CAMERA_DIRECTION_UNKNOWN;
+ }
+
+ private static String toSimpleString(DialerCall call) {
+ return call == null ? null : call.toSimpleString();
+ }
+
+ /**
+ * Initializes the presenter.
+ *
+ * @param context The current context.
+ */
+ @Override
+ public void initVideoCallScreenDelegate(Context context, VideoCallScreen videoCallScreen) {
+ mContext = context;
+ mVideoCallScreen = videoCallScreen;
+ mIsAutoFullscreenEnabled =
+ mContext.getResources().getBoolean(R.bool.video_call_auto_fullscreen);
+ mAutoFullscreenTimeoutMillis =
+ mContext.getResources().getInteger(R.integer.video_call_auto_fullscreen_timeout);
+ }
+
+ /** Called when the user interface is ready to be used. */
+ @Override
+ public void onVideoCallScreenUiReady() {
+ LogUtil.v("VideoCallPresenter.onVideoCallScreenUiReady", "");
+ Assert.checkState(!isVideoCallScreenUiReady);
+
+ // Do not register any listeners if video calling is not compatible to safeguard against
+ // any accidental calls of video calling code.
+ if (!CompatUtils.isVideoCompatible()) {
+ return;
+ }
+
+ mDeviceOrientation = InCallOrientationEventListener.getCurrentOrientation();
+
+ // Register for call state changes last
+ InCallPresenter.getInstance().addListener(this);
+ InCallPresenter.getInstance().addDetailsListener(this);
+ InCallPresenter.getInstance().addIncomingCallListener(this);
+ InCallPresenter.getInstance().addOrientationListener(this);
+ // To get updates of video call details changes
+ InCallPresenter.getInstance().addInCallEventListener(this);
+ InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(new LocalDelegate());
+ InCallPresenter.getInstance().getRemoteVideoSurfaceTexture().setDelegate(new RemoteDelegate());
+
+ // Register for surface and video events from {@link InCallVideoCallListener}s.
+ InCallVideoCallCallbackNotifier.getInstance().addSurfaceChangeListener(this);
+ InCallVideoCallCallbackNotifier.getInstance().addVideoEventListener(this);
+ mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
+ mCurrentCallState = DialerCall.State.INVALID;
+
+ InCallPresenter.InCallState inCallState = InCallPresenter.getInstance().getInCallState();
+ onStateChange(inCallState, inCallState, CallList.getInstance());
+ isVideoCallScreenUiReady = true;
+ }
+
+ /** Called when the user interface is no longer ready to be used. */
+ @Override
+ public void onVideoCallScreenUiUnready() {
+ LogUtil.v("VideoCallPresenter.onVideoCallScreenUiUnready", "");
+ Assert.checkState(isVideoCallScreenUiReady);
+
+ if (!CompatUtils.isVideoCompatible()) {
+ return;
+ }
+
+ cancelAutoFullScreen();
+
+ InCallPresenter.getInstance().removeListener(this);
+ InCallPresenter.getInstance().removeDetailsListener(this);
+ InCallPresenter.getInstance().removeIncomingCallListener(this);
+ InCallPresenter.getInstance().removeOrientationListener(this);
+ InCallPresenter.getInstance().removeInCallEventListener(this);
+ InCallPresenter.getInstance().getLocalVideoSurfaceTexture().setDelegate(null);
+
+ InCallVideoCallCallbackNotifier.getInstance().removeSurfaceChangeListener(this);
+ InCallVideoCallCallbackNotifier.getInstance().removeVideoEventListener(this);
+
+ // Ensure that the call's camera direction is updated (most likely to UNKNOWN). Normally this
+ // happens after any call state changes but we're unregistering from InCallPresenter above so
+ // we won't get any more call state changes. See b/32957114.
+ if (mPrimaryCall != null) {
+ updateCameraSelection(mPrimaryCall);
+ }
+
+ isVideoCallScreenUiReady = false;
+ }
+
+ /**
+ * Handles clicks on the video surfaces. If not currently in fullscreen mode, will set fullscreen.
+ */
+ private void onSurfaceClick() {
+ LogUtil.i("VideoCallPresenter.onSurfaceClick", "");
+ cancelAutoFullScreen();
+ if (!InCallPresenter.getInstance().isFullscreen()) {
+ InCallPresenter.getInstance().setFullScreen(true);
+ } else {
+ InCallPresenter.getInstance().setFullScreen(false);
+ maybeAutoEnterFullscreen(mPrimaryCall);
+ // If Activity is not multiwindow, fullscreen will be driven by SystemUI visibility changes
+ // instead. See #onSystemUiVisibilityChange(boolean)
+
+ // TODO (keyboardr): onSystemUiVisibilityChange isn't being called the first time
+ // visibility changes after orientation change, so this is currently always done as a backup.
+ }
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(boolean visible) {
+ // If the SystemUI has changed to be visible, take us out of fullscreen mode
+ LogUtil.i("VideoCallPresenter.onSystemUiVisibilityChange", "visible: " + visible);
+ if (visible) {
+ InCallPresenter.getInstance().setFullScreen(false);
+ maybeAutoEnterFullscreen(mPrimaryCall);
+ }
+ }
+
+ @Override
+ public VideoSurfaceTexture getLocalVideoSurfaceTexture() {
+ return InCallPresenter.getInstance().getLocalVideoSurfaceTexture();
+ }
+
+ @Override
+ public VideoSurfaceTexture getRemoteVideoSurfaceTexture() {
+ return InCallPresenter.getInstance().getRemoteVideoSurfaceTexture();
+ }
+
+ @Override
+ public int getDeviceOrientation() {
+ return mDeviceOrientation;
+ }
+
+ /**
+ * This should only be called when user approved the camera permission, which is local action and
+ * does NOT change any call states.
+ */
+ @Override
+ public void onCameraPermissionGranted() {
+ LogUtil.i("VideoCallPresenter.onCameraPermissionGranted", "");
+ VideoUtils.setCameraAllowedByUser(mContext);
+ enableCamera(mPrimaryCall.getVideoCall(), isCameraRequired());
+ showVideoUi(
+ mPrimaryCall.getVideoState(),
+ mPrimaryCall.getState(),
+ mPrimaryCall.getSessionModificationState(),
+ mPrimaryCall.isRemotelyHeld());
+ InCallPresenter.getInstance().getInCallCameraManager().onCameraPermissionGranted();
+ }
+
+ /**
+ * Called when the user interacts with the UI. If a fullscreen timer is pending then we start the
+ * timer from scratch to avoid having the UI disappear while the user is interacting with it.
+ */
+ @Override
+ public void resetAutoFullscreenTimer() {
+ if (mAutoFullScreenPending) {
+ LogUtil.i("VideoCallPresenter.resetAutoFullscreenTimer", "resetting");
+ mHandler.removeCallbacks(mAutoFullscreenRunnable);
+ mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis);
+ }
+ }
+
+ /**
+ * Handles incoming calls.
+ *
+ * @param oldState The old in call state.
+ * @param newState The new in call state.
+ * @param call The call.
+ */
+ @Override
+ public void onIncomingCall(
+ InCallPresenter.InCallState oldState, InCallPresenter.InCallState newState, DialerCall call) {
+ // same logic should happen as with onStateChange()
+ onStateChange(oldState, newState, CallList.getInstance());
+ }
+
+ /**
+ * Handles state changes (including incoming calls)
+ *
+ * @param newState The in call state.
+ * @param callList The call list.
+ */
+ @Override
+ public void onStateChange(
+ InCallPresenter.InCallState oldState,
+ InCallPresenter.InCallState newState,
+ CallList callList) {
+ LogUtil.v(
+ "VideoCallPresenter.onStateChange",
+ "oldState: %s, newState: %s, isVideoMode: %b",
+ oldState,
+ newState,
+ isVideoMode());
+
+ if (newState == InCallPresenter.InCallState.NO_CALLS) {
+ if (isVideoMode()) {
+ exitVideoMode();
+ }
+
+ InCallPresenter.getInstance().cleanupSurfaces();
+ }
+
+ // Determine the primary active call).
+ DialerCall primary = null;
+
+ // Determine the call which is the focus of the user's attention. In the case of an
+ // incoming call waiting call, the primary call is still the active video call, however
+ // the determination of whether we should be in fullscreen mode is based on the type of the
+ // incoming call, not the active video call.
+ DialerCall currentCall = null;
+
+ if (newState == InCallPresenter.InCallState.INCOMING) {
+ // We don't want to replace active video call (primary call)
+ // with a waiting call, since user may choose to ignore/decline the waiting call and
+ // this should have no impact on current active video call, that is, we should not
+ // change the camera or UI unless the waiting VT call becomes active.
+ primary = callList.getActiveCall();
+ currentCall = callList.getIncomingCall();
+ if (!VideoUtils.isActiveVideoCall(primary)) {
+ primary = callList.getIncomingCall();
+ }
+ } else if (newState == InCallPresenter.InCallState.OUTGOING) {
+ currentCall = primary = callList.getOutgoingCall();
+ } else if (newState == InCallPresenter.InCallState.PENDING_OUTGOING) {
+ currentCall = primary = callList.getPendingOutgoingCall();
+ } else if (newState == InCallPresenter.InCallState.INCALL) {
+ currentCall = primary = callList.getActiveCall();
+ }
+
+ final boolean primaryChanged = !Objects.equals(mPrimaryCall, primary);
+ LogUtil.i(
+ "VideoCallPresenter.onStateChange",
+ "primaryChanged: %b, primary: %s, mPrimaryCall: %s",
+ primaryChanged,
+ primary,
+ mPrimaryCall);
+ if (primaryChanged) {
+ onPrimaryCallChanged(primary);
+ } else if (mPrimaryCall != null) {
+ updateVideoCall(primary);
+ }
+ updateCallCache(primary);
+
+ // If the call context changed, potentially exit fullscreen or schedule auto enter of
+ // fullscreen mode.
+ // If the current call context is no longer a video call, exit fullscreen mode.
+ maybeExitFullscreen(currentCall);
+ // Schedule auto-enter of fullscreen mode if the current call context is a video call
+ maybeAutoEnterFullscreen(currentCall);
+ }
+
+ /**
+ * Handles a change to the fullscreen mode of the app.
+ *
+ * @param isFullscreenMode {@code true} if the app is now fullscreen, {@code false} otherwise.
+ */
+ @Override
+ public void onFullscreenModeChanged(boolean isFullscreenMode) {
+ cancelAutoFullScreen();
+ if (mPrimaryCall != null) {
+ updateFullscreenAndGreenScreenMode(
+ mPrimaryCall.getState(), mPrimaryCall.getSessionModificationState());
+ } else {
+ updateFullscreenAndGreenScreenMode(
+ State.INVALID, DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ private void checkForVideoStateChange(DialerCall call) {
+ final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call);
+ final boolean hasVideoStateChanged = mCurrentVideoState != call.getVideoState();
+
+ LogUtil.v(
+ "VideoCallPresenter.checkForVideoStateChange",
+ "shouldShowVideoUi: %b, hasVideoStateChanged: %b, isVideoMode: %b, previousVideoState: %s,"
+ + " newVideoState: %s",
+ shouldShowVideoUi,
+ hasVideoStateChanged,
+ isVideoMode(),
+ VideoProfile.videoStateToString(mCurrentVideoState),
+ VideoProfile.videoStateToString(call.getVideoState()));
+ if (!hasVideoStateChanged) {
+ return;
+ }
+
+ updateCameraSelection(call);
+
+ if (shouldShowVideoUi) {
+ adjustVideoMode(call);
+ } else if (isVideoMode()) {
+ exitVideoMode();
+ }
+ }
+
+ private void checkForCallStateChange(DialerCall call) {
+ final boolean shouldShowVideoUi = shouldShowVideoUiForCall(call);
+ final boolean hasCallStateChanged =
+ mCurrentCallState != call.getState() || mIsRemotelyHeld != call.isRemotelyHeld();
+ mIsRemotelyHeld = call.isRemotelyHeld();
+
+ LogUtil.v(
+ "VideoCallPresenter.checkForCallStateChange",
+ "shouldShowVideoUi: %b, hasCallStateChanged: %b, isVideoMode: %b",
+ shouldShowVideoUi,
+ hasCallStateChanged,
+ isVideoMode());
+
+ if (!hasCallStateChanged) {
+ return;
+ }
+
+ if (shouldShowVideoUi) {
+ final InCallCameraManager cameraManager =
+ InCallPresenter.getInstance().getInCallCameraManager();
+
+ String prevCameraId = cameraManager.getActiveCameraId();
+ updateCameraSelection(call);
+ String newCameraId = cameraManager.getActiveCameraId();
+
+ if (!Objects.equals(prevCameraId, newCameraId) && VideoUtils.isActiveVideoCall(call)) {
+ enableCamera(call.getVideoCall(), true);
+ }
+ }
+
+ // Make sure we hide or show the video UI if needed.
+ showVideoUi(
+ call.getVideoState(),
+ call.getState(),
+ call.getSessionModificationState(),
+ call.isRemotelyHeld());
+ }
+
+ private void onPrimaryCallChanged(DialerCall newPrimaryCall) {
+ final boolean shouldShowVideoUi = shouldShowVideoUiForCall(newPrimaryCall);
+ final boolean isVideoMode = isVideoMode();
+
+ LogUtil.v(
+ "VideoCallPresenter.onPrimaryCallChanged",
+ "shouldShowVideoUi: %b, isVideoMode: %b",
+ shouldShowVideoUi,
+ isVideoMode);
+
+ if (!shouldShowVideoUi && isVideoMode) {
+ // Terminate video mode if new primary call is not a video call
+ // and we are currently in video mode.
+ LogUtil.i("VideoCallPresenter.onPrimaryCallChanged", "exiting video mode...");
+ exitVideoMode();
+ } else if (shouldShowVideoUi) {
+ LogUtil.i("VideoCallPresenter.onPrimaryCallChanged", "entering video mode...");
+
+ updateCameraSelection(newPrimaryCall);
+ adjustVideoMode(newPrimaryCall);
+ }
+ checkForOrientationAllowedChange(newPrimaryCall);
+ }
+
+ private boolean isVideoMode() {
+ return mIsVideoMode;
+ }
+
+ private void updateCallCache(DialerCall call) {
+ if (call == null) {
+ mCurrentVideoState = VideoProfile.STATE_AUDIO_ONLY;
+ mCurrentCallState = DialerCall.State.INVALID;
+ mVideoCall = null;
+ mPrimaryCall = null;
+ } else {
+ mCurrentVideoState = call.getVideoState();
+ mVideoCall = call.getVideoCall();
+ mCurrentCallState = call.getState();
+ mPrimaryCall = call;
+ }
+ }
+
+ /**
+ * Handles changes to the details of the call. The {@link VideoCallPresenter} is interested in
+ * changes to the video state.
+ *
+ * @param call The call for which the details changed.
+ * @param details The new call details.
+ */
+ @Override
+ public void onDetailsChanged(DialerCall call, android.telecom.Call.Details details) {
+ LogUtil.v(
+ "VideoCallPresenter.onDetailsChanged",
+ "call: %s, details: %s, mPrimaryCall: %s",
+ call,
+ details,
+ mPrimaryCall);
+ if (call == null) {
+ return;
+ }
+ // If the details change is not for the currently active call no update is required.
+ if (!call.equals(mPrimaryCall)) {
+ LogUtil.v("VideoCallPresenter.onDetailsChanged", "details not for current active call");
+ return;
+ }
+
+ updateVideoCall(call);
+
+ updateCallCache(call);
+ }
+
+ private void updateVideoCall(DialerCall call) {
+ checkForVideoCallChange(call);
+ checkForVideoStateChange(call);
+ checkForCallStateChange(call);
+ checkForOrientationAllowedChange(call);
+ updateFullscreenAndGreenScreenMode(call.getState(), call.getSessionModificationState());
+ }
+
+ private void checkForOrientationAllowedChange(@Nullable DialerCall call) {
+ InCallPresenter.getInstance()
+ .setInCallAllowsOrientationChange(VideoUtils.isVideoCall(call) || isVideoUpgrade(call));
+ }
+
+ private void updateFullscreenAndGreenScreenMode(
+ int callState, @SessionModificationState int sessionModificationState) {
+ if (mVideoCallScreen != null) {
+ boolean shouldShowFullscreen = InCallPresenter.getInstance().isFullscreen();
+ boolean shouldShowGreenScreen =
+ callState == State.DIALING
+ || callState == State.CONNECTING
+ || callState == State.INCOMING
+ || isVideoUpgrade(sessionModificationState);
+ mVideoCallScreen.updateFullscreenAndGreenScreenMode(
+ shouldShowFullscreen, shouldShowGreenScreen);
+ }
+ }
+
+ /** Checks for a change to the video call and changes it if required. */
+ private void checkForVideoCallChange(DialerCall call) {
+ final VideoCall videoCall = call.getVideoCall();
+ LogUtil.v(
+ "VideoCallPresenter.checkForVideoCallChange",
+ "videoCall: %s, mVideoCall: %s",
+ videoCall,
+ mVideoCall);
+ if (!Objects.equals(videoCall, mVideoCall)) {
+ changeVideoCall(call);
+ }
+ }
+
+ /**
+ * Handles a change to the video call. Sets the surfaces on the previous call to null and sets the
+ * surfaces on the new video call accordingly.
+ *
+ * @param call The new video call.
+ */
+ private void changeVideoCall(DialerCall call) {
+ final VideoCall videoCall = call == null ? null : call.getVideoCall();
+ LogUtil.i(
+ "VideoCallPresenter.changeVideoCall",
+ "videoCall: %s, mVideoCall: %s",
+ videoCall,
+ mVideoCall);
+ final boolean hasChanged = mVideoCall == null && videoCall != null;
+
+ mVideoCall = videoCall;
+ if (mVideoCall == null) {
+ LogUtil.v("VideoCallPresenter.changeVideoCall", "video call or primary call is null. Return");
+ return;
+ }
+
+ if (shouldShowVideoUiForCall(call) && hasChanged) {
+ adjustVideoMode(call);
+ }
+ }
+
+ private boolean isCameraRequired() {
+ return mPrimaryCall != null
+ && isCameraRequired(
+ mPrimaryCall.getVideoState(), mPrimaryCall.getSessionModificationState());
+ }
+
+ /**
+ * Adjusts the current video mode by setting up the preview and display surfaces as necessary.
+ * Expected to be called whenever the video state associated with a call changes (e.g. a user
+ * turns their camera on or off) to ensure the correct surfaces are shown/hidden. TODO: Need
+ * to adjust size and orientation of preview surface here.
+ */
+ private void adjustVideoMode(DialerCall call) {
+ VideoCall videoCall = call.getVideoCall();
+ int newVideoState = call.getVideoState();
+
+ LogUtil.i(
+ "VideoCallPresenter.adjustVideoMode",
+ "videoCall: %s, videoState: %d",
+ videoCall,
+ newVideoState);
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.adjustVideoMode", "error VideoCallScreen is null so returning");
+ return;
+ }
+
+ showVideoUi(
+ newVideoState, call.getState(), call.getSessionModificationState(), call.isRemotelyHeld());
+
+ // Communicate the current camera to telephony and make a request for the camera
+ // capabilities.
+ if (videoCall != null) {
+ Surface surface = getRemoteVideoSurfaceTexture().getSavedSurface();
+ if (surface != null) {
+ LogUtil.v(
+ "VideoCallPresenter.adjustVideoMode", "calling setDisplaySurface with: " + surface);
+ videoCall.setDisplaySurface(surface);
+ }
+
+ Assert.checkState(
+ mDeviceOrientation != InCallOrientationEventListener.SCREEN_ORIENTATION_UNKNOWN);
+ videoCall.setDeviceOrientation(mDeviceOrientation);
+ enableCamera(videoCall, isCameraRequired(newVideoState, call.getSessionModificationState()));
+ }
+ int previousVideoState = mCurrentVideoState;
+ mCurrentVideoState = newVideoState;
+ mIsVideoMode = true;
+
+ // adjustVideoMode may be called if we are already in a 1-way video state. In this case
+ // we do not want to trigger auto-fullscreen mode.
+ if (!VideoUtils.isVideoCall(previousVideoState) && VideoUtils.isVideoCall(newVideoState)) {
+ maybeAutoEnterFullscreen(call);
+ }
+ }
+
+ private static boolean shouldShowVideoUiForCall(@Nullable DialerCall call) {
+ if (call == null) {
+ return false;
+ }
+
+ if (VideoUtils.isVideoCall(call)) {
+ return true;
+ }
+
+ if (isVideoUpgrade(call)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private void enableCamera(VideoCall videoCall, boolean isCameraRequired) {
+ LogUtil.v(
+ "VideoCallPresenter.enableCamera",
+ "videoCall: %s, enabling: %b",
+ videoCall,
+ isCameraRequired);
+ if (videoCall == null) {
+ LogUtil.i("VideoCallPresenter.enableCamera", "videoCall is null.");
+ return;
+ }
+
+ boolean hasCameraPermission = VideoUtils.hasCameraPermissionAndAllowedByUser(mContext);
+ if (!hasCameraPermission) {
+ videoCall.setCamera(null);
+ mPreviewSurfaceState = PreviewSurfaceState.NONE;
+ // TODO: Inform remote party that the video is off. This is similar to b/30256571.
+ } else if (isCameraRequired) {
+ InCallCameraManager cameraManager = InCallPresenter.getInstance().getInCallCameraManager();
+ videoCall.setCamera(cameraManager.getActiveCameraId());
+ mPreviewSurfaceState = PreviewSurfaceState.CAMERA_SET;
+ videoCall.requestCameraCapabilities();
+ } else {
+ mPreviewSurfaceState = PreviewSurfaceState.NONE;
+ videoCall.setCamera(null);
+ }
+ }
+
+ /** Exits video mode by hiding the video surfaces and making other adjustments (eg. audio). */
+ private void exitVideoMode() {
+ LogUtil.i("VideoCallPresenter.exitVideoMode", "");
+
+ showVideoUi(
+ VideoProfile.STATE_AUDIO_ONLY,
+ DialerCall.State.ACTIVE,
+ DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ false /* isRemotelyHeld */);
+ enableCamera(mVideoCall, false);
+ InCallPresenter.getInstance().setFullScreen(false);
+
+ mIsVideoMode = false;
+ }
+
+ /**
+ * Based on the current video state and call state, show or hide the incoming and outgoing video
+ * surfaces. The outgoing video surface is shown any time video is transmitting. The incoming
+ * video surface is shown whenever the video is un-paused and active.
+ *
+ * @param videoState The video state.
+ * @param callState The call state.
+ */
+ private void showVideoUi(
+ int videoState,
+ int callState,
+ @SessionModificationState int sessionModificationState,
+ boolean isRemotelyHeld) {
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.showVideoUi", "videoCallScreen is null returning");
+ return;
+ }
+ boolean showIncomingVideo = showIncomingVideo(videoState, callState);
+ boolean showOutgoingVideo = showOutgoingVideo(mContext, videoState, sessionModificationState);
+ LogUtil.i(
+ "VideoCallPresenter.showVideoUi",
+ "showIncoming: %b, showOutgoing: %b, isRemotelyHeld: %b",
+ showIncomingVideo,
+ showOutgoingVideo,
+ isRemotelyHeld);
+ updateRemoteVideoSurfaceDimensions();
+ mVideoCallScreen.showVideoViews(showOutgoingVideo, showIncomingVideo, isRemotelyHeld);
+
+ InCallPresenter.getInstance().enableScreenTimeout(VideoProfile.isAudioOnly(videoState));
+ updateFullscreenAndGreenScreenMode(callState, sessionModificationState);
+ }
+
+ /**
+ * Handles peer video pause state changes.
+ *
+ * @param call The call which paused or un-pausedvideo transmission.
+ * @param paused {@code True} when the video transmission is paused, {@code false} when video
+ * transmission resumes.
+ */
+ @Override
+ public void onPeerPauseStateChanged(DialerCall call, boolean paused) {
+ if (!call.equals(mPrimaryCall)) {
+ return;
+ }
+ }
+
+ /**
+ * Handles peer video dimension changes.
+ *
+ * @param call The call which experienced a peer video dimension change.
+ * @param width The new peer video width .
+ * @param height The new peer video height.
+ */
+ @Override
+ public void onUpdatePeerDimensions(DialerCall call, int width, int height) {
+ LogUtil.i("VideoCallPresenter.onUpdatePeerDimensions", "width: %d, height: %d", width, height);
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.onUpdatePeerDimensions", "videoCallScreen is null");
+ return;
+ }
+ if (!call.equals(mPrimaryCall)) {
+ LogUtil.e(
+ "VideoCallPresenter.onUpdatePeerDimensions", "current call is not equal to primary");
+ return;
+ }
+
+ // Change size of display surface to match the peer aspect ratio
+ if (width > 0 && height > 0 && mVideoCallScreen != null) {
+ getRemoteVideoSurfaceTexture().setSourceVideoDimensions(new Point(width, height));
+ mVideoCallScreen.onRemoteVideoDimensionsChanged();
+ }
+ }
+
+ /**
+ * Handles any video quality changes in the call.
+ *
+ * @param call The call which experienced a video quality change.
+ * @param videoQuality The new video call quality.
+ */
+ @Override
+ public void onVideoQualityChanged(DialerCall call, int videoQuality) {
+ // No-op
+ }
+
+ /**
+ * Handles a change to the dimensions of the local camera. Receiving the camera capabilities
+ * triggers the creation of the video
+ *
+ * @param call The call which experienced the camera dimension change.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ @Override
+ public void onCameraDimensionsChange(DialerCall call, int width, int height) {
+ LogUtil.i(
+ "VideoCallPresenter.onCameraDimensionsChange",
+ "call: %s, width: %d, height: %d",
+ call,
+ width,
+ height);
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "ui is null");
+ return;
+ }
+
+ if (!call.equals(mPrimaryCall)) {
+ LogUtil.e("VideoCallPresenter.onCameraDimensionsChange", "not the primary call");
+ return;
+ }
+
+ mPreviewSurfaceState = PreviewSurfaceState.CAPABILITIES_RECEIVED;
+ changePreviewDimensions(width, height);
+
+ // Check if the preview surface is ready yet; if it is, set it on the {@code VideoCall}.
+ // If it not yet ready, it will be set when when creation completes.
+ Surface surface = getLocalVideoSurfaceTexture().getSavedSurface();
+ if (surface != null) {
+ mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
+ mVideoCall.setPreviewSurface(surface);
+ }
+ }
+
+ /**
+ * Changes the dimensions of the preview surface.
+ *
+ * @param width The new width.
+ * @param height The new height.
+ */
+ private void changePreviewDimensions(int width, int height) {
+ if (mVideoCallScreen == null) {
+ return;
+ }
+
+ // Resize the surface used to display the preview video
+ getLocalVideoSurfaceTexture().setSurfaceDimensions(new Point(width, height));
+ mVideoCallScreen.onLocalVideoDimensionsChanged();
+ }
+
+ /**
+ * Called when call session event is raised.
+ *
+ * @param event The call session event.
+ */
+ @Override
+ public void onCallSessionEvent(int event) {
+ switch (event) {
+ case Connection.VideoProvider.SESSION_EVENT_RX_PAUSE:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_pause");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_RX_RESUME:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "rx_resume");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_failure");
+ break;
+ case Connection.VideoProvider.SESSION_EVENT_CAMERA_READY:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "camera_ready");
+ break;
+ default:
+ LogUtil.v("VideoCallPresenter.onCallSessionEvent", "unknown event = : " + event);
+ break;
+ }
+ }
+
+ /**
+ * Handles a change to the call data usage
+ *
+ * @param dataUsage call data usage value
+ */
+ @Override
+ public void onCallDataUsageChange(long dataUsage) {
+ LogUtil.v("VideoCallPresenter.onCallDataUsageChange", "dataUsage=" + dataUsage);
+ }
+
+ /**
+ * Handles changes to the device orientation.
+ *
+ * @param orientation The screen orientation of the device (one of: {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_0}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_90}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_180}, {@link
+ * InCallOrientationEventListener#SCREEN_ORIENTATION_270}).
+ */
+ @Override
+ public void onDeviceOrientationChanged(int orientation) {
+ LogUtil.i(
+ "VideoCallPresenter.onDeviceOrientationChanged",
+ "orientation: %d -> %d",
+ mDeviceOrientation,
+ orientation);
+ mDeviceOrientation = orientation;
+
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.onDeviceOrientationChanged", "videoCallScreen is null");
+ return;
+ }
+
+ Point previewDimensions = getLocalVideoSurfaceTexture().getSurfaceDimensions();
+ if (previewDimensions == null) {
+ return;
+ }
+ LogUtil.v(
+ "VideoCallPresenter.onDeviceOrientationChanged",
+ "orientation: %d, size: %s",
+ orientation,
+ previewDimensions);
+ changePreviewDimensions(previewDimensions.x, previewDimensions.y);
+
+ mVideoCallScreen.onLocalVideoOrientationChanged();
+ }
+
+ /**
+ * Exits fullscreen mode if the current call context has changed to a non-video call.
+ *
+ * @param call The call.
+ */
+ protected void maybeExitFullscreen(DialerCall call) {
+ if (call == null) {
+ return;
+ }
+
+ if (!VideoUtils.isVideoCall(call) || call.getState() == DialerCall.State.INCOMING) {
+ LogUtil.i("VideoCallPresenter.maybeExitFullscreen", "exiting fullscreen");
+ InCallPresenter.getInstance().setFullScreen(false);
+ }
+ }
+
+ /**
+ * Schedules auto-entering of fullscreen mode. Will not enter full screen mode if any of the
+ * following conditions are met: 1. No call 2. DialerCall is not active 3. The current video state
+ * is not bi-directional. 4. Already in fullscreen mode 5. In accessibility mode
+ *
+ * @param call The current call.
+ */
+ protected void maybeAutoEnterFullscreen(DialerCall call) {
+ if (!mIsAutoFullscreenEnabled) {
+ return;
+ }
+
+ if (call == null
+ || call.getState() != DialerCall.State.ACTIVE
+ || !VideoUtils.isBidirectionalVideoCall(call)
+ || InCallPresenter.getInstance().isFullscreen()
+ || (mContext != null && AccessibilityUtil.isTouchExplorationEnabled(mContext))) {
+ // Ensure any previously scheduled attempt to enter fullscreen is cancelled.
+ cancelAutoFullScreen();
+ return;
+ }
+
+ if (mAutoFullScreenPending) {
+ LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "already pending.");
+ return;
+ }
+ LogUtil.v("VideoCallPresenter.maybeAutoEnterFullscreen", "scheduled");
+ mAutoFullScreenPending = true;
+ mHandler.removeCallbacks(mAutoFullscreenRunnable);
+ mHandler.postDelayed(mAutoFullscreenRunnable, mAutoFullscreenTimeoutMillis);
+ }
+
+ /** Cancels pending auto fullscreen mode. */
+ @Override
+ public void cancelAutoFullScreen() {
+ if (!mAutoFullScreenPending) {
+ LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "none pending.");
+ return;
+ }
+ LogUtil.v("VideoCallPresenter.cancelAutoFullScreen", "cancelling pending");
+ mAutoFullScreenPending = false;
+ mHandler.removeCallbacks(mAutoFullscreenRunnable);
+ }
+
+ private void updateRemoteVideoSurfaceDimensions() {
+ Activity activity = mVideoCallScreen.getVideoCallScreenFragment().getActivity();
+ if (activity != null) {
+ Point screenSize = new Point();
+ activity.getWindowManager().getDefaultDisplay().getSize(screenSize);
+ getRemoteVideoSurfaceTexture().setSurfaceDimensions(screenSize);
+ }
+ }
+
+ private static boolean isVideoUpgrade(DialerCall call) {
+ return VideoUtils.hasSentVideoUpgradeRequest(call)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(call);
+ }
+
+ private static boolean isVideoUpgrade(@SessionModificationState int state) {
+ return VideoUtils.hasSentVideoUpgradeRequest(state)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(state);
+ }
+
+ private class LocalDelegate implements VideoSurfaceDelegate {
+ @Override
+ public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no UI");
+ return;
+ }
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceCreated", "no video call");
+ return;
+ }
+
+ // If the preview surface has just been created and we have already received camera
+ // capabilities, but not yet set the surface, we will set the surface now.
+ if (mPreviewSurfaceState == PreviewSurfaceState.CAPABILITIES_RECEIVED) {
+ mPreviewSurfaceState = PreviewSurfaceState.SURFACE_SET;
+ mVideoCall.setPreviewSurface(videoCallSurface.getSavedSurface());
+ } else if (mPreviewSurfaceState == PreviewSurfaceState.NONE && isCameraRequired()) {
+ enableCamera(mVideoCall, true);
+ }
+ }
+
+ @Override
+ public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceReleased", "no video call");
+ return;
+ }
+
+ mVideoCall.setPreviewSurface(null);
+ enableCamera(mVideoCall, false);
+ }
+
+ @Override
+ public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.LocalDelegate.onSurfaceDestroyed", "no video call");
+ return;
+ }
+
+ boolean isChangingConfigurations = InCallPresenter.getInstance().isChangingConfigurations();
+ if (!isChangingConfigurations) {
+ enableCamera(mVideoCall, false);
+ } else {
+ LogUtil.i(
+ "VideoCallPresenter.LocalDelegate.onSurfaceDestroyed",
+ "activity is being destroyed due to configuration changes. Not closing the camera.");
+ }
+ }
+
+ @Override
+ public void onSurfaceClick(VideoSurfaceTexture videoCallSurface) {
+ VideoCallPresenter.this.onSurfaceClick();
+ }
+ }
+
+ private class RemoteDelegate implements VideoSurfaceDelegate {
+ @Override
+ public void onSurfaceCreated(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCallScreen == null) {
+ LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no UI");
+ return;
+ }
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceCreated", "no video call");
+ return;
+ }
+ mVideoCall.setDisplaySurface(videoCallSurface.getSavedSurface());
+ }
+
+ @Override
+ public void onSurfaceReleased(VideoSurfaceTexture videoCallSurface) {
+ if (mVideoCall == null) {
+ LogUtil.e("VideoCallPresenter.RemoteDelegate.onSurfaceReleased", "no video call");
+ return;
+ }
+ mVideoCall.setDisplaySurface(null);
+ }
+
+ @Override
+ public void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface) {}
+
+ @Override
+ public void onSurfaceClick(VideoSurfaceTexture videoCallSurface) {
+ VideoCallPresenter.this.onSurfaceClick();
+ }
+ }
+
+ /** Defines the state of the preview surface negotiation with the telephony layer. */
+ private static class PreviewSurfaceState {
+
+ /**
+ * The camera has not yet been set on the {@link VideoCall}; negotiation has not yet started.
+ */
+ private static final int NONE = 0;
+
+ /**
+ * The camera has been set on the {@link VideoCall}, but camera capabilities have not yet been
+ * received.
+ */
+ private static final int CAMERA_SET = 1;
+
+ /**
+ * The camera capabilties have been received from telephony, but the surface has not yet been
+ * set on the {@link VideoCall}.
+ */
+ private static final int CAPABILITIES_RECEIVED = 2;
+
+ /** The surface has been set on the {@link VideoCall}. */
+ private static final int SURFACE_SET = 3;
+ }
+}
diff --git a/java/com/android/incallui/VideoPauseController.java b/java/com/android/incallui/VideoPauseController.java
new file mode 100644
index 000000000..2b4357704
--- /dev/null
+++ b/java/com/android/incallui/VideoPauseController.java
@@ -0,0 +1,416 @@
+/*
+ * 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.incallui;
+
+import android.support.annotation.NonNull;
+import android.telecom.VideoProfile;
+import com.android.incallui.InCallPresenter.InCallState;
+import com.android.incallui.InCallPresenter.InCallStateListener;
+import com.android.incallui.InCallPresenter.IncomingCallListener;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import java.util.Objects;
+
+/**
+ * This class is responsible for generating video pause/resume requests when the InCall UI is sent
+ * to the background and subsequently brought back to the foreground.
+ */
+class VideoPauseController implements InCallStateListener, IncomingCallListener {
+
+ private static final String TAG = "VideoPauseController";
+ private static VideoPauseController sVideoPauseController;
+ private InCallPresenter mInCallPresenter;
+ /** The current call context, if applicable. */
+ private CallContext mPrimaryCallContext = null;
+ /**
+ * Tracks whether the application is in the background. {@code True} if the application is in the
+ * background, {@code false} otherwise.
+ */
+ private boolean mIsInBackground = false;
+
+ /**
+ * Singleton accessor for the {@link VideoPauseController}.
+ *
+ * @return Singleton instance of the {@link VideoPauseController}.
+ */
+ /*package*/
+ static synchronized VideoPauseController getInstance() {
+ if (sVideoPauseController == null) {
+ sVideoPauseController = new VideoPauseController();
+ }
+ return sVideoPauseController;
+ }
+
+ /**
+ * Determines if a given call is the same one stored in a {@link CallContext}.
+ *
+ * @param call The call.
+ * @param callContext The call context.
+ * @return {@code true} if the {@link DialerCall} is the same as the one referenced in the {@link
+ * CallContext}.
+ */
+ private static boolean areSame(DialerCall call, CallContext callContext) {
+ if (call == null && callContext == null) {
+ return true;
+ } else if (call == null || callContext == null) {
+ return false;
+ }
+ return call.equals(callContext.getCall());
+ }
+
+ /**
+ * Determines if a video call can be paused. Only a video call which is active can be paused.
+ *
+ * @param callContext The call context to check.
+ * @return {@code true} if the call is an active video call.
+ */
+ private static boolean canVideoPause(CallContext callContext) {
+ return isVideoCall(callContext) && callContext.getState() == DialerCall.State.ACTIVE;
+ }
+
+ /**
+ * Determines if a call referenced by a {@link CallContext} is a video call.
+ *
+ * @param callContext The call context.
+ * @return {@code true} if the call is a video call, {@code false} otherwise.
+ */
+ private static boolean isVideoCall(CallContext callContext) {
+ return callContext != null && VideoUtils.isVideoCall(callContext.getVideoState());
+ }
+
+ /**
+ * Determines if call is in incoming/waiting state.
+ *
+ * @param call The call context.
+ * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
+ */
+ private static boolean isIncomingCall(CallContext call) {
+ return call != null && isIncomingCall(call.getCall());
+ }
+
+ /**
+ * Determines if a call is in incoming/waiting state.
+ *
+ * @param call The call.
+ * @return {@code true} if the call is in incoming or waiting state, {@code false} otherwise.
+ */
+ private static boolean isIncomingCall(DialerCall call) {
+ return call != null
+ && (call.getState() == DialerCall.State.CALL_WAITING
+ || call.getState() == DialerCall.State.INCOMING);
+ }
+
+ /**
+ * Determines if a call is dialing.
+ *
+ * @param call The call context.
+ * @return {@code true} if the call is dialing, {@code false} otherwise.
+ */
+ private static boolean isDialing(CallContext call) {
+ return call != null && DialerCall.State.isDialing(call.getState());
+ }
+
+ /**
+ * Configures the {@link VideoPauseController} to listen to call events. Configured via the {@link
+ * com.android.incallui.InCallPresenter}.
+ *
+ * @param inCallPresenter The {@link com.android.incallui.InCallPresenter}.
+ */
+ public void setUp(@NonNull InCallPresenter inCallPresenter) {
+ log("setUp");
+ mInCallPresenter = Objects.requireNonNull(inCallPresenter);
+ mInCallPresenter.addListener(this);
+ mInCallPresenter.addIncomingCallListener(this);
+ }
+
+ /**
+ * Cleans up the {@link VideoPauseController} by removing all listeners and clearing its internal
+ * state. Called from {@link com.android.incallui.InCallPresenter}.
+ */
+ public void tearDown() {
+ log("tearDown...");
+ mInCallPresenter.removeListener(this);
+ mInCallPresenter.removeIncomingCallListener(this);
+ clear();
+ }
+
+ /** Clears the internal state for the {@link VideoPauseController}. */
+ private void clear() {
+ mInCallPresenter = null;
+ mPrimaryCallContext = null;
+ mIsInBackground = false;
+ }
+
+ /**
+ * Handles changes in the {@link InCallState}. Triggers pause and resumption of video for the
+ * current foreground call.
+ *
+ * @param oldState The previous {@link InCallState}.
+ * @param newState The current {@link InCallState}.
+ * @param callList List of current call.
+ */
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ log("onStateChange, OldState=" + oldState + " NewState=" + newState);
+
+ DialerCall call;
+ if (newState == InCallState.INCOMING) {
+ call = callList.getIncomingCall();
+ } else if (newState == InCallState.WAITING_FOR_ACCOUNT) {
+ call = callList.getWaitingForAccountCall();
+ } else if (newState == InCallState.PENDING_OUTGOING) {
+ call = callList.getPendingOutgoingCall();
+ } else if (newState == InCallState.OUTGOING) {
+ call = callList.getOutgoingCall();
+ } else {
+ call = callList.getActiveCall();
+ }
+
+ boolean hasPrimaryCallChanged = !areSame(call, mPrimaryCallContext);
+ boolean canVideoPause = VideoUtils.canVideoPause(call);
+ log("onStateChange, hasPrimaryCallChanged=" + hasPrimaryCallChanged);
+ log("onStateChange, canVideoPause=" + canVideoPause);
+ log("onStateChange, IsInBackground=" + mIsInBackground);
+
+ if (hasPrimaryCallChanged) {
+ onPrimaryCallChanged(call);
+ return;
+ }
+
+ if (isDialing(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ // Bring UI to foreground if outgoing request becomes active while UI is in
+ // background.
+ bringToForeground();
+ } else if (!isVideoCall(mPrimaryCallContext) && canVideoPause && mIsInBackground) {
+ // Bring UI to foreground if VoLTE call becomes active while UI is in
+ // background.
+ bringToForeground();
+ }
+
+ updatePrimaryCallContext(call);
+ }
+
+ /**
+ * Handles a change to the primary call.
+ *
+ * <p>Reject incoming or hangup dialing call: Where the previous call was an incoming call or a
+ * call in dialing state, resume the new primary call. DialerCall swap: Where the new primary call
+ * is incoming, pause video on the previous primary call.
+ *
+ * @param call The new primary call.
+ */
+ private void onPrimaryCallChanged(DialerCall call) {
+ log("onPrimaryCallChanged: New call = " + call);
+ log("onPrimaryCallChanged: Old call = " + mPrimaryCallContext);
+ log("onPrimaryCallChanged, IsInBackground=" + mIsInBackground);
+
+ if (areSame(call, mPrimaryCallContext)) {
+ throw new IllegalStateException();
+ }
+ final boolean canVideoPause = VideoUtils.canVideoPause(call);
+
+ if ((isIncomingCall(mPrimaryCallContext)
+ || isDialing(mPrimaryCallContext)
+ || (call != null && VideoProfile.isPaused(call.getVideoState())))
+ && canVideoPause
+ && !mIsInBackground) {
+ // Send resume request for the active call, if user rejects incoming call, ends dialing
+ // call, or the call was previously in a paused state and UI is in the foreground.
+ sendRequest(call, true);
+ } else if (isIncomingCall(call) && canVideoPause(mPrimaryCallContext)) {
+ // Send pause request if there is an active video call, and we just received a new
+ // incoming call.
+ sendRequest(mPrimaryCallContext.getCall(), false);
+ }
+
+ updatePrimaryCallContext(call);
+ }
+
+ /**
+ * Handles new incoming calls by triggering a change in the primary call.
+ *
+ * @param oldState the old {@link InCallState}.
+ * @param newState the new {@link InCallState}.
+ * @param call the incoming call.
+ */
+ @Override
+ public void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call) {
+ log("onIncomingCall, OldState=" + oldState + " NewState=" + newState + " DialerCall=" + call);
+
+ if (areSame(call, mPrimaryCallContext)) {
+ return;
+ }
+
+ onPrimaryCallChanged(call);
+ }
+
+ /**
+ * Caches a reference to the primary call and stores its previous state.
+ *
+ * @param call The new primary call.
+ */
+ private void updatePrimaryCallContext(DialerCall call) {
+ if (call == null) {
+ mPrimaryCallContext = null;
+ } else if (mPrimaryCallContext != null) {
+ mPrimaryCallContext.update(call);
+ } else {
+ mPrimaryCallContext = new CallContext(call);
+ }
+ }
+
+ /**
+ * Called when UI goes in/out of the foreground.
+ *
+ * @param showing true if UI is in the foreground, false otherwise.
+ */
+ public void onUiShowing(boolean showing) {
+ if (mInCallPresenter == null) {
+ return;
+ }
+
+ final boolean isInCall = mInCallPresenter.getInCallState() == InCallState.INCALL;
+ if (showing) {
+ onResume(isInCall);
+ } else {
+ onPause(isInCall);
+ }
+ }
+
+ /**
+ * Called when UI is brought to the foreground. Sends a session modification request to resume the
+ * outgoing video.
+ *
+ * @param isInCall {@code true} if we are in an active call. A resume request is only sent to the
+ * video provider if we are in a call.
+ */
+ private void onResume(boolean isInCall) {
+ log("onResume");
+
+ mIsInBackground = false;
+ if (canVideoPause(mPrimaryCallContext) && isInCall) {
+ sendRequest(mPrimaryCallContext.getCall(), true);
+ } else {
+ log("onResume. Ignoring...");
+ }
+ }
+
+ /**
+ * Called when UI is sent to the background. Sends a session modification request to pause the
+ * outgoing video.
+ *
+ * @param isInCall {@code true} if we are in an active call. A pause request is only sent to the
+ * video provider if we are in a call.
+ */
+ private void onPause(boolean isInCall) {
+ log("onPause");
+
+ mIsInBackground = true;
+ if (canVideoPause(mPrimaryCallContext) && isInCall) {
+ sendRequest(mPrimaryCallContext.getCall(), false);
+ } else {
+ log("onPause, Ignoring...");
+ }
+ }
+
+ private void bringToForeground() {
+ if (mInCallPresenter != null) {
+ log("Bringing UI to foreground");
+ mInCallPresenter.bringToForeground(false);
+ } else {
+ loge("InCallPresenter is null. Cannot bring UI to foreground");
+ }
+ }
+
+ /**
+ * Sends Pause/Resume request.
+ *
+ * @param call DialerCall to be paused/resumed.
+ * @param resume If true resume request will be sent, otherwise pause request.
+ */
+ private void sendRequest(DialerCall call, boolean resume) {
+ // Check if this call supports pause/un-pause.
+ if (!call.can(android.telecom.Call.Details.CAPABILITY_CAN_PAUSE_VIDEO)) {
+ return;
+ }
+
+ if (resume) {
+ log("sending resume request, call=" + call);
+ call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoUnPauseProfile(call));
+ } else {
+ log("sending pause request, call=" + call);
+ call.getVideoCall().sendSessionModifyRequest(VideoUtils.makeVideoPauseProfile(call));
+ }
+ }
+
+ /**
+ * Logs a debug message.
+ *
+ * @param msg The message.
+ */
+ private void log(String msg) {
+ Log.d(this, TAG + msg);
+ }
+
+ /**
+ * Logs an error message.
+ *
+ * @param msg The message.
+ */
+ private void loge(String msg) {
+ Log.e(this, TAG + msg);
+ }
+
+ /** Keeps track of the current active/foreground call. */
+ private static class CallContext {
+
+ private int mState = State.INVALID;
+ private int mVideoState;
+ private DialerCall mCall;
+
+ public CallContext(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ update(call);
+ }
+
+ public void update(@NonNull DialerCall call) {
+ mCall = Objects.requireNonNull(call);
+ mState = call.getState();
+ mVideoState = call.getVideoState();
+ }
+
+ public int getState() {
+ return mState;
+ }
+
+ public int getVideoState() {
+ return mVideoState;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "CallContext {CallId=%s, State=%s, VideoState=%d}", mCall.getId(), mState, mVideoState);
+ }
+
+ public DialerCall getCall() {
+ return mCall;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/bindings/AnswerBindings.java b/java/com/android/incallui/answer/bindings/AnswerBindings.java
new file mode 100644
index 000000000..f7a7a0a95
--- /dev/null
+++ b/java/com/android/incallui/answer/bindings/AnswerBindings.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.incallui.answer.bindings;
+
+import com.android.incallui.answer.impl.AnswerFragment;
+import com.android.incallui.answer.protocol.AnswerScreen;
+
+/** Bindings for answer module. */
+public class AnswerBindings {
+
+ public static AnswerScreen createAnswerScreen(
+ String callId, int videoState, boolean isVideoUpgradeRequest) {
+ return AnswerFragment.newInstance(callId, videoState, isVideoUpgradeRequest);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/AffordanceHolderLayout.java b/java/com/android/incallui/answer/impl/AffordanceHolderLayout.java
new file mode 100644
index 000000000..0f93abe68
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AffordanceHolderLayout.java
@@ -0,0 +1,178 @@
+/*
+ * 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.incallui.answer.impl;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+import com.android.incallui.answer.impl.affordance.SwipeButtonHelper;
+import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback;
+import com.android.incallui.answer.impl.affordance.SwipeButtonView;
+import com.android.incallui.util.AccessibilityUtil;
+
+/** Layout that delegates touches to its SwipeButtonHelper */
+public class AffordanceHolderLayout extends FrameLayout {
+
+ private SwipeButtonHelper affordanceHelper;
+
+ private Callback affordanceCallback;
+
+ public AffordanceHolderLayout(Context context) {
+ this(context, null);
+ }
+
+ public AffordanceHolderLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public AffordanceHolderLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ affordanceHelper =
+ new SwipeButtonHelper(
+ new Callback() {
+ @Override
+ public void onAnimationToSideStarted(
+ boolean rightPage, float translation, float vel) {
+ if (affordanceCallback != null) {
+ affordanceCallback.onAnimationToSideStarted(rightPage, translation, vel);
+ }
+ }
+
+ @Override
+ public void onAnimationToSideEnded() {
+ if (affordanceCallback != null) {
+ affordanceCallback.onAnimationToSideEnded();
+ }
+ }
+
+ @Override
+ public float getMaxTranslationDistance() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getMaxTranslationDistance();
+ }
+ return 0;
+ }
+
+ @Override
+ public void onSwipingStarted(boolean rightIcon) {
+ if (affordanceCallback != null) {
+ affordanceCallback.onSwipingStarted(rightIcon);
+ }
+ }
+
+ @Override
+ public void onSwipingAborted() {
+ if (affordanceCallback != null) {
+ affordanceCallback.onSwipingAborted();
+ }
+ }
+
+ @Override
+ public void onIconClicked(boolean rightIcon) {
+ if (affordanceCallback != null) {
+ affordanceCallback.onIconClicked(rightIcon);
+ }
+ }
+
+ @Nullable
+ @Override
+ public SwipeButtonView getLeftIcon() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getLeftIcon();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public SwipeButtonView getRightIcon() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getRightIcon();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public View getLeftPreview() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getLeftPreview();
+ }
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public View getRightPreview() {
+ if (affordanceCallback != null) {
+ affordanceCallback.getRightPreview();
+ }
+ return null;
+ }
+
+ @Override
+ public float getAffordanceFalsingFactor() {
+ if (affordanceCallback != null) {
+ return affordanceCallback.getAffordanceFalsingFactor();
+ }
+ return 1.0f;
+ }
+ },
+ context);
+ }
+
+ public void setAffordanceCallback(@Nullable Callback callback) {
+ affordanceCallback = callback;
+ affordanceHelper.init();
+ }
+
+ public void startHintAnimation(boolean rightIcon, @Nullable Runnable onFinishListener) {
+ affordanceHelper.startHintAnimation(rightIcon, onFinishListener);
+ }
+
+ public void animateHideLeftRightIcon() {
+ affordanceHelper.animateHideLeftRightIcon();
+ }
+
+ public void reset(boolean animate) {
+ affordanceHelper.reset(animate);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
+ return false;
+ }
+ return affordanceHelper.onTouchEvent(event) || super.onInterceptTouchEvent(event);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return affordanceHelper.onTouchEvent(event) || super.onTouchEvent(event);
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ affordanceHelper.onConfigurationChanged();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/AndroidManifest.xml b/java/com/android/incallui/answer/impl/AndroidManifest.xml
new file mode 100644
index 000000000..482c716db
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.answer.impl">
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/AnswerFragment.java b/java/com/android/incallui/answer/impl/AnswerFragment.java
new file mode 100644
index 000000000..98439ee7f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AnswerFragment.java
@@ -0,0 +1,981 @@
+/*
+ * 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.incallui.answer.impl;
+
+import android.Manifest.permission;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.StringRes;
+import android.support.annotation.VisibleForTesting;
+import android.support.transition.TransitionManager;
+import android.support.v4.app.Fragment;
+import android.telecom.VideoProfile;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.widget.ImageView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.dialer.util.ViewUtil;
+import com.android.incallui.answer.impl.CreateCustomSmsDialogFragment.CreateCustomSmsHolder;
+import com.android.incallui.answer.impl.SmsBottomSheetFragment.SmsSheetHolder;
+import com.android.incallui.answer.impl.affordance.SwipeButtonHelper.Callback;
+import com.android.incallui.answer.impl.affordance.SwipeButtonView;
+import com.android.incallui.answer.impl.answermethod.AnswerMethod;
+import com.android.incallui.answer.impl.answermethod.AnswerMethodFactory;
+import com.android.incallui.answer.impl.answermethod.AnswerMethodHolder;
+import com.android.incallui.answer.impl.utils.Interpolators;
+import com.android.incallui.answer.protocol.AnswerScreen;
+import com.android.incallui.answer.protocol.AnswerScreenDelegate;
+import com.android.incallui.answer.protocol.AnswerScreenDelegateFactory;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.sessiondata.AvatarPresenter;
+import com.android.incallui.sessiondata.MultimediaFragment;
+import com.android.incallui.util.AccessibilityUtil;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/** The new version of the incoming call screen. */
+@SuppressLint("ClickableViewAccessibility")
+public class AnswerFragment extends Fragment
+ implements AnswerScreen,
+ InCallScreen,
+ SmsSheetHolder,
+ CreateCustomSmsHolder,
+ AnswerMethodHolder,
+ MultimediaFragment.Holder {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_CALL_ID = "call_id";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_VIDEO_STATE = "video_state";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String ARG_IS_VIDEO_UPGRADE_REQUEST = "is_video_upgrade_request";
+
+ private static final String STATE_HAS_ANIMATED_ENTRY = "hasAnimated";
+
+ private static final int HINT_SECONDARY_SHOW_DURATION_MILLIS = 5000;
+ private static final float ANIMATE_LERP_PROGRESS = 0.5f;
+ private static final int STATUS_BAR_DISABLE_RECENT = 0x01000000;
+ private static final int STATUS_BAR_DISABLE_HOME = 0x00200000;
+ private static final int STATUS_BAR_DISABLE_BACK = 0x00400000;
+
+ private static void fadeToward(View view, float newAlpha) {
+ view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, ANIMATE_LERP_PROGRESS));
+ }
+
+ private static void scaleToward(View view, float newScale) {
+ view.setScaleX(MathUtil.lerp(view.getScaleX(), newScale, ANIMATE_LERP_PROGRESS));
+ view.setScaleY(MathUtil.lerp(view.getScaleY(), newScale, ANIMATE_LERP_PROGRESS));
+ }
+
+ private AnswerScreenDelegate answerScreenDelegate;
+ private InCallScreenDelegate inCallScreenDelegate;
+
+ private View importanceBadge;
+ private SwipeButtonView secondaryButton;
+ private AffordanceHolderLayout affordanceHolderLayout;
+ // Use these flags to prevent user from clicking accept/reject buttons multiple times.
+ // We use separate flags because in some rare cases accepting a call may fail to join the room,
+ // and then user is stuck in the incoming call view until it times out. Two flags at least give
+ // the user a chance to get out of the CallActivity.
+ private boolean buttonAcceptClicked;
+ private boolean buttonRejectClicked;
+ private boolean hasAnimatedEntry;
+ private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo();
+ private PrimaryCallState primaryCallState;
+ private ArrayList<CharSequence> textResponses;
+ private SmsBottomSheetFragment textResponsesFragment;
+ private CreateCustomSmsDialogFragment createCustomSmsDialogFragment;
+ private SecondaryBehavior secondaryBehavior = SecondaryBehavior.REJECT_WITH_SMS;
+ private ContactGridManager contactGridManager;
+ private AnswerVideoCallScreen answerVideoCallScreen;
+ private Handler handler = new Handler(Looper.getMainLooper());
+
+ private enum SecondaryBehavior {
+ REJECT_WITH_SMS(
+ R.drawable.quantum_ic_message_white_24,
+ R.string.a11y_description_incoming_call_reject_with_sms,
+ R.string.a11y_incoming_call_reject_with_sms,
+ R.string.call_incoming_swipe_to_decline_with_message) {
+ @Override
+ public void performAction(AnswerFragment fragment) {
+ fragment.showMessageMenu();
+ }
+ },
+
+ ANSWER_VIDEO_AS_AUDIO(
+ R.drawable.quantum_ic_videocam_off_white_24,
+ R.string.a11y_description_incoming_call_answer_video_as_audio,
+ R.string.a11y_incoming_call_answer_video_as_audio,
+ R.string.call_incoming_swipe_to_answer_video_as_audio) {
+ @Override
+ public void performAction(AnswerFragment fragment) {
+ fragment.acceptCallByUser(true /* answerVideoAsAudio */);
+ }
+ };
+
+ @DrawableRes public final int icon;
+ @StringRes public final int contentDescription;
+ @StringRes public final int accessibilityLabel;
+ @StringRes public final int hintText;
+
+ SecondaryBehavior(
+ @DrawableRes int icon,
+ @StringRes int contentDescription,
+ @StringRes int accessibilityLabel,
+ @StringRes int hintText) {
+ this.icon = icon;
+ this.contentDescription = contentDescription;
+ this.accessibilityLabel = accessibilityLabel;
+ this.hintText = hintText;
+ }
+
+ public abstract void performAction(AnswerFragment fragment);
+
+ public void applyToView(ImageView view) {
+ view.setImageResource(icon);
+ view.setContentDescription(view.getContext().getText(contentDescription));
+ }
+ }
+
+ private AccessibilityDelegate accessibilityDelegate =
+ new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ if (host == secondaryButton) {
+ CharSequence label = getText(secondaryBehavior.accessibilityLabel);
+ info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+ if (host == secondaryButton) {
+ performSecondaryButtonAction();
+ return true;
+ }
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ };
+
+ private Callback affordanceCallback =
+ new Callback() {
+ @Override
+ public void onAnimationToSideStarted(boolean rightPage, float translation, float vel) {}
+
+ @Override
+ public void onAnimationToSideEnded() {
+ performSecondaryButtonAction();
+ }
+
+ @Override
+ public float getMaxTranslationDistance() {
+ View view = getView();
+ if (view == null) {
+ return 0;
+ }
+ return (float) Math.hypot(view.getWidth(), view.getHeight());
+ }
+
+ @Override
+ public void onSwipingStarted(boolean rightIcon) {}
+
+ @Override
+ public void onSwipingAborted() {}
+
+ @Override
+ public void onIconClicked(boolean rightIcon) {
+ affordanceHolderLayout.startHintAnimation(rightIcon, null);
+ getAnswerMethod().setHintText(getText(secondaryBehavior.hintText));
+ handler.removeCallbacks(swipeHintRestoreTimer);
+ handler.postDelayed(swipeHintRestoreTimer, HINT_SECONDARY_SHOW_DURATION_MILLIS);
+ }
+
+ @Override
+ public SwipeButtonView getLeftIcon() {
+ return secondaryButton;
+ }
+
+ @Override
+ public SwipeButtonView getRightIcon() {
+ return null;
+ }
+
+ @Override
+ public View getLeftPreview() {
+ return null;
+ }
+
+ @Override
+ public View getRightPreview() {
+ return null;
+ }
+
+ @Override
+ public float getAffordanceFalsingFactor() {
+ return 1.0f;
+ }
+ };
+
+ private Runnable swipeHintRestoreTimer =
+ new Runnable() {
+ @Override
+ public void run() {
+ restoreSwipeHintTexts();
+ }
+ };
+
+ private void performSecondaryButtonAction() {
+ secondaryBehavior.performAction(this);
+ }
+
+ public static AnswerFragment newInstance(
+ String callId, int videoState, boolean isVideoUpgradeRequest) {
+ Bundle bundle = new Bundle();
+ bundle.putString(ARG_CALL_ID, Assert.isNotNull(callId));
+ bundle.putInt(ARG_VIDEO_STATE, videoState);
+ bundle.putBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST, isVideoUpgradeRequest);
+
+ AnswerFragment instance = new AnswerFragment();
+ instance.setArguments(bundle);
+ return instance;
+ }
+
+ @Override
+ @NonNull
+ public String getCallId() {
+ return Assert.isNotNull(getArguments().getString(ARG_CALL_ID));
+ }
+
+ @Override
+ public int getVideoState() {
+ return getArguments().getInt(ARG_VIDEO_STATE);
+ }
+
+ @Override
+ public boolean isVideoUpgradeRequest() {
+ return getArguments().getBoolean(ARG_IS_VIDEO_UPGRADE_REQUEST);
+ }
+
+ @Override
+ public void setTextResponses(List<String> textResponses) {
+ if (isVideoCall()) {
+ LogUtil.i("AnswerFragment.setTextResponses", "no-op for video calls");
+ } else if (textResponses == null) {
+ LogUtil.i("AnswerFragment.setTextResponses", "no text responses, hiding secondary button");
+ this.textResponses = null;
+ secondaryButton.setVisibility(View.INVISIBLE);
+ } else if (ActivityCompat.isInMultiWindowMode(getActivity())) {
+ LogUtil.i("AnswerFragment.setTextResponses", "in multiwindow, hiding secondary button");
+ this.textResponses = null;
+ secondaryButton.setVisibility(View.INVISIBLE);
+ } else {
+ LogUtil.i("AnswerFragment.setTextResponses", "textResponses.size: " + textResponses.size());
+ this.textResponses = new ArrayList<CharSequence>(textResponses);
+ secondaryButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ private void initSecondaryButton() {
+ secondaryBehavior =
+ isVideoCall() ? SecondaryBehavior.ANSWER_VIDEO_AS_AUDIO : SecondaryBehavior.REJECT_WITH_SMS;
+ secondaryBehavior.applyToView(secondaryButton);
+
+ secondaryButton.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ performSecondaryButtonAction();
+ }
+ });
+ secondaryButton.setClickable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
+ secondaryButton.setFocusable(AccessibilityUtil.isAccessibilityEnabled(getContext()));
+ secondaryButton.setAccessibilityDelegate(accessibilityDelegate);
+
+ if (isVideoCall()) {
+ //noinspection WrongConstant
+ if (!isVideoUpgradeRequest() && VideoProfile.isTransmissionEnabled(getVideoState())) {
+ secondaryButton.setVisibility(View.VISIBLE);
+ } else {
+ secondaryButton.setVisibility(View.INVISIBLE);
+ }
+ }
+ }
+
+ @Override
+ public boolean hasPendingDialogs() {
+ boolean hasPendingDialogs =
+ textResponsesFragment != null || createCustomSmsDialogFragment != null;
+ LogUtil.i("AnswerFragment.hasPendingDialogs", "" + hasPendingDialogs);
+ return hasPendingDialogs;
+ }
+
+ @Override
+ public void dismissPendingDialogs() {
+ LogUtil.i("AnswerFragment.dismissPendingDialogs", null);
+ if (textResponsesFragment != null) {
+ textResponsesFragment.dismiss();
+ textResponsesFragment = null;
+ }
+
+ if (createCustomSmsDialogFragment != null) {
+ createCustomSmsDialogFragment.dismiss();
+ createCustomSmsDialogFragment = null;
+ }
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ return fragment != null && fragment.isVisible();
+ }
+
+ @Override
+ public void showLocationUi(@Nullable Fragment locationUi) {
+ boolean isShowing = isShowingLocationUi();
+ if (!isShowing && locationUi != null) {
+ // Show the location fragment.
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.incall_location_holder, locationUi)
+ .commitAllowingStateLoss();
+ } else if (isShowing && locationUi == null) {
+ // Hide the location fragment
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+ }
+ }
+
+ @Override
+ public Fragment getAnswerScreenFragment() {
+ return this;
+ }
+
+ private AnswerMethod getAnswerMethod() {
+ return ((AnswerMethod)
+ getChildFragmentManager().findFragmentById(R.id.answer_method_container));
+ }
+
+ @Override
+ public void setPrimary(PrimaryInfo primaryInfo) {
+ LogUtil.i("AnswerFragment.setPrimary", primaryInfo.toString());
+ this.primaryInfo = primaryInfo;
+ updatePrimaryUI();
+ updateImportanceBadgeVisibility();
+ }
+
+ private void updatePrimaryUI() {
+ if (getView() == null) {
+ return;
+ }
+ contactGridManager.setPrimary(primaryInfo);
+ getAnswerMethod().setShowIncomingWillDisconnect(primaryInfo.answeringDisconnectsOngoingCall);
+ getAnswerMethod()
+ .setContactPhoto(
+ primaryInfo.photoType == ContactPhotoType.CONTACT ? primaryInfo.photo : null);
+ updateDataFragment();
+
+ if (primaryInfo.shouldShowLocation) {
+ // Hide the avatar to make room for location
+ contactGridManager.setAvatarHidden(true);
+ }
+ }
+
+ private void updateDataFragment() {
+ if (!isAdded()) {
+ return;
+ }
+ Fragment current = getChildFragmentManager().findFragmentById(R.id.incall_data_container);
+ Fragment newFragment = null;
+
+ MultimediaData multimediaData = getSessionData();
+ if (multimediaData != null
+ && (!TextUtils.isEmpty(multimediaData.getSubject())
+ || (multimediaData.getImageUri() != null)
+ || (multimediaData.getLocation() != null && canShowMap()))) {
+ // Need message fragment
+ String subject = multimediaData.getSubject();
+ Uri imageUri = multimediaData.getImageUri();
+ Location location = multimediaData.getLocation();
+ if (!(current instanceof MultimediaFragment)
+ || !Objects.equals(((MultimediaFragment) current).getSubject(), subject)
+ || !Objects.equals(((MultimediaFragment) current).getImageUri(), imageUri)
+ || !Objects.equals(((MultimediaFragment) current).getLocation(), location)) {
+ // Needs replacement
+ newFragment =
+ MultimediaFragment.newInstance(
+ multimediaData, false /* isInteractive */, true /* showAvatar */);
+ }
+ } else if (shouldShowAvatar()) {
+ // Needs Avatar
+ if (!(current instanceof AvatarFragment)) {
+ // Needs replacement
+ newFragment = new AvatarFragment();
+ }
+ } else {
+ // Needs empty
+ if (current != null) {
+ getChildFragmentManager().beginTransaction().remove(current).commitNow();
+ }
+ contactGridManager.setAvatarImageView(null, 0, false);
+ }
+
+ if (newFragment != null) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.incall_data_container, newFragment)
+ .commitNow();
+ }
+ }
+
+ private boolean shouldShowAvatar() {
+ return !isVideoCall();
+ }
+
+ private boolean canShowMap() {
+ return StaticMapBinding.get(getActivity().getApplication()) != null;
+ }
+
+ @Override
+ public void updateAvatar(AvatarPresenter avatarContainer) {
+ contactGridManager.setAvatarImageView(
+ avatarContainer.getAvatarImageView(),
+ avatarContainer.getAvatarSize(),
+ avatarContainer.shouldShowAnonymousAvatar());
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {}
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("AnswerFragment.setCallState", primaryCallState.toString());
+ this.primaryCallState = primaryCallState;
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {}
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {}
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ return false;
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ // Add prompt of how to accept/decline call with swipe gesture.
+ if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
+ event
+ .getText()
+ .add(getResources().getString(R.string.a11y_incoming_call_swipe_gesture_prompt));
+ }
+ }
+
+ @Override
+ public void showNoteSentToast() {}
+
+ @Override
+ public void updateInCallScreenColors() {}
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {}
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ Assert.fail();
+ return 0;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ Bundle arguments = getArguments();
+ Assert.checkState(arguments.containsKey(ARG_CALL_ID));
+ Assert.checkState(arguments.containsKey(ARG_VIDEO_STATE));
+ Assert.checkState(arguments.containsKey(ARG_IS_VIDEO_UPGRADE_REQUEST));
+
+ buttonAcceptClicked = false;
+ buttonRejectClicked = false;
+
+ View view = inflater.inflate(R.layout.fragment_incoming_call, container, false);
+ secondaryButton = (SwipeButtonView) view.findViewById(R.id.incoming_secondary_button);
+
+ affordanceHolderLayout = (AffordanceHolderLayout) view.findViewById(R.id.incoming_container);
+ affordanceHolderLayout.setAffordanceCallback(affordanceCallback);
+
+ importanceBadge = view.findViewById(R.id.incall_important_call_badge);
+ PillDrawable importanceBackground = new PillDrawable();
+ importanceBackground.setColor(getContext().getColor(android.R.color.white));
+ importanceBadge.setBackground(importanceBackground);
+ importanceBadge
+ .getViewTreeObserver()
+ .addOnGlobalLayoutListener(
+ new OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ int leftRightPadding = importanceBadge.getHeight() / 2;
+ importanceBadge.setPadding(
+ leftRightPadding,
+ importanceBadge.getPaddingTop(),
+ leftRightPadding,
+ importanceBadge.getPaddingBottom());
+ }
+ });
+ updateImportanceBadgeVisibility();
+
+ boolean isVideoCall = isVideoCall();
+ contactGridManager = new ContactGridManager(view, null, 0, false /* showAnonymousAvatar */);
+
+ Fragment answerMethod =
+ getChildFragmentManager().findFragmentById(R.id.answer_method_container);
+ if (AnswerMethodFactory.needsReplacement(answerMethod)) {
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(
+ R.id.answer_method_container, AnswerMethodFactory.createAnswerMethod(getActivity()))
+ .commitNow();
+ }
+
+ answerScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, AnswerScreenDelegateFactory.class)
+ .newAnswerScreenDelegate(this);
+
+ initSecondaryButton();
+
+ int flags = View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
+ if (!ActivityCompat.isInMultiWindowMode(getActivity())
+ && (getActivity().checkSelfPermission(permission.STATUS_BAR)
+ == PackageManager.PERMISSION_GRANTED)) {
+ LogUtil.i("AnswerFragment.onCreateView", "STATUS_BAR permission granted, disabling nav bar");
+ // These flags will suppress the alert that the activity is in full view mode
+ // during an incoming call on a fresh system/factory reset of the app
+ flags |= STATUS_BAR_DISABLE_BACK | STATUS_BAR_DISABLE_HOME | STATUS_BAR_DISABLE_RECENT;
+ }
+ view.setSystemUiVisibility(flags);
+ if (isVideoCall) {
+ if (VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ answerVideoCallScreen = new AnswerVideoCallScreen(this, view);
+ } else {
+ view.findViewById(R.id.videocall_video_off).setVisibility(View.VISIBLE);
+ }
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, InCallScreenDelegateFactory.class);
+ }
+
+ @Override
+ public void onViewCreated(final View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ createInCallScreenDelegate();
+ updateUI();
+
+ if (savedInstanceState == null || !savedInstanceState.getBoolean(STATE_HAS_ANIMATED_ENTRY)) {
+ ViewUtil.doOnPreDraw(view, false, this::animateEntry);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("AnswerFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ LogUtil.i("AnswerFragment.onStart", null);
+
+ updateUI();
+ if (answerVideoCallScreen != null) {
+ answerVideoCallScreen.onStart();
+ }
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ LogUtil.i("AnswerFragment.onStop", null);
+
+ handler.removeCallbacks(swipeHintRestoreTimer);
+ if (answerVideoCallScreen != null) {
+ answerVideoCallScreen.onStop();
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ LogUtil.i("AnswerFragment.onPause", null);
+ }
+
+ @Override
+ public void onDestroyView() {
+ LogUtil.i("AnswerFragment.onDestroyView", null);
+ if (answerVideoCallScreen != null) {
+ answerVideoCallScreen = null;
+ }
+ super.onDestroyView();
+ inCallScreenDelegate.onInCallScreenUnready();
+ answerScreenDelegate.onAnswerScreenUnready();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ bundle.putBoolean(STATE_HAS_ANIMATED_ENTRY, hasAnimatedEntry);
+ }
+
+ private void updateUI() {
+ if (getView() == null) {
+ return;
+ }
+
+ if (primaryInfo != null) {
+ updatePrimaryUI();
+ }
+ if (primaryCallState != null) {
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ restoreBackgroundMaskColor();
+ }
+
+ @Override
+ public boolean isVideoCall() {
+ return VideoUtils.isVideoCall(getVideoState());
+ }
+
+ @Override
+ public void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress) {
+ // Don't fade the window background for call waiting or video upgrades. Fading the background
+ // shows the system wallpaper which looks bad because on reject we switch to another call.
+ if (primaryCallState.state == State.INCOMING && !isVideoCall()) {
+ answerScreenDelegate.updateWindowBackgroundColor(answerProgress);
+ }
+
+ // Fade and scale contact name and video call text
+ float startDelay = .25f;
+ // Header progress is zero over positiveAdjustedProgress = [0, startDelay],
+ // linearly increases over (startDelay, 1] until reaching 1 when positiveAdjustedProgress = 1
+ float headerProgress = Math.max(0, (Math.abs(answerProgress) - 1) / (1 - startDelay) + 1);
+ fadeToward(contactGridManager.getContainerView(), 1 - headerProgress);
+ scaleToward(contactGridManager.getContainerView(), MathUtil.lerp(1f, .75f, headerProgress));
+
+ if (Math.abs(answerProgress) >= .0001) {
+ affordanceHolderLayout.animateHideLeftRightIcon();
+ handler.removeCallbacks(swipeHintRestoreTimer);
+ restoreSwipeHintTexts();
+ }
+ }
+
+ @Override
+ public void answerFromMethod() {
+ acceptCallByUser(false /* answerVideoAsAudio */);
+ }
+
+ @Override
+ public void rejectFromMethod() {
+ rejectCall();
+ }
+
+ @Override
+ public void resetAnswerProgress() {
+ affordanceHolderLayout.reset(true);
+ restoreBackgroundMaskColor();
+ }
+
+ private void animateEntry(@NonNull View rootView) {
+ contactGridManager.getContainerView().setAlpha(0f);
+ Animator alpha =
+ ObjectAnimator.ofFloat(contactGridManager.getContainerView(), View.ALPHA, 0, 1);
+ Animator topRow = createTranslation(rootView.findViewById(R.id.contactgrid_top_row));
+ Animator contactName = createTranslation(rootView.findViewById(R.id.contactgrid_contact_name));
+ Animator bottomRow = createTranslation(rootView.findViewById(R.id.contactgrid_bottom_row));
+ Animator important = createTranslation(importanceBadge);
+ Animator dataContainer = createTranslation(rootView.findViewById(R.id.incall_data_container));
+
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet
+ .play(alpha)
+ .with(topRow)
+ .with(contactName)
+ .with(bottomRow)
+ .with(important)
+ .with(dataContainer);
+ animatorSet.setDuration(getResources().getInteger(android.R.integer.config_longAnimTime));
+ animatorSet.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ hasAnimatedEntry = true;
+ }
+ });
+ animatorSet.start();
+ }
+
+ private ObjectAnimator createTranslation(View view) {
+ float translationY = view.getTop() * 0.5f;
+ ObjectAnimator animator = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, translationY, 0);
+ animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ return animator;
+ }
+
+ private void acceptCallByUser(boolean answerVideoAsAudio) {
+ LogUtil.i("AnswerFragment.acceptCallByUser", answerVideoAsAudio ? " answerVideoAsAudio" : "");
+ if (!buttonAcceptClicked) {
+ int desiredVideoState = getVideoState();
+ if (answerVideoAsAudio) {
+ desiredVideoState = VideoProfile.STATE_AUDIO_ONLY;
+ }
+
+ // Notify the lower layer first to start signaling ASAP.
+ answerScreenDelegate.onAnswer(desiredVideoState);
+
+ buttonAcceptClicked = true;
+ }
+ }
+
+ private void rejectCall() {
+ LogUtil.i("AnswerFragment.rejectCall", null);
+ if (!buttonRejectClicked) {
+ Context context = getContext();
+ if (context == null) {
+ LogUtil.w(
+ "AnswerFragment.rejectCall",
+ "Null context when rejecting call. Logger call was skipped");
+ } else {
+ Logger.get(context)
+ .logImpression(DialerImpression.Type.REJECT_INCOMING_CALL_FROM_ANSWER_SCREEN);
+ }
+ buttonRejectClicked = true;
+ answerScreenDelegate.onReject();
+ }
+ }
+
+ private void restoreBackgroundMaskColor() {
+ answerScreenDelegate.updateWindowBackgroundColor(0);
+ }
+
+ private void restoreSwipeHintTexts() {
+ if (getAnswerMethod() != null) {
+ getAnswerMethod().setHintText(null);
+ }
+ }
+
+ private void showMessageMenu() {
+ LogUtil.i("AnswerFragment.showMessageMenu", "Show sms menu.");
+
+ textResponsesFragment = SmsBottomSheetFragment.newInstance(textResponses);
+ textResponsesFragment.show(getChildFragmentManager(), null);
+ secondaryButton
+ .animate()
+ .alpha(0)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ affordanceHolderLayout.reset(false);
+ secondaryButton.animate().alpha(1);
+ }
+ });
+ }
+
+ @Override
+ public void smsSelected(@Nullable CharSequence text) {
+ LogUtil.i("AnswerFragment.smsSelected", null);
+ textResponsesFragment = null;
+
+ if (text == null) {
+ createCustomSmsDialogFragment = CreateCustomSmsDialogFragment.newInstance();
+ createCustomSmsDialogFragment.show(getChildFragmentManager(), null);
+ return;
+ }
+
+ if (primaryCallState != null && canRejectCallWithSms()) {
+ rejectCall();
+ answerScreenDelegate.onRejectCallWithMessage(text.toString());
+ }
+ }
+
+ @Override
+ public void smsDismissed() {
+ LogUtil.i("AnswerFragment.smsDismissed", null);
+ textResponsesFragment = null;
+ answerScreenDelegate.onDismissDialog();
+ }
+
+ @Override
+ public void customSmsCreated(@NonNull CharSequence text) {
+ LogUtil.i("AnswerFragment.customSmsCreated", null);
+ createCustomSmsDialogFragment = null;
+ if (primaryCallState != null && canRejectCallWithSms()) {
+ rejectCall();
+ answerScreenDelegate.onRejectCallWithMessage(text.toString());
+ }
+ }
+
+ @Override
+ public void customSmsDismissed() {
+ LogUtil.i("AnswerFragment.customSmsDismissed", null);
+ createCustomSmsDialogFragment = null;
+ answerScreenDelegate.onDismissDialog();
+ }
+
+ private boolean canRejectCallWithSms() {
+ return primaryCallState != null
+ && !(primaryCallState.state == State.DISCONNECTED
+ || primaryCallState.state == State.DISCONNECTING
+ || primaryCallState.state == State.IDLE);
+ }
+
+ private void createInCallScreenDelegate() {
+ inCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
+ .newInCallScreenDelegate();
+ Assert.isNotNull(inCallScreenDelegate);
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ }
+
+ private void updateImportanceBadgeVisibility() {
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!getResources().getBoolean(R.bool.answer_important_call_allowed)) {
+ importanceBadge.setVisibility(View.GONE);
+ return;
+ }
+
+ MultimediaData multimediaData = getSessionData();
+ boolean showImportant = multimediaData != null && multimediaData.isImportant();
+ TransitionManager.beginDelayedTransition((ViewGroup) importanceBadge.getParent());
+ // TODO (keyboardr): Change this back to being View.INVISIBLE once mocks are available to
+ // properly handle smaller screens
+ importanceBadge.setVisibility(showImportant ? View.VISIBLE : View.GONE);
+ }
+
+ @Nullable
+ private MultimediaData getSessionData() {
+ if (primaryInfo == null) {
+ return null;
+ }
+ return primaryInfo.multimediaData;
+ }
+
+ /** Shows the Avatar image if available. */
+ public static class AvatarFragment extends Fragment implements AvatarPresenter {
+
+ private ImageView avatarImageView;
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ return layoutInflater.inflate(R.layout.fragment_avatar, viewGroup, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ avatarImageView = ((ImageView) view.findViewById(R.id.contactgrid_avatar));
+ FragmentUtils.getParentUnsafe(this, MultimediaFragment.Holder.class).updateAvatar(this);
+ }
+
+ @NonNull
+ @Override
+ public ImageView getAvatarImageView() {
+ return avatarImageView;
+ }
+
+ @Override
+ public int getAvatarSize() {
+ return getResources().getDimensionPixelSize(R.dimen.answer_avatar_size);
+ }
+
+ @Override
+ public boolean shouldShowAnonymousAvatar() {
+ return false;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
new file mode 100644
index 000000000..0316a5fab
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/AnswerVideoCallScreen.java
@@ -0,0 +1,127 @@
+/*
+ * 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.incallui.answer.impl;
+
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.view.TextureView;
+import android.view.View;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+
+/** Shows a video preview for an incoming call. */
+public class AnswerVideoCallScreen implements VideoCallScreen {
+ @NonNull private final Fragment fragment;
+ @NonNull private final TextureView textureView;
+ @NonNull private final VideoCallScreenDelegate delegate;
+
+ public AnswerVideoCallScreen(@NonNull Fragment fragment, @NonNull View view) {
+ this.fragment = fragment;
+
+ textureView =
+ Assert.isNotNull((TextureView) view.findViewById(R.id.incoming_preview_texture_view));
+ View overlayView =
+ Assert.isNotNull(view.findViewById(R.id.incoming_preview_texture_view_overlay));
+ view.setBackgroundColor(0xff000000);
+ delegate =
+ FragmentUtils.getParentUnsafe(fragment, VideoCallScreenDelegateFactory.class)
+ .newVideoCallScreenDelegate();
+ delegate.initVideoCallScreenDelegate(fragment.getContext(), this);
+
+ textureView.setVisibility(View.VISIBLE);
+ overlayView.setVisibility(View.VISIBLE);
+ }
+
+ public void onStart() {
+ LogUtil.i("AnswerVideoCallScreen.onStart", null);
+ delegate.onVideoCallScreenUiReady();
+ delegate.getLocalVideoSurfaceTexture().attachToTextureView(textureView);
+ }
+
+ public void onStop() {
+ LogUtil.i("AnswerVideoCallScreen.onStop", null);
+ delegate.onVideoCallScreenUiUnready();
+ }
+
+ @Override
+ public void showVideoViews(
+ boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
+ LogUtil.i(
+ "AnswerVideoCallScreen.showVideoViews",
+ "showPreview: %b, shouldShowRemote: %b",
+ shouldShowPreview,
+ shouldShowRemote);
+ }
+
+ @Override
+ public void onLocalVideoDimensionsChanged() {
+ LogUtil.i("AnswerVideoCallScreen.onLocalVideoDimensionsChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void onRemoteVideoDimensionsChanged() {}
+
+ @Override
+ public void onLocalVideoOrientationChanged() {
+ LogUtil.i("AnswerVideoCallScreen.onLocalVideoOrientationChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {}
+
+ @Override
+ public Fragment getVideoCallScreenFragment() {
+ return fragment;
+ }
+
+ private void updatePreviewVideoScaling() {
+ if (textureView.getWidth() == 0 || textureView.getHeight() == 0) {
+ LogUtil.i(
+ "AnswerVideoCallScreen.updatePreviewVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+ Point cameraDimensions = delegate.getLocalVideoSurfaceTexture().getSurfaceDimensions();
+ if (cameraDimensions == null) {
+ LogUtil.i("AnswerVideoCallScreen.updatePreviewVideoScaling", "camera dimensions not set");
+ return;
+ }
+ if (isLandscape()) {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ textureView, cameraDimensions.x, cameraDimensions.y, delegate.getDeviceOrientation());
+ } else {
+ // Landscape, so dimensions are swapped
+ //noinspection SuspiciousNameCombination
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ textureView, cameraDimensions.y, cameraDimensions.x, delegate.getDeviceOrientation());
+ }
+ }
+
+ private boolean isLandscape() {
+ return fragment.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java b/java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java
new file mode 100644
index 000000000..b49409258
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/CreateCustomSmsDialogFragment.java
@@ -0,0 +1,137 @@
+/*
+ * 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.incallui.answer.impl;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnShowListener;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatDialogFragment;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.WindowManager.LayoutParams;
+import android.widget.Button;
+import android.widget.EditText;
+import com.android.dialer.common.FragmentUtils;
+
+/**
+ * Shows the dialog for users to enter a custom message when rejecting a call with an SMS message.
+ */
+public class CreateCustomSmsDialogFragment extends AppCompatDialogFragment {
+
+ private static final String ARG_ENTERED_TEXT = "enteredText";
+
+ private EditText editText;
+
+ public static CreateCustomSmsDialogFragment newInstance() {
+ return new CreateCustomSmsDialogFragment();
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ View view = View.inflate(builder.getContext(), R.layout.fragment_custom_sms_dialog, null);
+ editText = (EditText) view.findViewById(R.id.custom_sms_input);
+ if (savedInstanceState != null) {
+ editText.setText(savedInstanceState.getCharSequence(ARG_ENTERED_TEXT));
+ }
+ builder
+ .setCancelable(true)
+ .setView(view)
+ .setPositiveButton(
+ R.string.call_incoming_custom_message_send,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ FragmentUtils.getParentUnsafe(
+ CreateCustomSmsDialogFragment.this, CreateCustomSmsHolder.class)
+ .customSmsCreated(editText.getText().toString().trim());
+ dismiss();
+ }
+ })
+ .setNegativeButton(
+ R.string.call_incoming_custom_message_cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ dismiss();
+ }
+ })
+ .setOnCancelListener(
+ new OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ dismiss();
+ }
+ })
+ .setTitle(R.string.call_incoming_respond_via_sms_custom_message);
+ final AlertDialog customMessagePopup = builder.create();
+ customMessagePopup.setOnShowListener(
+ new OnShowListener() {
+ @Override
+ public void onShow(DialogInterface dialogInterface) {
+ ((AlertDialog) dialogInterface)
+ .getButton(AlertDialog.BUTTON_POSITIVE)
+ .setEnabled(false);
+ }
+ });
+
+ editText.addTextChangedListener(
+ new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ Button sendButton = customMessagePopup.getButton(DialogInterface.BUTTON_POSITIVE);
+ sendButton.setEnabled(editable != null && editable.toString().trim().length() != 0);
+ }
+ });
+ customMessagePopup.getWindow().setSoftInputMode(LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+ customMessagePopup.getWindow().addFlags(LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return customMessagePopup;
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putCharSequence(ARG_ENTERED_TEXT, editText.getText());
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ super.onDismiss(dialogInterface);
+ FragmentUtils.getParentUnsafe(this, CreateCustomSmsHolder.class).customSmsDismissed();
+ }
+
+ /** Call back for {@link CreateCustomSmsDialogFragment} */
+ public interface CreateCustomSmsHolder {
+
+ void customSmsCreated(@NonNull CharSequence text);
+
+ void customSmsDismissed();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/PillDrawable.java b/java/com/android/incallui/answer/impl/PillDrawable.java
new file mode 100644
index 000000000..57d84c45f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/PillDrawable.java
@@ -0,0 +1,43 @@
+/*
+ * 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.incallui.answer.impl;
+
+import android.graphics.Rect;
+import android.graphics.drawable.GradientDrawable;
+
+/** Draws a pill-shaped background */
+public class PillDrawable extends GradientDrawable {
+
+ public PillDrawable() {
+ super();
+ setShape(RECTANGLE);
+ }
+
+ @Override
+ protected void onBoundsChange(Rect r) {
+ super.onBoundsChange(r);
+ setCornerRadius(r.height() / 2);
+ }
+
+ @Override
+ public void setShape(int shape) {
+ if (shape != GradientDrawable.RECTANGLE) {
+ throw new UnsupportedOperationException("PillDrawable must be a rectangle");
+ }
+ super.setShape(shape);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java b/java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java
new file mode 100644
index 000000000..085430ea2
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/SmsBottomSheetFragment.java
@@ -0,0 +1,136 @@
+/*
+ * 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.incallui.answer.impl;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetDialogFragment;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Shows options for rejecting call with SMS */
+public class SmsBottomSheetFragment extends BottomSheetDialogFragment {
+
+ private static final String ARG_OPTIONS = "options";
+
+ public static SmsBottomSheetFragment newInstance(@Nullable ArrayList<CharSequence> options) {
+ SmsBottomSheetFragment fragment = new SmsBottomSheetFragment();
+ Bundle args = new Bundle();
+ args.putCharSequenceArrayList(ARG_OPTIONS, options);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ LinearLayout layout = new LinearLayout(getContext());
+ layout.setOrientation(LinearLayout.VERTICAL);
+ List<CharSequence> items = getArguments().getCharSequenceArrayList(ARG_OPTIONS);
+ if (items != null) {
+ for (CharSequence item : items) {
+ layout.addView(newTextViewItem(item));
+ }
+ }
+ layout.addView(newTextViewItem(null));
+ layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+ return layout;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, SmsSheetHolder.class);
+ }
+
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ LogUtil.i("SmsBottomSheetFragment.onCreateDialog", null);
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return dialog;
+ }
+
+ private TextView newTextViewItem(@Nullable final CharSequence text) {
+ int[] attrs = new int[] {android.R.attr.selectableItemBackground};
+ Context context = new ContextThemeWrapper(getContext(), getTheme());
+ TypedArray typedArray = context.obtainStyledAttributes(attrs);
+ Drawable background = typedArray.getDrawable(0);
+ //noinspection ResourceType
+ typedArray.recycle();
+
+ TextView textView = new TextView(context);
+ textView.setText(text == null ? getString(R.string.call_incoming_message_custom) : text);
+ int padding = (int) DpUtil.dpToPx(context, 16);
+ textView.setPadding(padding, padding, padding, padding);
+ textView.setBackground(background);
+ textView.setTextColor(context.getColor(R.color.blue_grey_100));
+ textView.setTextAppearance(R.style.TextAppearance_AppCompat_Widget_PopupMenu_Large);
+
+ LayoutParams params =
+ new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ textView.setLayoutParams(params);
+
+ textView.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ FragmentUtils.getParentUnsafe(SmsBottomSheetFragment.this, SmsSheetHolder.class)
+ .smsSelected(text);
+ dismiss();
+ }
+ });
+ return textView;
+ }
+
+ @Override
+ public int getTheme() {
+ return R.style.Theme_Design_Light_BottomSheetDialog;
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialogInterface) {
+ super.onDismiss(dialogInterface);
+ FragmentUtils.getParentUnsafe(this, SmsSheetHolder.class).smsDismissed();
+ }
+
+ /** Callback interface for {@link SmsBottomSheetFragment} */
+ public interface SmsSheetHolder {
+
+ void smsSelected(@Nullable CharSequence text);
+
+ void smsDismissed();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml b/java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml
new file mode 100644
index 000000000..960fd71c1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.answer.impl.affordance">
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
new file mode 100644
index 000000000..62845b748
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonHelper.java
@@ -0,0 +1,642 @@
+/*
+ * 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.incallui.answer.impl.affordance;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import com.android.incallui.answer.impl.utils.Interpolators;
+
+/** A touch handler of the swipe buttons */
+public class SwipeButtonHelper {
+
+ public static final float SWIPE_RESTING_ALPHA_AMOUNT = 0.87f;
+ public static final long HINT_PHASE1_DURATION = 200;
+ private static final long HINT_PHASE2_DURATION = 350;
+ private static final float BACKGROUND_RADIUS_SCALE_FACTOR = 0.25f;
+ private static final int HINT_CIRCLE_OPEN_DURATION = 500;
+
+ private final Context context;
+ private final Callback callback;
+
+ private FlingAnimationUtils flingAnimationUtils;
+ private VelocityTracker velocityTracker;
+ private boolean swipingInProgress;
+ private float initialTouchX;
+ private float initialTouchY;
+ private float translation;
+ private float translationOnDown;
+ private int touchSlop;
+ private int minTranslationAmount;
+ private int minFlingVelocity;
+ private int hintGrowAmount;
+ @Nullable private SwipeButtonView leftIcon;
+ @Nullable private SwipeButtonView rightIcon;
+ private Animator swipeAnimator;
+ private int minBackgroundRadius;
+ private boolean motionCancelled;
+ private int touchTargetSize;
+ private View targetedView;
+ private boolean touchSlopExeeded;
+ private AnimatorListenerAdapter flingEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ swipeAnimator = null;
+ swipingInProgress = false;
+ targetedView = null;
+ }
+ };
+ private Runnable animationEndRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ callback.onAnimationToSideEnded();
+ }
+ };
+
+ public SwipeButtonHelper(Callback callback, Context context) {
+ this.context = context;
+ this.callback = callback;
+ init();
+ }
+
+ public void init() {
+ initIcons();
+ updateIcon(
+ leftIcon,
+ 0.0f,
+ leftIcon != null ? leftIcon.getRestingAlpha() : 0,
+ false,
+ false,
+ true,
+ false);
+ updateIcon(
+ rightIcon,
+ 0.0f,
+ rightIcon != null ? rightIcon.getRestingAlpha() : 0,
+ false,
+ false,
+ true,
+ false);
+ initDimens();
+ }
+
+ private void initDimens() {
+ final ViewConfiguration configuration = ViewConfiguration.get(context);
+ touchSlop = configuration.getScaledPagingTouchSlop();
+ minFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+ minTranslationAmount =
+ context.getResources().getDimensionPixelSize(R.dimen.answer_min_swipe_amount);
+ minBackgroundRadius =
+ context
+ .getResources()
+ .getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius);
+ touchTargetSize =
+ context.getResources().getDimensionPixelSize(R.dimen.answer_affordance_touch_target_size);
+ hintGrowAmount =
+ context.getResources().getDimensionPixelSize(R.dimen.hint_grow_amount_sideways);
+ flingAnimationUtils = new FlingAnimationUtils(context, 0.4f);
+ }
+
+ private void initIcons() {
+ leftIcon = callback.getLeftIcon();
+ rightIcon = callback.getRightIcon();
+ updatePreviews();
+ }
+
+ public void updatePreviews() {
+ if (leftIcon != null) {
+ leftIcon.setPreviewView(callback.getLeftPreview());
+ }
+ if (rightIcon != null) {
+ rightIcon.setPreviewView(callback.getRightPreview());
+ }
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+ if (motionCancelled && action != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ final float y = event.getY();
+ final float x = event.getX();
+
+ boolean isUp = false;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ View targetView = getIconAtPosition(x, y);
+ if (targetView == null || (targetedView != null && targetedView != targetView)) {
+ motionCancelled = true;
+ return false;
+ }
+ if (targetedView != null) {
+ cancelAnimation();
+ } else {
+ touchSlopExeeded = false;
+ }
+ startSwiping(targetView);
+ initialTouchX = x;
+ initialTouchY = y;
+ translationOnDown = translation;
+ initVelocityTracker();
+ trackMovement(event);
+ motionCancelled = false;
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ motionCancelled = true;
+ endMotion(true /* forceSnapBack */, x, y);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ trackMovement(event);
+ float xDist = x - initialTouchX;
+ float yDist = y - initialTouchY;
+ float distance = (float) Math.hypot(xDist, yDist);
+ if (!touchSlopExeeded && distance > touchSlop) {
+ touchSlopExeeded = true;
+ }
+ if (swipingInProgress) {
+ if (targetedView == rightIcon) {
+ distance = translationOnDown - distance;
+ distance = Math.min(0, distance);
+ } else {
+ distance = translationOnDown + distance;
+ distance = Math.max(0, distance);
+ }
+ setTranslation(distance, false /* isReset */, false /* animateReset */);
+ }
+ break;
+
+ case MotionEvent.ACTION_UP:
+ isUp = true;
+ //fallthrough_intended
+ case MotionEvent.ACTION_CANCEL:
+ boolean hintOnTheRight = targetedView == rightIcon;
+ trackMovement(event);
+ endMotion(!isUp, x, y);
+ if (!touchSlopExeeded && isUp) {
+ callback.onIconClicked(hintOnTheRight);
+ }
+ break;
+ }
+ return true;
+ }
+
+ private void startSwiping(View targetView) {
+ callback.onSwipingStarted(targetView == rightIcon);
+ swipingInProgress = true;
+ targetedView = targetView;
+ }
+
+ private View getIconAtPosition(float x, float y) {
+ if (leftSwipePossible() && isOnIcon(leftIcon, x, y)) {
+ return leftIcon;
+ }
+ if (rightSwipePossible() && isOnIcon(rightIcon, x, y)) {
+ return rightIcon;
+ }
+ return null;
+ }
+
+ public boolean isOnAffordanceIcon(float x, float y) {
+ return isOnIcon(leftIcon, x, y) || isOnIcon(rightIcon, x, y);
+ }
+
+ private boolean isOnIcon(View icon, float x, float y) {
+ float iconX = icon.getX() + icon.getWidth() / 2.0f;
+ float iconY = icon.getY() + icon.getHeight() / 2.0f;
+ double distance = Math.hypot(x - iconX, y - iconY);
+ return distance <= touchTargetSize / 2;
+ }
+
+ private void endMotion(boolean forceSnapBack, float lastX, float lastY) {
+ if (swipingInProgress) {
+ flingWithCurrentVelocity(forceSnapBack, lastX, lastY);
+ } else {
+ targetedView = null;
+ }
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ velocityTracker = null;
+ }
+ }
+
+ private boolean rightSwipePossible() {
+ return rightIcon != null && rightIcon.getVisibility() == View.VISIBLE;
+ }
+
+ private boolean leftSwipePossible() {
+ return leftIcon != null && leftIcon.getVisibility() == View.VISIBLE;
+ }
+
+ public void startHintAnimation(boolean right, @Nullable Runnable onFinishedListener) {
+ cancelAnimation();
+ startHintAnimationPhase1(right, onFinishedListener);
+ }
+
+ private void startHintAnimationPhase1(
+ final boolean right, @Nullable final Runnable onFinishedListener) {
+ final SwipeButtonView targetView = right ? rightIcon : leftIcon;
+ ValueAnimator animator = getAnimatorToRadius(right, hintGrowAmount);
+ if (animator == null) {
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ return;
+ }
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ private boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mCancelled) {
+ swipeAnimator = null;
+ targetedView = null;
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ } else {
+ startUnlockHintAnimationPhase2(right, onFinishedListener);
+ }
+ }
+ });
+ animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ animator.setDuration(HINT_PHASE1_DURATION);
+ animator.start();
+ swipeAnimator = animator;
+ targetedView = targetView;
+ }
+
+ /** Phase 2: Move back. */
+ private void startUnlockHintAnimationPhase2(
+ boolean right, @Nullable final Runnable onFinishedListener) {
+ ValueAnimator animator = getAnimatorToRadius(right, 0);
+ if (animator == null) {
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ return;
+ }
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ swipeAnimator = null;
+ targetedView = null;
+ if (onFinishedListener != null) {
+ onFinishedListener.run();
+ }
+ }
+ });
+ animator.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
+ animator.setDuration(HINT_PHASE2_DURATION);
+ animator.setStartDelay(HINT_CIRCLE_OPEN_DURATION);
+ animator.start();
+ swipeAnimator = animator;
+ }
+
+ private ValueAnimator getAnimatorToRadius(final boolean right, int radius) {
+ final SwipeButtonView targetView = right ? rightIcon : leftIcon;
+ if (targetView == null) {
+ return null;
+ }
+ ValueAnimator animator = ValueAnimator.ofFloat(targetView.getCircleRadius(), radius);
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ float newRadius = (float) animation.getAnimatedValue();
+ targetView.setCircleRadiusWithoutAnimation(newRadius);
+ float translation = getTranslationFromRadius(newRadius);
+ SwipeButtonHelper.this.translation = right ? -translation : translation;
+ updateIconsFromTranslation(targetView);
+ }
+ });
+ return animator;
+ }
+
+ private void cancelAnimation() {
+ if (swipeAnimator != null) {
+ swipeAnimator.cancel();
+ }
+ }
+
+ private void flingWithCurrentVelocity(boolean forceSnapBack, float lastX, float lastY) {
+ float vel = getCurrentVelocity(lastX, lastY);
+
+ // We snap back if the current translation is not far enough
+ boolean snapBack = isBelowFalsingThreshold();
+
+ // or if the velocity is in the opposite direction.
+ boolean velIsInWrongDirection = vel * translation < 0;
+ snapBack |= Math.abs(vel) > minFlingVelocity && velIsInWrongDirection;
+ vel = snapBack ^ velIsInWrongDirection ? 0 : vel;
+ fling(vel, snapBack || forceSnapBack, translation < 0);
+ }
+
+ private boolean isBelowFalsingThreshold() {
+ return Math.abs(translation) < Math.abs(translationOnDown) + getMinTranslationAmount();
+ }
+
+ private int getMinTranslationAmount() {
+ float factor = callback.getAffordanceFalsingFactor();
+ return (int) (minTranslationAmount * factor);
+ }
+
+ private void fling(float vel, final boolean snapBack, boolean right) {
+ float target =
+ right ? -callback.getMaxTranslationDistance() : callback.getMaxTranslationDistance();
+ target = snapBack ? 0 : target;
+
+ ValueAnimator animator = ValueAnimator.ofFloat(translation, target);
+ flingAnimationUtils.apply(animator, translation, target, vel);
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ translation = (float) animation.getAnimatedValue();
+ }
+ });
+ animator.addListener(flingEndListener);
+ if (!snapBack) {
+ startFinishingCircleAnimation(vel * 0.375f, animationEndRunnable, right);
+ callback.onAnimationToSideStarted(right, translation, vel);
+ } else {
+ reset(true);
+ }
+ animator.start();
+ swipeAnimator = animator;
+ if (snapBack) {
+ callback.onSwipingAborted();
+ }
+ }
+
+ private void startFinishingCircleAnimation(
+ float velocity, Runnable mAnimationEndRunnable, boolean right) {
+ SwipeButtonView targetView = right ? rightIcon : leftIcon;
+ if (targetView != null) {
+ targetView.finishAnimation(velocity, mAnimationEndRunnable);
+ }
+ }
+
+ private void setTranslation(float translation, boolean isReset, boolean animateReset) {
+ translation = rightSwipePossible() ? translation : Math.max(0, translation);
+ translation = leftSwipePossible() ? translation : Math.min(0, translation);
+ float absTranslation = Math.abs(translation);
+ if (translation != this.translation || isReset) {
+ SwipeButtonView targetView = translation > 0 ? leftIcon : rightIcon;
+ SwipeButtonView otherView = translation > 0 ? rightIcon : leftIcon;
+ float alpha = absTranslation / getMinTranslationAmount();
+
+ // We interpolate the alpha of the other icons to 0
+ float fadeOutAlpha = 1.0f - alpha;
+ fadeOutAlpha = Math.max(fadeOutAlpha, 0.0f);
+
+ boolean animateIcons = isReset && animateReset;
+ boolean forceNoCircleAnimation = isReset && !animateReset;
+ float radius = getRadiusFromTranslation(absTranslation);
+ boolean slowAnimation = isReset && isBelowFalsingThreshold();
+ if (targetView != null) {
+ if (!isReset) {
+ updateIcon(
+ targetView,
+ radius,
+ alpha + fadeOutAlpha * targetView.getRestingAlpha(),
+ false,
+ false,
+ false,
+ false);
+ } else {
+ updateIcon(
+ targetView,
+ 0.0f,
+ fadeOutAlpha * targetView.getRestingAlpha(),
+ animateIcons,
+ slowAnimation,
+ false,
+ forceNoCircleAnimation);
+ }
+ }
+ if (otherView != null) {
+ updateIcon(
+ otherView,
+ 0.0f,
+ fadeOutAlpha * otherView.getRestingAlpha(),
+ animateIcons,
+ slowAnimation,
+ false,
+ forceNoCircleAnimation);
+ }
+
+ this.translation = translation;
+ }
+ }
+
+ private void updateIconsFromTranslation(SwipeButtonView targetView) {
+ float absTranslation = Math.abs(translation);
+ float alpha = absTranslation / getMinTranslationAmount();
+
+ // We interpolate the alpha of the other icons to 0
+ float fadeOutAlpha = 1.0f - alpha;
+ fadeOutAlpha = Math.max(0.0f, fadeOutAlpha);
+
+ // We interpolate the alpha of the targetView to 1
+ SwipeButtonView otherView = targetView == rightIcon ? leftIcon : rightIcon;
+ updateIconAlpha(targetView, alpha + fadeOutAlpha * targetView.getRestingAlpha(), false);
+ if (otherView != null) {
+ updateIconAlpha(otherView, fadeOutAlpha * otherView.getRestingAlpha(), false);
+ }
+ }
+
+ private float getTranslationFromRadius(float circleSize) {
+ float translation = (circleSize - minBackgroundRadius) / BACKGROUND_RADIUS_SCALE_FACTOR;
+ return translation > 0.0f ? translation + touchSlop : 0.0f;
+ }
+
+ private float getRadiusFromTranslation(float translation) {
+ if (translation <= touchSlop) {
+ return 0.0f;
+ }
+ return (translation - touchSlop) * BACKGROUND_RADIUS_SCALE_FACTOR + minBackgroundRadius;
+ }
+
+ public void animateHideLeftRightIcon() {
+ cancelAnimation();
+ updateIcon(rightIcon, 0f, 0f, true, false, false, false);
+ updateIcon(leftIcon, 0f, 0f, true, false, false, false);
+ }
+
+ private void updateIcon(
+ @Nullable SwipeButtonView view,
+ float circleRadius,
+ float alpha,
+ boolean animate,
+ boolean slowRadiusAnimation,
+ boolean force,
+ boolean forceNoCircleAnimation) {
+ if (view == null) {
+ return;
+ }
+ if (view.getVisibility() != View.VISIBLE && !force) {
+ return;
+ }
+ if (forceNoCircleAnimation) {
+ view.setCircleRadiusWithoutAnimation(circleRadius);
+ } else {
+ view.setCircleRadius(circleRadius, slowRadiusAnimation);
+ }
+ updateIconAlpha(view, alpha, animate);
+ }
+
+ private void updateIconAlpha(SwipeButtonView view, float alpha, boolean animate) {
+ float scale = getScale(alpha, view);
+ alpha = Math.min(1.0f, alpha);
+ view.setImageAlpha(alpha, animate);
+ view.setImageScale(scale, animate);
+ }
+
+ private float getScale(float alpha, SwipeButtonView icon) {
+ float scale = alpha / icon.getRestingAlpha() * 0.2f + SwipeButtonView.MIN_ICON_SCALE_AMOUNT;
+ return Math.min(scale, SwipeButtonView.MAX_ICON_SCALE_AMOUNT);
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (velocityTracker != null) {
+ velocityTracker.addMovement(event);
+ }
+ }
+
+ private void initVelocityTracker() {
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ }
+ velocityTracker = VelocityTracker.obtain();
+ }
+
+ private float getCurrentVelocity(float lastX, float lastY) {
+ if (velocityTracker == null) {
+ return 0;
+ }
+ velocityTracker.computeCurrentVelocity(1000);
+ float aX = velocityTracker.getXVelocity();
+ float aY = velocityTracker.getYVelocity();
+ float bX = lastX - initialTouchX;
+ float bY = lastY - initialTouchY;
+ float bLen = (float) Math.hypot(bX, bY);
+ // Project the velocity onto the distance vector: a * b / |b|
+ float projectedVelocity = (aX * bX + aY * bY) / bLen;
+ if (targetedView == rightIcon) {
+ projectedVelocity = -projectedVelocity;
+ }
+ return projectedVelocity;
+ }
+
+ public void onConfigurationChanged() {
+ initDimens();
+ initIcons();
+ }
+
+ public void onRtlPropertiesChanged() {
+ initIcons();
+ }
+
+ public void reset(boolean animate) {
+ cancelAnimation();
+ setTranslation(0.0f, true, animate);
+ motionCancelled = true;
+ if (swipingInProgress) {
+ callback.onSwipingAborted();
+ swipingInProgress = false;
+ }
+ }
+
+ public boolean isSwipingInProgress() {
+ return swipingInProgress;
+ }
+
+ public void launchAffordance(boolean animate, boolean left) {
+ SwipeButtonView targetView = left ? leftIcon : rightIcon;
+ if (swipingInProgress || targetView == null) {
+ // We don't want to mess with the state if the user is actually swiping already.
+ return;
+ }
+ SwipeButtonView otherView = left ? rightIcon : leftIcon;
+ startSwiping(targetView);
+ if (animate) {
+ fling(0, false, !left);
+ updateIcon(otherView, 0.0f, 0, true, false, true, false);
+ } else {
+ callback.onAnimationToSideStarted(!left, translation, 0);
+ translation =
+ left ? callback.getMaxTranslationDistance() : callback.getMaxTranslationDistance();
+ updateIcon(otherView, 0.0f, 0.0f, false, false, true, false);
+ targetView.instantFinishAnimation();
+ flingEndListener.onAnimationEnd(null);
+ animationEndRunnable.run();
+ }
+ }
+
+ /** Callback interface for various actions */
+ public interface Callback {
+
+ /**
+ * Notifies the callback when an animation to a side page was started.
+ *
+ * @param rightPage Is the page animated to the right page?
+ */
+ void onAnimationToSideStarted(boolean rightPage, float translation, float vel);
+
+ /** Notifies the callback the animation to a side page has ended. */
+ void onAnimationToSideEnded();
+
+ float getMaxTranslationDistance();
+
+ void onSwipingStarted(boolean rightIcon);
+
+ void onSwipingAborted();
+
+ void onIconClicked(boolean rightIcon);
+
+ @Nullable
+ SwipeButtonView getLeftIcon();
+
+ @Nullable
+ SwipeButtonView getRightIcon();
+
+ @Nullable
+ View getLeftPreview();
+
+ @Nullable
+ View getRightPreview();
+
+ /** @return The factor the minimum swipe amount should be multiplied with. */
+ float getAffordanceFalsingFactor();
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java b/java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java
new file mode 100644
index 000000000..46879ea3f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/SwipeButtonView.java
@@ -0,0 +1,505 @@
+/*
+ * 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.incallui.answer.impl.affordance;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ArgbEvaluator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewAnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import com.android.incallui.answer.impl.utils.Interpolators;
+
+/** Button that allows swiping to trigger */
+public class SwipeButtonView extends ImageView {
+
+ private static final long CIRCLE_APPEAR_DURATION = 80;
+ private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200;
+ private static final long NORMAL_ANIMATION_DURATION = 200;
+ public static final float MAX_ICON_SCALE_AMOUNT = 1.5f;
+ public static final float MIN_ICON_SCALE_AMOUNT = 0.8f;
+
+ private final int minBackgroundRadius;
+ private final Paint circlePaint;
+ private final int inverseColor;
+ private final int normalColor;
+ private final ArgbEvaluator colorInterpolator;
+ private final FlingAnimationUtils flingAnimationUtils;
+ private float circleRadius;
+ private int centerX;
+ private int centerY;
+ private ValueAnimator circleAnimator;
+ private ValueAnimator alphaAnimator;
+ private ValueAnimator scaleAnimator;
+ private float circleStartValue;
+ private boolean circleWillBeHidden;
+ private int[] tempPoint = new int[2];
+ private float tmageScale = 1f;
+ private int circleColor;
+ private View previewView;
+ private float circleStartRadius;
+ private float maxCircleSize;
+ private Animator previewClipper;
+ private float restingAlpha = SwipeButtonHelper.SWIPE_RESTING_ALPHA_AMOUNT;
+ private boolean finishing;
+ private boolean launchingAffordance;
+
+ private AnimatorListenerAdapter clipEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ previewClipper = null;
+ }
+ };
+ private AnimatorListenerAdapter circleEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ circleAnimator = null;
+ }
+ };
+ private AnimatorListenerAdapter scaleEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ scaleAnimator = null;
+ }
+ };
+ private AnimatorListenerAdapter alphaEndListener =
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ alphaAnimator = null;
+ }
+ };
+
+ public SwipeButtonView(Context context) {
+ this(context, null);
+ }
+
+ public SwipeButtonView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SwipeButtonView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ circlePaint = new Paint();
+ circlePaint.setAntiAlias(true);
+ circleColor = 0xffffffff;
+ circlePaint.setColor(circleColor);
+
+ normalColor = 0xffffffff;
+ inverseColor = 0xff000000;
+ minBackgroundRadius =
+ context
+ .getResources()
+ .getDimensionPixelSize(R.dimen.answer_affordance_min_background_radius);
+ colorInterpolator = new ArgbEvaluator();
+ flingAnimationUtils = new FlingAnimationUtils(context, 0.3f);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ centerX = getWidth() / 2;
+ centerY = getHeight() / 2;
+ maxCircleSize = getMaxCircleSize();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ drawBackgroundCircle(canvas);
+ canvas.save();
+ canvas.scale(tmageScale, tmageScale, getWidth() / 2, getHeight() / 2);
+ super.onDraw(canvas);
+ canvas.restore();
+ }
+
+ public void setPreviewView(@Nullable View v) {
+ View oldPreviewView = previewView;
+ previewView = v;
+ if (previewView != null) {
+ previewView.setVisibility(launchingAffordance ? oldPreviewView.getVisibility() : INVISIBLE);
+ }
+ }
+
+ private void updateIconColor() {
+ Drawable drawable = getDrawable().mutate();
+ float alpha = circleRadius / minBackgroundRadius;
+ alpha = Math.min(1.0f, alpha);
+ int color = (int) colorInterpolator.evaluate(alpha, normalColor, inverseColor);
+ drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+ }
+
+ private void drawBackgroundCircle(Canvas canvas) {
+ if (circleRadius > 0 || finishing) {
+ updateCircleColor();
+ canvas.drawCircle(centerX, centerY, circleRadius, circlePaint);
+ }
+ }
+
+ private void updateCircleColor() {
+ float fraction =
+ 0.5f
+ + 0.5f
+ * Math.max(
+ 0.0f,
+ Math.min(
+ 1.0f, (circleRadius - minBackgroundRadius) / (0.5f * minBackgroundRadius)));
+ if (previewView != null && previewView.getVisibility() == VISIBLE) {
+ float finishingFraction =
+ 1 - Math.max(0, circleRadius - circleStartRadius) / (maxCircleSize - circleStartRadius);
+ fraction *= finishingFraction;
+ }
+ int color =
+ Color.argb(
+ (int) (Color.alpha(circleColor) * fraction),
+ Color.red(circleColor),
+ Color.green(circleColor),
+ Color.blue(circleColor));
+ circlePaint.setColor(color);
+ }
+
+ public void finishAnimation(float velocity, @Nullable final Runnable mAnimationEndRunnable) {
+ cancelAnimator(circleAnimator);
+ cancelAnimator(previewClipper);
+ finishing = true;
+ circleStartRadius = circleRadius;
+ final float maxCircleSize = getMaxCircleSize();
+ Animator animatorToRadius;
+ animatorToRadius = getAnimatorToRadius(maxCircleSize);
+ flingAnimationUtils.applyDismissing(
+ animatorToRadius, circleRadius, maxCircleSize, velocity, maxCircleSize);
+ animatorToRadius.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mAnimationEndRunnable != null) {
+ mAnimationEndRunnable.run();
+ }
+ finishing = false;
+ circleRadius = maxCircleSize;
+ invalidate();
+ }
+ });
+ animatorToRadius.start();
+ setImageAlpha(0, true);
+ if (previewView != null) {
+ previewView.setVisibility(View.VISIBLE);
+ previewClipper =
+ ViewAnimationUtils.createCircularReveal(
+ previewView, getLeft() + centerX, getTop() + centerY, circleRadius, maxCircleSize);
+ flingAnimationUtils.applyDismissing(
+ previewClipper, circleRadius, maxCircleSize, velocity, maxCircleSize);
+ previewClipper.addListener(clipEndListener);
+ previewClipper.start();
+ }
+ }
+
+ public void instantFinishAnimation() {
+ cancelAnimator(previewClipper);
+ if (previewView != null) {
+ previewView.setClipBounds(null);
+ previewView.setVisibility(View.VISIBLE);
+ }
+ circleRadius = getMaxCircleSize();
+ setImageAlpha(0, false);
+ invalidate();
+ }
+
+ private float getMaxCircleSize() {
+ getLocationInWindow(tempPoint);
+ float rootWidth = getRootView().getWidth();
+ float width = tempPoint[0] + centerX;
+ width = Math.max(rootWidth - width, width);
+ float height = tempPoint[1] + centerY;
+ return (float) Math.hypot(width, height);
+ }
+
+ public void setCircleRadius(float circleRadius) {
+ setCircleRadius(circleRadius, false, false);
+ }
+
+ public void setCircleRadius(float circleRadius, boolean slowAnimation) {
+ setCircleRadius(circleRadius, slowAnimation, false);
+ }
+
+ public void setCircleRadiusWithoutAnimation(float circleRadius) {
+ cancelAnimator(circleAnimator);
+ setCircleRadius(circleRadius, false, true);
+ }
+
+ private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) {
+
+ // Check if we need a new animation
+ boolean radiusHidden =
+ (circleAnimator != null && circleWillBeHidden)
+ || (circleAnimator == null && this.circleRadius == 0.0f);
+ boolean nowHidden = circleRadius == 0.0f;
+ boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation;
+ if (!radiusNeedsAnimation) {
+ if (circleAnimator == null) {
+ this.circleRadius = circleRadius;
+ updateIconColor();
+ invalidate();
+ if (nowHidden) {
+ if (previewView != null) {
+ previewView.setVisibility(View.INVISIBLE);
+ }
+ }
+ } else if (!circleWillBeHidden) {
+
+ // We just update the end value
+ float diff = circleRadius - minBackgroundRadius;
+ PropertyValuesHolder[] values = circleAnimator.getValues();
+ values[0].setFloatValues(circleStartValue + diff, circleRadius);
+ circleAnimator.setCurrentPlayTime(circleAnimator.getCurrentPlayTime());
+ }
+ } else {
+ cancelAnimator(circleAnimator);
+ cancelAnimator(previewClipper);
+ ValueAnimator animator = getAnimatorToRadius(circleRadius);
+ Interpolator interpolator =
+ circleRadius == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ animator.setInterpolator(interpolator);
+ long duration = 250;
+ if (!slowAnimation) {
+ float durationFactor =
+ Math.abs(this.circleRadius - circleRadius) / (float) minBackgroundRadius;
+ duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor);
+ duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION);
+ }
+ animator.setDuration(duration);
+ animator.start();
+ if (previewView != null && previewView.getVisibility() == View.VISIBLE) {
+ previewView.setVisibility(View.VISIBLE);
+ previewClipper =
+ ViewAnimationUtils.createCircularReveal(
+ previewView,
+ getLeft() + centerX,
+ getTop() + centerY,
+ this.circleRadius,
+ circleRadius);
+ previewClipper.setInterpolator(interpolator);
+ previewClipper.setDuration(duration);
+ previewClipper.addListener(clipEndListener);
+ previewClipper.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ previewView.setVisibility(View.INVISIBLE);
+ }
+ });
+ previewClipper.start();
+ }
+ }
+ }
+
+ private ValueAnimator getAnimatorToRadius(float circleRadius) {
+ ValueAnimator animator = ValueAnimator.ofFloat(this.circleRadius, circleRadius);
+ circleAnimator = animator;
+ circleStartValue = this.circleRadius;
+ circleWillBeHidden = circleRadius == 0.0f;
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ SwipeButtonView.this.circleRadius = (float) animation.getAnimatedValue();
+ updateIconColor();
+ invalidate();
+ }
+ });
+ animator.addListener(circleEndListener);
+ return animator;
+ }
+
+ private void cancelAnimator(Animator animator) {
+ if (animator != null) {
+ animator.cancel();
+ }
+ }
+
+ public void setImageScale(float imageScale, boolean animate) {
+ setImageScale(imageScale, animate, -1, null);
+ }
+
+ /**
+ * Sets the scale of the containing image
+ *
+ * @param imageScale The new Scale.
+ * @param animate Should an animation be performed
+ * @param duration If animate, whats the duration? When -1 we take the default duration
+ * @param interpolator If animate, whats the interpolator? When null we take the default
+ * interpolator.
+ */
+ public void setImageScale(
+ float imageScale, boolean animate, long duration, @Nullable Interpolator interpolator) {
+ cancelAnimator(scaleAnimator);
+ if (!animate) {
+ tmageScale = imageScale;
+ invalidate();
+ } else {
+ ValueAnimator animator = ValueAnimator.ofFloat(tmageScale, imageScale);
+ scaleAnimator = animator;
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ tmageScale = (float) animation.getAnimatedValue();
+ invalidate();
+ }
+ });
+ animator.addListener(scaleEndListener);
+ if (interpolator == null) {
+ interpolator =
+ imageScale == 0.0f
+ ? Interpolators.FAST_OUT_LINEAR_IN
+ : Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ animator.setInterpolator(interpolator);
+ if (duration == -1) {
+ float durationFactor = Math.abs(tmageScale - imageScale) / (1.0f - MIN_ICON_SCALE_AMOUNT);
+ durationFactor = Math.min(1.0f, durationFactor);
+ duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+ }
+ animator.setDuration(duration);
+ animator.start();
+ }
+ }
+
+ public void setRestingAlpha(float alpha) {
+ restingAlpha = alpha;
+
+ // TODO: Handle the case an animation is playing.
+ setImageAlpha(alpha, false);
+ }
+
+ public float getRestingAlpha() {
+ return restingAlpha;
+ }
+
+ public void setImageAlpha(float alpha, boolean animate) {
+ setImageAlpha(alpha, animate, -1, null, null);
+ }
+
+ /**
+ * Sets the alpha of the containing image
+ *
+ * @param alpha The new alpha.
+ * @param animate Should an animation be performed
+ * @param duration If animate, whats the duration? When -1 we take the default duration
+ * @param interpolator If animate, whats the interpolator? When null we take the default
+ * interpolator.
+ */
+ public void setImageAlpha(
+ float alpha,
+ boolean animate,
+ long duration,
+ @Nullable Interpolator interpolator,
+ @Nullable Runnable runnable) {
+ cancelAnimator(alphaAnimator);
+ alpha = launchingAffordance ? 0 : alpha;
+ int endAlpha = (int) (alpha * 255);
+ final Drawable background = getBackground();
+ if (!animate) {
+ if (background != null) {
+ background.mutate().setAlpha(endAlpha);
+ }
+ setImageAlpha(endAlpha);
+ } else {
+ int currentAlpha = getImageAlpha();
+ ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha);
+ alphaAnimator = animator;
+ animator.addUpdateListener(
+ new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ int alpha = (int) animation.getAnimatedValue();
+ if (background != null) {
+ background.mutate().setAlpha(alpha);
+ }
+ setImageAlpha(alpha);
+ }
+ });
+ animator.addListener(alphaEndListener);
+ if (interpolator == null) {
+ interpolator =
+ alpha == 0.0f ? Interpolators.FAST_OUT_LINEAR_IN : Interpolators.LINEAR_OUT_SLOW_IN;
+ }
+ animator.setInterpolator(interpolator);
+ if (duration == -1) {
+ float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f;
+ durationFactor = Math.min(1.0f, durationFactor);
+ duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor);
+ }
+ animator.setDuration(duration);
+ if (runnable != null) {
+ animator.addListener(getEndListener(runnable));
+ }
+ animator.start();
+ }
+ }
+
+ private Animator.AnimatorListener getEndListener(final Runnable runnable) {
+ return new AnimatorListenerAdapter() {
+ boolean mCancelled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mCancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!mCancelled) {
+ runnable.run();
+ }
+ }
+ };
+ }
+
+ public float getCircleRadius() {
+ return circleRadius;
+ }
+
+ @Override
+ public boolean performClick() {
+ return isClickable() && super.performClick();
+ }
+
+ public void setLaunchingAffordance(boolean launchingAffordance) {
+ this.launchingAffordance = launchingAffordance;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml b/java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml
new file mode 100644
index 000000000..71d014dd9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/affordance/res/values/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="answer_min_swipe_amount">110dp</dimen>
+ <dimen name="answer_affordance_min_background_radius">30dp</dimen>
+ <dimen name="answer_affordance_touch_target_size">120dp</dimen>
+ <dimen name="hint_grow_amount_sideways">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml b/java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml
new file mode 100644
index 000000000..9082407f1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.answer.impl.answermethod">
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethod.java
new file mode 100644
index 000000000..5efd3f05b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethod.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.incallui.answer.impl.answermethod;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import com.android.dialer.common.FragmentUtils;
+
+/** A fragment that can be used to answer/reject calls. */
+public abstract class AnswerMethod extends Fragment {
+
+ public abstract void setHintText(@Nullable CharSequence hintText);
+
+ public abstract void setShowIncomingWillDisconnect(boolean incomingWillDisconnect);
+
+ public void setContactPhoto(@Nullable Drawable contactPhoto) {
+ // default implementation does nothing. Only some AnswerMethods show a photo
+ }
+
+ protected AnswerMethodHolder getParent() {
+ return FragmentUtils.getParentUnsafe(this, AnswerMethodHolder.class);
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, AnswerMethodHolder.class);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.java
new file mode 100644
index 000000000..35f36f727
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodFactory.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.incallui.answer.impl.answermethod;
+
+import android.app.Activity;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.incallui.util.AccessibilityUtil;
+
+/** Creates the appropriate {@link AnswerMethod} for the circumstances. */
+public class AnswerMethodFactory {
+
+ @NonNull
+ public static AnswerMethod createAnswerMethod(@NonNull Activity activity) {
+ if (needTwoButton(activity)) {
+ return new TwoButtonMethod();
+ } else {
+ return new FlingUpDownMethod();
+ }
+ }
+
+ public static boolean needsReplacement(@Nullable Fragment answerMethod) {
+ //noinspection SimplifiableIfStatement
+ if (answerMethod == null) {
+ return true;
+ }
+ // If we have already started showing TwoButtonMethod, we should keep showing TwoButtonMethod.
+ // Otherwise check if we need to change to TwoButtonMethod
+ return !(answerMethod instanceof TwoButtonMethod) && needTwoButton(answerMethod.getActivity());
+ }
+
+ private static boolean needTwoButton(@NonNull Activity activity) {
+ return AccessibilityUtil.isTouchExplorationEnabled(activity)
+ || ActivityCompat.isInMultiWindowMode(activity);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.java
new file mode 100644
index 000000000..4052281b7
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/AnswerMethodHolder.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.incallui.answer.impl.answermethod;
+
+import android.support.annotation.FloatRange;
+
+/** Defines callbacks {@link AnswerMethod AnswerMethods} may use to update their parent. */
+public interface AnswerMethodHolder {
+
+ /**
+ * Update animation based on method progress.
+ *
+ * @param answerProgress float representing progress. -1 is fully declined, 1 is fully answered,
+ * and 0 is neutral.
+ */
+ void onAnswerProgressUpdate(@FloatRange(from = -1f, to = 1f) float answerProgress);
+
+ /** Answer the current call. */
+ void answerFromMethod();
+
+ /** Reject the current call. */
+ void rejectFromMethod();
+
+ /** Set AnswerProgress to zero (not due to normal updates). */
+ void resetAnswerProgress();
+
+ /**
+ * Check whether the current call is a video call.
+ *
+ * @return true iff the current call is a video call.
+ */
+ boolean isVideoCall();
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
new file mode 100644
index 000000000..0bc65818c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownMethod.java
@@ -0,0 +1,1149 @@
+/*
+ * 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.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.ColorInt;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.graphics.ColorUtils;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.support.v4.view.animation.PathInterpolatorCompat;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.dialer.util.DrawableConverter;
+import com.android.dialer.util.ViewUtil;
+import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnProgressChangedListener;
+import com.android.incallui.answer.impl.classifier.FalsingManager;
+import com.android.incallui.answer.impl.hint.AnswerHint;
+import com.android.incallui.answer.impl.hint.AnswerHintFactory;
+import com.android.incallui.answer.impl.hint.EventPayloadLoaderImpl;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Answer method that swipes up to answer or down to reject. */
+@SuppressLint("ClickableViewAccessibility")
+public class FlingUpDownMethod extends AnswerMethod implements OnProgressChangedListener {
+
+ private static final float SWIPE_LERP_PROGRESS_FACTOR = 0.5f;
+ private static final long ANIMATE_DURATION_SHORT_MILLIS = 667;
+ private static final long ANIMATE_DURATION_NORMAL_MILLIS = 1_333;
+ private static final long ANIMATE_DURATION_LONG_MILLIS = 1_500;
+ private static final long BOUNCE_ANIMATION_DELAY = 167;
+ private static final long VIBRATION_TIME_MILLIS = 1_833;
+ private static final long SETTLE_ANIMATION_DURATION_MILLIS = 100;
+ private static final int HINT_JUMP_DP = 60;
+ private static final int HINT_DIP_DP = 8;
+ private static final float HINT_SCALE_RATIO = 1.15f;
+ private static final long SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS = 333;
+ private static final int HINT_REJECT_SHOW_DURATION_MILLIS = 2000;
+ private static final int ICON_END_CALL_ROTATION_DEGREES = 135;
+ private static final int HINT_REJECT_FADE_TRANSLATION_Y_DP = -8;
+ private static final float SWIPE_TO_ANSWER_MAX_TRANSLATION_Y_DP = 150;
+ private static final int SWIPE_TO_REJECT_MAX_TRANSLATION_Y_DP = 24;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ value = {
+ AnimationState.NONE,
+ AnimationState.ENTRY,
+ AnimationState.BOUNCE,
+ AnimationState.SWIPE,
+ AnimationState.SETTLE,
+ AnimationState.HINT,
+ AnimationState.COMPLETED
+ }
+ )
+ @VisibleForTesting
+ @interface AnimationState {
+
+ int NONE = 0;
+ int ENTRY = 1; // Entry animation for incoming call
+ int BOUNCE = 2; // An idle state in which text and icon slightly bounces off its base repeatedly
+ int SWIPE = 3; // A special state in which text and icon follows the finger movement
+ int SETTLE = 4; // A short animation to reset from swipe and prepare for hint or bounce
+ int HINT = 5; // Jump animation to suggest what to do
+ int COMPLETED = 6; // Animation loop completed. Occurs after user swipes beyond threshold
+ }
+
+ private static void moveTowardY(View view, float newY) {
+ view.setTranslationY(MathUtil.lerp(view.getTranslationY(), newY, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private static void moveTowardX(View view, float newX) {
+ view.setTranslationX(MathUtil.lerp(view.getTranslationX(), newX, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private static void fadeToward(View view, float newAlpha) {
+ view.setAlpha(MathUtil.lerp(view.getAlpha(), newAlpha, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private static void rotateToward(View view, float newRotation) {
+ view.setRotation(MathUtil.lerp(view.getRotation(), newRotation, SWIPE_LERP_PROGRESS_FACTOR));
+ }
+
+ private TextView swipeToAnswerText;
+ private TextView swipeToRejectText;
+ private View contactPuckContainer;
+ private ImageView contactPuckBackground;
+ private ImageView contactPuckIcon;
+ private View incomingDisconnectText;
+ private Animator lockBounceAnim;
+ private AnimatorSet lockEntryAnim;
+ private AnimatorSet lockHintAnim;
+ private AnimatorSet lockSettleAnim;
+ @AnimationState private int animationState = AnimationState.NONE;
+ @AnimationState private int afterSettleAnimationState = AnimationState.NONE;
+ // a value for finger swipe progress. -1 or less for "reject"; 1 or more for "accept".
+ private float swipeProgress;
+ private Animator rejectHintHide;
+ private Animator vibrationAnimator;
+ private Drawable contactPhoto;
+ private boolean incomingWillDisconnect;
+ private FlingUpDownTouchHandler touchHandler;
+ private FalsingManager falsingManager;
+
+ private AnswerHint answerHint;
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ falsingManager = new FalsingManager(getContext());
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ falsingManager.onScreenOn();
+ if (getView() != null) {
+ if (animationState == AnimationState.SWIPE || animationState == AnimationState.HINT) {
+ swipeProgress = 0;
+ updateContactPuck();
+ onMoveReset(false);
+ } else if (animationState == AnimationState.ENTRY) {
+ // When starting from the lock screen, the activity may be stopped and started briefly.
+ // Don't let that interrupt the entry animation
+ startSwipeToAnswerEntryAnimation();
+ }
+ }
+ }
+
+ @Override
+ public void onStop() {
+ endAnimation();
+ falsingManager.onScreenOff();
+ if (getActivity().isFinishing()) {
+ setAnimationState(AnimationState.COMPLETED);
+ }
+ super.onStop();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = layoutInflater.inflate(R.layout.swipe_up_down_method, viewGroup, false);
+
+ contactPuckContainer = view.findViewById(R.id.incoming_call_puck_container);
+ contactPuckBackground = (ImageView) view.findViewById(R.id.incoming_call_puck_bg);
+ contactPuckIcon = (ImageView) view.findViewById(R.id.incoming_call_puck_icon);
+ swipeToAnswerText = (TextView) view.findViewById(R.id.incoming_swipe_to_answer_text);
+ swipeToRejectText = (TextView) view.findViewById(R.id.incoming_swipe_to_reject_text);
+ incomingDisconnectText = view.findViewById(R.id.incoming_will_disconnect_text);
+ incomingDisconnectText.setAlpha(incomingWillDisconnect ? 1 : 0);
+
+ view.setAccessibilityDelegate(
+ new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(
+ new AccessibilityAction(
+ R.id.accessibility_action_answer, getString(R.string.call_incoming_answer)));
+ info.addAction(
+ new AccessibilityAction(
+ R.id.accessibility_action_decline, getString(R.string.call_incoming_decline)));
+ }
+
+ @Override
+ public boolean performAccessibilityAction(View host, int action, Bundle args) {
+ if (action == R.id.accessibility_action_answer) {
+ performAccept();
+ return true;
+ } else if (action == R.id.accessibility_action_decline) {
+ performReject();
+ return true;
+ }
+ return super.performAccessibilityAction(host, action, args);
+ }
+ });
+
+ swipeProgress = 0;
+
+ updateContactPuck();
+
+ touchHandler = FlingUpDownTouchHandler.attach(view, this, falsingManager);
+
+ answerHint =
+ new AnswerHintFactory(new EventPayloadLoaderImpl())
+ .create(getContext(), ANIMATE_DURATION_LONG_MILLIS, BOUNCE_ANIMATION_DELAY);
+ answerHint.onCreateView(
+ layoutInflater,
+ (ViewGroup) view.findViewById(R.id.hint_container),
+ contactPuckContainer,
+ swipeToAnswerText);
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ setAnimationState(AnimationState.ENTRY);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (touchHandler != null) {
+ touchHandler.detach();
+ touchHandler = null;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress) {
+ swipeProgress = progress;
+ if (animationState == AnimationState.SWIPE && getContext() != null && isVisible()) {
+ updateSwipeTextAndPuckForTouch();
+ }
+ }
+
+ @Override
+ public void onTrackingStart() {
+ setAnimationState(AnimationState.SWIPE);
+ }
+
+ @Override
+ public void onTrackingStopped() {}
+
+ @Override
+ public void onMoveReset(boolean showHint) {
+ if (showHint) {
+ showSwipeHint();
+ } else {
+ setAnimationState(AnimationState.BOUNCE);
+ }
+ resetTouchState();
+ getParent().resetAnswerProgress();
+ }
+
+ @Override
+ public void onMoveFinish(boolean accept) {
+ touchHandler.setTouchEnabled(false);
+ answerHint.onAnswered();
+ if (accept) {
+ performAccept();
+ } else {
+ performReject();
+ }
+ }
+
+ @Override
+ public boolean shouldUseFalsing(@NonNull MotionEvent downEvent) {
+ if (contactPuckContainer == null) {
+ return false;
+ }
+
+ float puckCenterX = contactPuckContainer.getX() + (contactPuckContainer.getWidth() / 2);
+ float puckCenterY = contactPuckContainer.getY() + (contactPuckContainer.getHeight() / 2);
+ double radius = contactPuckContainer.getHeight() / 2;
+
+ // Squaring a number is more performant than taking a sqrt, so we compare the square of the
+ // distance with the square of the radius.
+ double distSq =
+ Math.pow(downEvent.getX() - puckCenterX, 2) + Math.pow(downEvent.getY() - puckCenterY, 2);
+ return distSq >= Math.pow(radius, 2);
+ }
+
+ @Override
+ public void setContactPhoto(Drawable contactPhoto) {
+ this.contactPhoto = contactPhoto;
+
+ updateContactPuck();
+ }
+
+ private void updateContactPuck() {
+ if (contactPuckIcon == null) {
+ return;
+ }
+ if (getParent().isVideoCall()) {
+ contactPuckIcon.setImageResource(R.drawable.quantum_ic_videocam_white_24);
+ } else {
+ contactPuckIcon.setImageResource(R.drawable.quantum_ic_call_white_24);
+ }
+
+ int size =
+ contactPuckBackground
+ .getResources()
+ .getDimensionPixelSize(
+ shouldShowPhotoInPuck()
+ ? R.dimen.answer_contact_puck_size_photo
+ : R.dimen.answer_contact_puck_size_no_photo);
+ contactPuckBackground.setImageDrawable(
+ shouldShowPhotoInPuck()
+ ? makeRoundedDrawable(contactPuckBackground.getContext(), contactPhoto, size)
+ : null);
+ ViewGroup.LayoutParams contactPuckParams = contactPuckBackground.getLayoutParams();
+ contactPuckParams.height = size;
+ contactPuckParams.width = size;
+ contactPuckBackground.setLayoutParams(contactPuckParams);
+ contactPuckIcon.setAlpha(shouldShowPhotoInPuck() ? 0f : 1f);
+ }
+
+ private Drawable makeRoundedDrawable(Context context, Drawable contactPhoto, int size) {
+ return DrawableConverter.getRoundedDrawable(context, contactPhoto, size, size);
+ }
+
+ private boolean shouldShowPhotoInPuck() {
+ return getParent().isVideoCall() && contactPhoto != null;
+ }
+
+ @Override
+ public void setHintText(@Nullable CharSequence hintText) {
+ if (hintText == null) {
+ swipeToAnswerText.setText(R.string.call_incoming_swipe_to_answer);
+ swipeToRejectText.setText(R.string.call_incoming_swipe_to_reject);
+ } else {
+ swipeToAnswerText.setText(hintText);
+ swipeToRejectText.setText(null);
+ }
+ }
+
+ @Override
+ public void setShowIncomingWillDisconnect(boolean incomingWillDisconnect) {
+ this.incomingWillDisconnect = incomingWillDisconnect;
+ if (incomingDisconnectText != null) {
+ incomingDisconnectText.animate().alpha(incomingWillDisconnect ? 1 : 0);
+ }
+ }
+
+ private void showSwipeHint() {
+ setAnimationState(AnimationState.HINT);
+ }
+
+ private void updateSwipeTextAndPuckForTouch() {
+ // Clamp progress value between -1 and 1.
+ final float clampedProgress = MathUtil.clamp(swipeProgress, -1 /* min */, 1 /* max */);
+ final float positiveAdjustedProgress = Math.abs(clampedProgress);
+ final boolean isAcceptingFlow = clampedProgress >= 0;
+
+ // Cancel view property animators on views we're about to mutate
+ swipeToAnswerText.animate().cancel();
+ contactPuckIcon.animate().cancel();
+
+ // Since the animation progression is controlled by user gesture instead of real timeline, the
+ // spec timeline can be divided into 9 slots. Each slot is equivalent to 83ms in the spec.
+ // Therefore, we use 9 slots of 83ms to map user gesture into the spec timeline.
+ final float progressSlots = 9;
+
+ // Fade out the "swipe up to answer". It only takes 1 slot to complete the fade.
+ float swipeTextAlpha = Math.max(0, 1 - Math.abs(clampedProgress) * progressSlots);
+ fadeToward(swipeToAnswerText, swipeTextAlpha);
+ // Fade out the "swipe down to dismiss" at the same time. Don't ever increase its alpha
+ fadeToward(swipeToRejectText, Math.min(swipeTextAlpha, swipeToRejectText.getAlpha()));
+ // Fade out the "incoming will disconnect" text
+ fadeToward(incomingDisconnectText, incomingWillDisconnect ? swipeTextAlpha : 0);
+
+ // Move swipe text back to zero.
+ moveTowardX(swipeToAnswerText, 0 /* newX */);
+ moveTowardY(swipeToAnswerText, 0 /* newY */);
+
+ // Animate puck color
+ @ColorInt
+ int destPuckColor =
+ getContext()
+ .getColor(
+ isAcceptingFlow ? R.color.call_accept_background : R.color.call_hangup_background);
+ destPuckColor =
+ ColorUtils.setAlphaComponent(destPuckColor, (int) (0xFF * positiveAdjustedProgress));
+ contactPuckBackground.setBackgroundTintList(ColorStateList.valueOf(destPuckColor));
+ contactPuckBackground.setBackgroundTintMode(Mode.SRC_ATOP);
+ contactPuckBackground.setColorFilter(destPuckColor);
+
+ // Animate decline icon
+ if (isAcceptingFlow || getParent().isVideoCall()) {
+ rotateToward(contactPuckIcon, 0f);
+ } else {
+ rotateToward(contactPuckIcon, positiveAdjustedProgress * ICON_END_CALL_ROTATION_DEGREES);
+ }
+
+ // Fade in icon
+ if (shouldShowPhotoInPuck()) {
+ fadeToward(contactPuckIcon, positiveAdjustedProgress);
+ }
+ float iconProgress = Math.min(1f, positiveAdjustedProgress * 4);
+ @ColorInt
+ int iconColor =
+ ColorUtils.setAlphaComponent(
+ contactPuckIcon.getContext().getColor(R.color.incoming_answer_icon),
+ (int) (0xFF * (1 - iconProgress)));
+ contactPuckIcon.setImageTintList(ColorStateList.valueOf(iconColor));
+
+ // Move puck.
+ if (isAcceptingFlow) {
+ moveTowardY(
+ contactPuckContainer,
+ -clampedProgress * DpUtil.dpToPx(getContext(), SWIPE_TO_ANSWER_MAX_TRANSLATION_Y_DP));
+ } else {
+ moveTowardY(
+ contactPuckContainer,
+ -clampedProgress * DpUtil.dpToPx(getContext(), SWIPE_TO_REJECT_MAX_TRANSLATION_Y_DP));
+ }
+
+ getParent().onAnswerProgressUpdate(clampedProgress);
+ }
+
+ private void startSwipeToAnswerSwipeAnimation() {
+ LogUtil.i("FlingUpDownMethod.startSwipeToAnswerSwipeAnimation", "Start swipe animation.");
+ resetTouchState();
+ endAnimation();
+ }
+
+ private void setPuckTouchState() {
+ contactPuckBackground.setActivated(touchHandler.isTracking());
+ }
+
+ private void resetTouchState() {
+ if (getContext() == null) {
+ // State will be reset in onStart(), so just abort.
+ return;
+ }
+ contactPuckContainer.animate().scaleX(1 /* scaleX */);
+ contactPuckContainer.animate().scaleY(1 /* scaleY */);
+ contactPuckBackground.animate().scaleX(1 /* scaleX */);
+ contactPuckBackground.animate().scaleY(1 /* scaleY */);
+ contactPuckBackground.setBackgroundTintList(null);
+ contactPuckBackground.setColorFilter(null);
+ contactPuckIcon.setImageTintList(
+ ColorStateList.valueOf(getContext().getColor(R.color.incoming_answer_icon)));
+ contactPuckIcon.animate().rotation(0);
+
+ getParent().resetAnswerProgress();
+ setPuckTouchState();
+
+ final float alpha = 1;
+ swipeToAnswerText.animate().alpha(alpha);
+ contactPuckContainer.animate().alpha(alpha);
+ contactPuckBackground.animate().alpha(alpha);
+ contactPuckIcon.animate().alpha(shouldShowPhotoInPuck() ? 0 : alpha);
+ }
+
+ @VisibleForTesting
+ void setAnimationState(@AnimationState int state) {
+ if (state != AnimationState.HINT && animationState == state) {
+ return;
+ }
+
+ if (animationState == AnimationState.COMPLETED) {
+ LogUtil.e(
+ "FlingUpDownMethod.setAnimationState",
+ "Animation loop has completed. Cannot switch to new state: " + state);
+ return;
+ }
+
+ if (state == AnimationState.HINT || state == AnimationState.BOUNCE) {
+ if (animationState == AnimationState.SWIPE) {
+ afterSettleAnimationState = state;
+ state = AnimationState.SETTLE;
+ }
+ }
+
+ LogUtil.i("FlingUpDownMethod.setAnimationState", "animation state: " + state);
+ animationState = state;
+
+ // Start animation after the current one is finished completely.
+ View view = getView();
+ if (view != null) {
+ // As long as the fragment is added, we can start update the animation state.
+ if (isAdded() && (animationState == state)) {
+ updateAnimationState();
+ } else {
+ endAnimation();
+ }
+ }
+ }
+
+ @AnimationState
+ @VisibleForTesting
+ int getAnimationState() {
+ return animationState;
+ }
+
+ private void updateAnimationState() {
+ switch (animationState) {
+ case AnimationState.ENTRY:
+ startSwipeToAnswerEntryAnimation();
+ break;
+ case AnimationState.BOUNCE:
+ startSwipeToAnswerBounceAnimation();
+ break;
+ case AnimationState.SWIPE:
+ startSwipeToAnswerSwipeAnimation();
+ break;
+ case AnimationState.SETTLE:
+ startSwipeToAnswerSettleAnimation();
+ break;
+ case AnimationState.COMPLETED:
+ clearSwipeToAnswerUi();
+ break;
+ case AnimationState.HINT:
+ startSwipeToAnswerHintAnimation();
+ break;
+ case AnimationState.NONE:
+ default:
+ LogUtil.e(
+ "FlingUpDownMethod.updateAnimationState",
+ "Unexpected animation state: " + animationState);
+ break;
+ }
+ }
+
+ private void startSwipeToAnswerEntryAnimation() {
+ LogUtil.i("FlingUpDownMethod.startSwipeToAnswerEntryAnimation", "Swipe entry animation.");
+ endAnimation();
+
+ lockEntryAnim = new AnimatorSet();
+ Animator textUp =
+ ObjectAnimator.ofFloat(
+ swipeToAnswerText,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), 192 /* dp */),
+ DpUtil.dpToPx(getContext(), -20 /* dp */));
+ textUp.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+ textUp.setInterpolator(new LinearOutSlowInInterpolator());
+
+ Animator textDown =
+ ObjectAnimator.ofFloat(
+ swipeToAnswerText,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), -20) /* dp */,
+ 0 /* end pos */);
+ textDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+ textUp.setInterpolator(new FastOutSlowInInterpolator());
+
+ // "Swipe down to reject" text fades in with a slight translation
+ swipeToRejectText.setAlpha(0f);
+ Animator rejectTextShow =
+ ObjectAnimator.ofPropertyValuesHolder(
+ swipeToRejectText,
+ PropertyValuesHolder.ofFloat(View.ALPHA, 1f),
+ PropertyValuesHolder.ofFloat(
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), HINT_REJECT_FADE_TRANSLATION_Y_DP),
+ 0f));
+ rejectTextShow.setInterpolator(new FastOutLinearInInterpolator());
+ rejectTextShow.setDuration(ANIMATE_DURATION_SHORT_MILLIS);
+ rejectTextShow.setStartDelay(SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS);
+
+ Animator puckUp =
+ ObjectAnimator.ofFloat(
+ contactPuckContainer,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), 400 /* dp */),
+ DpUtil.dpToPx(getContext(), -12 /* dp */));
+ puckUp.setDuration(ANIMATE_DURATION_LONG_MILLIS);
+ puckUp.setInterpolator(
+ PathInterpolatorCompat.create(
+ 0 /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */));
+
+ Animator puckDown =
+ ObjectAnimator.ofFloat(
+ contactPuckContainer,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), -12 /* dp */),
+ 0 /* end pos */);
+ puckDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+ puckDown.setInterpolator(new FastOutSlowInInterpolator());
+
+ Animator puckScaleUp =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ 0.33f /* beginScale */,
+ 1.1f /* endScale */,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ PathInterpolatorCompat.create(
+ 0.4f /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */));
+ Animator puckScaleDown =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ 1.1f /* beginScale */,
+ 1 /* endScale */,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ new FastOutSlowInInterpolator());
+
+ // Upward animation chain.
+ lockEntryAnim.play(textUp).with(puckScaleUp).with(puckUp);
+
+ // Downward animation chain.
+ lockEntryAnim.play(textDown).with(puckDown).with(puckScaleDown).after(puckUp);
+
+ lockEntryAnim.play(rejectTextShow).after(puckUp);
+
+ // Add vibration animation.
+ addVibrationAnimator(lockEntryAnim);
+
+ lockEntryAnim.addListener(
+ new AnimatorListenerAdapter() {
+
+ public boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (!canceled) {
+ onEntryAnimationDone();
+ }
+ }
+ });
+ lockEntryAnim.start();
+ }
+
+ @VisibleForTesting
+ void onEntryAnimationDone() {
+ LogUtil.i("FlingUpDownMethod.onEntryAnimationDone", "Swipe entry anim ends.");
+ if (animationState == AnimationState.ENTRY) {
+ setAnimationState(AnimationState.BOUNCE);
+ }
+ }
+
+ private void startSwipeToAnswerBounceAnimation() {
+ LogUtil.i("FlingUpDownMethod.startSwipeToAnswerBounceAnimation", "Swipe bounce animation.");
+ endAnimation();
+
+ if (ViewUtil.areAnimationsDisabled(getContext())) {
+ swipeToAnswerText.setTranslationY(0);
+ contactPuckContainer.setTranslationY(0);
+ contactPuckBackground.setScaleY(1f);
+ contactPuckBackground.setScaleX(1f);
+ swipeToRejectText.setAlpha(1f);
+ swipeToRejectText.setTranslationY(0);
+ return;
+ }
+
+ lockBounceAnim = createBreatheAnimation();
+
+ answerHint.onBounceStart();
+ lockBounceAnim.addListener(
+ new AnimatorListenerAdapter() {
+ boolean firstPass = true;
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (getContext() != null
+ && lockBounceAnim != null
+ && animationState == AnimationState.BOUNCE) {
+ // AnimatorSet doesn't have repeat settings. Instead, we start a new one after the
+ // previous set is completed, until endAnimation is called.
+ LogUtil.v("FlingUpDownMethod.onAnimationEnd", "Bounce again.");
+
+ // If this is the first time repeating the animation, we should recreate it so its
+ // starting values will be correct
+ if (firstPass) {
+ lockBounceAnim = createBreatheAnimation();
+ lockBounceAnim.addListener(this);
+ }
+ firstPass = false;
+ answerHint.onBounceStart();
+ lockBounceAnim.start();
+ }
+ }
+ });
+ lockBounceAnim.start();
+ }
+
+ private Animator createBreatheAnimation() {
+ AnimatorSet breatheAnimation = new AnimatorSet();
+ float textOffset = DpUtil.dpToPx(getContext(), 42 /* dp */);
+ Animator textUp =
+ ObjectAnimator.ofFloat(
+ swipeToAnswerText, View.TRANSLATION_Y, 0 /* begin pos */, -textOffset);
+ textUp.setInterpolator(new FastOutSlowInInterpolator());
+ textUp.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ Animator textDown =
+ ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, -textOffset, 0 /* end pos */);
+ textDown.setInterpolator(new FastOutSlowInInterpolator());
+ textDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ // "Swipe down to reject" text fade in
+ Animator rejectTextShow = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 1f);
+ rejectTextShow.setInterpolator(new LinearOutSlowInInterpolator());
+ rejectTextShow.setDuration(ANIMATE_DURATION_SHORT_MILLIS);
+ rejectTextShow.setStartDelay(SWIPE_TO_DECLINE_FADE_IN_DELAY_MILLIS);
+
+ // reject hint text translate in
+ Animator rejectTextTranslate =
+ ObjectAnimator.ofFloat(
+ swipeToRejectText,
+ View.TRANSLATION_Y,
+ DpUtil.dpToPx(getContext(), HINT_REJECT_FADE_TRANSLATION_Y_DP),
+ 0f);
+ rejectTextTranslate.setInterpolator(new FastOutSlowInInterpolator());
+ rejectTextTranslate.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ // reject hint text fade out
+ Animator rejectTextHide = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 0f);
+ rejectTextHide.setInterpolator(new FastOutLinearInInterpolator());
+ rejectTextHide.setDuration(ANIMATE_DURATION_SHORT_MILLIS);
+
+ Interpolator curve =
+ PathInterpolatorCompat.create(
+ 0.4f /* controlX1 */, 0 /* controlY1 */, 0 /* controlX2 */, 1 /* controlY2 */);
+ float puckOffset = DpUtil.dpToPx(getContext(), 42 /* dp */);
+ Animator puckUp = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, -puckOffset);
+ puckUp.setInterpolator(curve);
+ puckUp.setDuration(ANIMATE_DURATION_LONG_MILLIS);
+
+ final float scale = 1.0625f;
+ Animator puckScaleUp =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ 1 /* beginScale */,
+ scale,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ curve);
+
+ Animator puckDown =
+ ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, 0 /* end pos */);
+ puckDown.setInterpolator(new FastOutSlowInInterpolator());
+ puckDown.setDuration(ANIMATE_DURATION_NORMAL_MILLIS);
+
+ Animator puckScaleDown =
+ createUniformScaleAnimators(
+ contactPuckBackground,
+ scale,
+ 1 /* endScale */,
+ ANIMATE_DURATION_NORMAL_MILLIS,
+ new FastOutSlowInInterpolator());
+
+ // Bounce upward animation chain.
+ breatheAnimation
+ .play(textUp)
+ .with(rejectTextHide)
+ .with(puckUp)
+ .with(puckScaleUp)
+ .after(167 /* delay */);
+
+ // Bounce downward animation chain.
+ breatheAnimation
+ .play(puckDown)
+ .with(textDown)
+ .with(puckScaleDown)
+ .with(rejectTextShow)
+ .with(rejectTextTranslate)
+ .after(puckUp);
+
+ // Add vibration animation to the animator set.
+ addVibrationAnimator(breatheAnimation);
+
+ return breatheAnimation;
+ }
+
+ private void startSwipeToAnswerSettleAnimation() {
+ endAnimation();
+
+ ObjectAnimator puckScale =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckBackground,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 1),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, 1));
+ puckScale.setDuration(SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator iconRotation = ObjectAnimator.ofFloat(contactPuckIcon, View.ROTATION, 0);
+ iconRotation.setDuration(SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator swipeToAnswerTextFade =
+ createFadeAnimation(swipeToAnswerText, 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckContainerFade =
+ createFadeAnimation(contactPuckContainer, 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckBackgroundFade =
+ createFadeAnimation(contactPuckBackground, 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckIconFade =
+ createFadeAnimation(
+ contactPuckIcon, shouldShowPhotoInPuck() ? 0 : 1, SETTLE_ANIMATION_DURATION_MILLIS);
+
+ ObjectAnimator contactPuckTranslation =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckContainer,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0));
+ contactPuckTranslation.setDuration(SETTLE_ANIMATION_DURATION_MILLIS);
+
+ lockSettleAnim = new AnimatorSet();
+ lockSettleAnim
+ .play(puckScale)
+ .with(iconRotation)
+ .with(swipeToAnswerTextFade)
+ .with(contactPuckContainerFade)
+ .with(contactPuckBackgroundFade)
+ .with(contactPuckIconFade)
+ .with(contactPuckTranslation);
+
+ lockSettleAnim.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ afterSettleAnimationState = AnimationState.NONE;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ onSettleAnimationDone();
+ }
+ });
+
+ lockSettleAnim.start();
+ }
+
+ @VisibleForTesting
+ void onSettleAnimationDone() {
+ if (afterSettleAnimationState != AnimationState.NONE) {
+ int nextState = afterSettleAnimationState;
+ afterSettleAnimationState = AnimationState.NONE;
+ lockSettleAnim = null;
+
+ setAnimationState(nextState);
+ }
+ }
+
+ private ObjectAnimator createFadeAnimation(View target, float targetAlpha, long duration) {
+ ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(target, View.ALPHA, targetAlpha);
+ objectAnimator.setDuration(duration);
+ return objectAnimator;
+ }
+
+ private void startSwipeToAnswerHintAnimation() {
+ if (rejectHintHide != null) {
+ rejectHintHide.cancel();
+ }
+
+ endAnimation();
+ resetTouchState();
+
+ if (ViewUtil.areAnimationsDisabled(getContext())) {
+ onHintAnimationDone(false);
+ return;
+ }
+
+ lockHintAnim = new AnimatorSet();
+ float jumpOffset = DpUtil.dpToPx(getContext(), HINT_JUMP_DP);
+ float dipOffset = DpUtil.dpToPx(getContext(), HINT_DIP_DP);
+ float scaleSize = HINT_SCALE_RATIO;
+ float textOffset = jumpOffset + (scaleSize - 1) * contactPuckBackground.getHeight();
+ int shortAnimTime =
+ getContext().getResources().getInteger(android.R.integer.config_shortAnimTime);
+ int mediumAnimTime =
+ getContext().getResources().getInteger(android.R.integer.config_mediumAnimTime);
+
+ // Puck squashes to anticipate jump
+ ObjectAnimator puckAnticipate =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckContainer,
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, .95f),
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 1.05f));
+ puckAnticipate.setRepeatCount(1);
+ puckAnticipate.setRepeatMode(ValueAnimator.REVERSE);
+ puckAnticipate.setDuration(shortAnimTime / 2);
+ puckAnticipate.setInterpolator(new DecelerateInterpolator());
+ puckAnticipate.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ contactPuckContainer.setPivotY(contactPuckContainer.getHeight());
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ contactPuckContainer.setPivotY(contactPuckContainer.getHeight() / 2);
+ }
+ });
+
+ // Ensure puck is at the right starting point for the jump
+ ObjectAnimator puckResetTranslation =
+ ObjectAnimator.ofPropertyValuesHolder(
+ contactPuckContainer,
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_X, 0));
+ puckResetTranslation.setDuration(shortAnimTime / 2);
+ puckAnticipate.setInterpolator(new DecelerateInterpolator());
+
+ Animator textUp = ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, -textOffset);
+ textUp.setInterpolator(new LinearOutSlowInInterpolator());
+ textUp.setDuration(shortAnimTime);
+
+ Animator puckUp = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, -jumpOffset);
+ puckUp.setInterpolator(new LinearOutSlowInInterpolator());
+ puckUp.setDuration(shortAnimTime);
+
+ Animator puckScaleUp =
+ createUniformScaleAnimators(
+ contactPuckBackground, 1f, scaleSize, shortAnimTime, new LinearOutSlowInInterpolator());
+
+ Animator rejectHintShow =
+ ObjectAnimator.ofPropertyValuesHolder(
+ swipeToRejectText,
+ PropertyValuesHolder.ofFloat(View.ALPHA, 1f),
+ PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, 0f));
+ rejectHintShow.setDuration(shortAnimTime);
+
+ Animator rejectHintDip =
+ ObjectAnimator.ofFloat(swipeToRejectText, View.TRANSLATION_Y, dipOffset);
+ rejectHintDip.setInterpolator(new LinearOutSlowInInterpolator());
+ rejectHintDip.setDuration(shortAnimTime);
+
+ Animator textDown = ObjectAnimator.ofFloat(swipeToAnswerText, View.TRANSLATION_Y, 0);
+ textDown.setInterpolator(new LinearOutSlowInInterpolator());
+ textDown.setDuration(mediumAnimTime);
+
+ Animator puckDown = ObjectAnimator.ofFloat(contactPuckContainer, View.TRANSLATION_Y, 0);
+ BounceInterpolator bounce = new BounceInterpolator();
+ puckDown.setInterpolator(bounce);
+ puckDown.setDuration(mediumAnimTime);
+
+ Animator puckScaleDown =
+ createUniformScaleAnimators(
+ contactPuckBackground, scaleSize, 1f, shortAnimTime, new LinearOutSlowInInterpolator());
+
+ Animator rejectHintUp = ObjectAnimator.ofFloat(swipeToRejectText, View.TRANSLATION_Y, 0);
+ rejectHintUp.setInterpolator(new LinearOutSlowInInterpolator());
+ rejectHintUp.setDuration(mediumAnimTime);
+
+ lockHintAnim.play(puckAnticipate).with(puckResetTranslation).before(puckUp);
+ lockHintAnim
+ .play(textUp)
+ .with(puckUp)
+ .with(puckScaleUp)
+ .with(rejectHintDip)
+ .with(rejectHintShow);
+ lockHintAnim.play(textDown).with(puckDown).with(puckScaleDown).with(rejectHintUp).after(puckUp);
+ lockHintAnim.start();
+
+ rejectHintHide = ObjectAnimator.ofFloat(swipeToRejectText, View.ALPHA, 0);
+ rejectHintHide.setStartDelay(HINT_REJECT_SHOW_DURATION_MILLIS);
+ rejectHintHide.addListener(
+ new AnimatorListenerAdapter() {
+
+ private boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ super.onAnimationCancel(animation);
+ canceled = true;
+ rejectHintHide = null;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ onHintAnimationDone(canceled);
+ }
+ });
+ rejectHintHide.start();
+ }
+
+ @VisibleForTesting
+ void onHintAnimationDone(boolean canceled) {
+ if (!canceled && animationState == AnimationState.HINT) {
+ setAnimationState(AnimationState.BOUNCE);
+ }
+ rejectHintHide = null;
+ }
+
+ private void clearSwipeToAnswerUi() {
+ LogUtil.i("FlingUpDownMethod.clearSwipeToAnswerUi", "Clear swipe animation.");
+ endAnimation();
+ swipeToAnswerText.setVisibility(View.GONE);
+ contactPuckContainer.setVisibility(View.GONE);
+ }
+
+ private void endAnimation() {
+ LogUtil.i("FlingUpDownMethod.endAnimation", "End animations.");
+ if (lockSettleAnim != null) {
+ lockSettleAnim.cancel();
+ lockSettleAnim = null;
+ }
+ if (lockBounceAnim != null) {
+ lockBounceAnim.cancel();
+ lockBounceAnim = null;
+ }
+ if (lockEntryAnim != null) {
+ lockEntryAnim.cancel();
+ lockEntryAnim = null;
+ }
+ if (lockHintAnim != null) {
+ lockHintAnim.cancel();
+ lockHintAnim = null;
+ }
+ if (rejectHintHide != null) {
+ rejectHintHide.cancel();
+ rejectHintHide = null;
+ }
+ if (vibrationAnimator != null) {
+ vibrationAnimator.end();
+ vibrationAnimator = null;
+ }
+ answerHint.onBounceEnd();
+ }
+
+ // Create an animator to scale on X/Y directions uniformly.
+ private Animator createUniformScaleAnimators(
+ View target, float begin, float end, long duration, Interpolator interpolator) {
+ ObjectAnimator animator =
+ ObjectAnimator.ofPropertyValuesHolder(
+ target,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, begin, end),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, begin, end));
+ animator.setDuration(duration);
+ animator.setInterpolator(interpolator);
+ return animator;
+ }
+
+ private void addVibrationAnimator(AnimatorSet animatorSet) {
+ if (vibrationAnimator != null) {
+ vibrationAnimator.end();
+ }
+
+ // Note that we animate the value between 0 and 1, but internally VibrateInterpolator will
+ // translate it into actually X translation value.
+ vibrationAnimator =
+ ObjectAnimator.ofFloat(
+ contactPuckContainer, View.TRANSLATION_X, 0 /* begin value */, 1 /* end value */);
+ vibrationAnimator.setDuration(VIBRATION_TIME_MILLIS);
+ vibrationAnimator.setInterpolator(new VibrateInterpolator(getContext()));
+
+ animatorSet.play(vibrationAnimator).after(0 /* delay */);
+ }
+
+ private void performAccept() {
+ LogUtil.i("FlingUpDownMethod.performAccept", null);
+ swipeToAnswerText.setVisibility(View.GONE);
+ contactPuckContainer.setVisibility(View.GONE);
+
+ // Complete the animation loop.
+ setAnimationState(AnimationState.COMPLETED);
+ getParent().answerFromMethod();
+ }
+
+ private void performReject() {
+ LogUtil.i("FlingUpDownMethod.performReject", null);
+ swipeToAnswerText.setVisibility(View.GONE);
+ contactPuckContainer.setVisibility(View.GONE);
+
+ // Complete the animation loop.
+ setAnimationState(AnimationState.COMPLETED);
+ getParent().rejectFromMethod();
+ }
+
+ /** Custom interpolator class for puck vibration. */
+ private static class VibrateInterpolator implements Interpolator {
+
+ private static final long RAMP_UP_BEGIN_MS = 583;
+ private static final long RAMP_UP_DURATION_MS = 167;
+ private static final long RAMP_UP_END_MS = RAMP_UP_BEGIN_MS + RAMP_UP_DURATION_MS;
+ private static final long RAMP_DOWN_BEGIN_MS = 1_583;
+ private static final long RAMP_DOWN_DURATION_MS = 250;
+ private static final long RAMP_DOWN_END_MS = RAMP_DOWN_BEGIN_MS + RAMP_DOWN_DURATION_MS;
+ private static final long RAMP_TOTAL_TIME_MS = RAMP_DOWN_END_MS;
+ private final float ampMax;
+ private final float freqMax = 80;
+ private Interpolator sliderInterpolator = new FastOutSlowInInterpolator();
+
+ VibrateInterpolator(Context context) {
+ ampMax = DpUtil.dpToPx(context, 1 /* dp */);
+ }
+
+ @Override
+ public float getInterpolation(float t) {
+ float slider = 0;
+ float time = t * RAMP_TOTAL_TIME_MS;
+
+ // Calculate the slider value based on RAMP_UP and RAMP_DOWN times. Between RAMP_UP and
+ // RAMP_DOWN, the slider remains the maximum value of 1.
+ if (time > RAMP_UP_BEGIN_MS && time < RAMP_UP_END_MS) {
+ // Ramp up.
+ slider =
+ sliderInterpolator.getInterpolation(
+ (time - RAMP_UP_BEGIN_MS) / (float) RAMP_UP_DURATION_MS);
+ } else if ((time >= RAMP_UP_END_MS) && time <= RAMP_DOWN_BEGIN_MS) {
+ // Vibrate at maximum
+ slider = 1;
+ } else if (time > RAMP_DOWN_BEGIN_MS && time < RAMP_DOWN_END_MS) {
+ // Ramp down.
+ slider =
+ 1
+ - sliderInterpolator.getInterpolation(
+ (time - RAMP_DOWN_BEGIN_MS) / (float) RAMP_DOWN_DURATION_MS);
+ }
+
+ float ampNormalized = ampMax * slider;
+ float freqNormalized = freqMax * slider;
+
+ return (float) (ampNormalized * Math.sin(time * freqNormalized));
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
new file mode 100644
index 000000000..a21073d65
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
@@ -0,0 +1,496 @@
+/*
+ * 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.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import com.android.dialer.common.DpUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.MathUtil;
+import com.android.incallui.answer.impl.classifier.FalsingManager;
+import com.android.incallui.answer.impl.utils.FlingAnimationUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Touch handler that keeps track of flings for {@link FlingUpDownMethod}. */
+@SuppressLint("ClickableViewAccessibility")
+class FlingUpDownTouchHandler implements OnTouchListener {
+
+ /** Callback interface for significant events with this touch handler */
+ interface OnProgressChangedListener {
+
+ /**
+ * Called when the visible answer progress has changed. Implementations should use this for
+ * animation, but should not perform accepts or rejects until {@link #onMoveFinish(boolean)} is
+ * called.
+ *
+ * @param progress float representation of the progress with +1f fully accepted, -1f fully
+ * rejected, and 0 neutral.
+ */
+ void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress);
+
+ /** Called when a touch event has started being tracked. */
+ void onTrackingStart();
+
+ /** Called when touch events stop being tracked. */
+ void onTrackingStopped();
+
+ /**
+ * Called when the progress has fully animated back to neutral. Normal resting animation should
+ * resume, possibly with a hint animation first.
+ *
+ * @param showHint {@code true} iff the hint animation should be run before resuming normal
+ * animation.
+ */
+ void onMoveReset(boolean showHint);
+
+ /**
+ * Called when the progress has animated fully to accept or reject.
+ *
+ * @param accept {@code true} if the call has been accepted, {@code false} if it has been
+ * rejected.
+ */
+ void onMoveFinish(boolean accept);
+
+ /**
+ * Determine whether this gesture should use the {@link FalsingManager} to reject accidental
+ * touches
+ *
+ * @param downEvent the MotionEvent corresponding to the start of the gesture
+ * @return {@code true} if the {@link FalsingManager} should be used to reject accidental
+ * touches for this gesture
+ */
+ boolean shouldUseFalsing(@NonNull MotionEvent downEvent);
+ }
+
+ // Progress that must be moved through to not show the hint animation after gesture completes
+ private static final float HINT_MOVE_THRESHOLD_RATIO = .1f;
+ // Dp touch needs to move upward to be considered fully accepted
+ private static final int ACCEPT_THRESHOLD_DP = 150;
+ // Dp touch needs to move downward to be considered fully rejected
+ private static final int REJECT_THRESHOLD_DP = 150;
+ // Dp touch needs to move for it to not be considered a false touch (if FalsingManager is not
+ // enabled)
+ private static final int FALSING_THRESHOLD_DP = 40;
+
+ // Progress at which a fling in the opposite direction will recenter instead of
+ // accepting/rejecting
+ private static final float PROGRESS_FLING_RECENTER = .1f;
+
+ // Progress at which a slow swipe would continue toward accept/reject after the
+ // touch has been let go, otherwise will recenter
+ private static final float PROGRESS_SWIPE_RECENTER = .8f;
+
+ private static final float REJECT_FLING_THRESHOLD_MODIFIER = 2f;
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({FlingTarget.CENTER, FlingTarget.ACCEPT, FlingTarget.REJECT})
+ private @interface FlingTarget {
+ int CENTER = 0;
+ int ACCEPT = 1;
+ int REJECT = -1;
+ }
+
+ /**
+ * Create a new FlingUpDownTouchHandler and attach it to the target. Will call {@link
+ * View#setOnTouchListener(OnTouchListener)} before returning.
+ *
+ * @param target View whose touches are to be listened to
+ * @param listener Callback to listen to major events
+ * @param falsingManager FalsingManager to identify false touches
+ * @return the instance of FlingUpDownTouchHandler that has been added as a touch listener
+ */
+ public static FlingUpDownTouchHandler attach(
+ @NonNull View target,
+ @NonNull OnProgressChangedListener listener,
+ @Nullable FalsingManager falsingManager) {
+ FlingUpDownTouchHandler handler = new FlingUpDownTouchHandler(target, listener, falsingManager);
+ target.setOnTouchListener(handler);
+ return handler;
+ }
+
+ @NonNull private final View target;
+ @NonNull private final OnProgressChangedListener listener;
+
+ private VelocityTracker velocityTracker;
+ private FlingAnimationUtils flingAnimationUtils;
+
+ private boolean touchEnabled = true;
+ private boolean flingEnabled = true;
+ private float currentProgress;
+ private boolean tracking;
+
+ private boolean motionAborted;
+ private boolean touchSlopExceeded;
+ private boolean hintDistanceExceeded;
+ private int trackingPointer;
+ private Animator progressAnimator;
+
+ private float touchSlop;
+ private float initialTouchY;
+ private float acceptThresholdY;
+ private float rejectThresholdY;
+ private float zeroY;
+
+ private boolean touchAboveFalsingThreshold;
+ private float falsingThresholdPx;
+ private boolean touchUsesFalsing;
+
+ private final float acceptThresholdPx;
+ private final float rejectThresholdPx;
+ private final float deadZoneTopPx;
+
+ @Nullable private final FalsingManager falsingManager;
+
+ private FlingUpDownTouchHandler(
+ @NonNull View target,
+ @NonNull OnProgressChangedListener listener,
+ @Nullable FalsingManager falsingManager) {
+ this.target = target;
+ this.listener = listener;
+ Context context = target.getContext();
+ touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ flingAnimationUtils = new FlingAnimationUtils(context, .6f);
+ falsingThresholdPx = DpUtil.dpToPx(context, FALSING_THRESHOLD_DP);
+ acceptThresholdPx = DpUtil.dpToPx(context, ACCEPT_THRESHOLD_DP);
+ rejectThresholdPx = DpUtil.dpToPx(context, REJECT_THRESHOLD_DP);
+
+ deadZoneTopPx =
+ Math.max(
+ context.getResources().getDimension(R.dimen.answer_swipe_dead_zone_top),
+ acceptThresholdPx);
+ this.falsingManager = falsingManager;
+ }
+
+ /** Returns {@code true} iff a touch is being tracked */
+ public boolean isTracking() {
+ return tracking;
+ }
+
+ /**
+ * Sets whether touch events will continue to be listened to
+ *
+ * @param touchEnabled whether future touch events will be listened to
+ */
+ public void setTouchEnabled(boolean touchEnabled) {
+ this.touchEnabled = touchEnabled;
+ }
+
+ /**
+ * Sets whether fling velocity is used to affect accept/reject behavior
+ *
+ * @param flingEnabled whether fling velocity will be used when determining whether to
+ * accept/reject or recenter
+ */
+ public void setFlingEnabled(boolean flingEnabled) {
+ this.flingEnabled = flingEnabled;
+ }
+
+ public void detach() {
+ cancelProgressAnimator();
+ setTouchEnabled(false);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (falsingManager != null) {
+ falsingManager.onTouchEvent(event);
+ }
+ if (!touchEnabled) {
+ return false;
+ }
+ if (motionAborted && (event.getActionMasked() != MotionEvent.ACTION_DOWN)) {
+ return false;
+ }
+
+ int pointerIndex = event.findPointerIndex(trackingPointer);
+ if (pointerIndex < 0) {
+ pointerIndex = 0;
+ trackingPointer = event.getPointerId(pointerIndex);
+ }
+ final float pointerY = event.getY(pointerIndex);
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_DOWN:
+ if (pointerY < deadZoneTopPx) {
+ return false;
+ }
+ motionAborted = false;
+ startMotion(pointerY, false, currentProgress);
+ touchAboveFalsingThreshold = false;
+ touchUsesFalsing = listener.shouldUseFalsing(event);
+ if (velocityTracker == null) {
+ initVelocityTracker();
+ }
+ trackMovement(event);
+ cancelProgressAnimator();
+ touchSlopExceeded = progressAnimator != null;
+ onTrackingStarted();
+ break;
+ case MotionEvent.ACTION_POINTER_UP:
+ final int upPointer = event.getPointerId(event.getActionIndex());
+ if (trackingPointer == upPointer) {
+ // gesture is ongoing, find a new pointer to track
+ int newIndex = event.getPointerId(0) != upPointer ? 0 : 1;
+ float newY = event.getY(newIndex);
+ trackingPointer = event.getPointerId(newIndex);
+ startMotion(newY, true, currentProgress);
+ }
+ break;
+ case MotionEvent.ACTION_POINTER_DOWN:
+ motionAborted = true;
+ endMotionEvent(event, pointerY, true);
+ return false;
+ case MotionEvent.ACTION_MOVE:
+ float deltaY = pointerY - initialTouchY;
+
+ if (Math.abs(deltaY) > touchSlop) {
+ touchSlopExceeded = true;
+ }
+ if (Math.abs(deltaY) >= falsingThresholdPx) {
+ touchAboveFalsingThreshold = true;
+ }
+ setCurrentProgress(pointerYToProgress(pointerY));
+ trackMovement(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ trackMovement(event);
+ endMotionEvent(event, pointerY, false);
+ }
+ return true;
+ }
+
+ private void endMotionEvent(MotionEvent event, float pointerY, boolean forceCancel) {
+ trackingPointer = -1;
+ if ((tracking && touchSlopExceeded)
+ || Math.abs(pointerY - initialTouchY) > touchSlop
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || forceCancel) {
+ float vel = 0f;
+ float vectorVel = 0f;
+ if (velocityTracker != null) {
+ velocityTracker.computeCurrentVelocity(1000);
+ vel = velocityTracker.getYVelocity();
+ vectorVel =
+ Math.copySign(
+ (float) Math.hypot(velocityTracker.getXVelocity(), velocityTracker.getYVelocity()),
+ vel);
+ }
+
+ boolean falseTouch = isFalseTouch();
+ boolean forceRecenter =
+ falseTouch
+ || !touchSlopExceeded
+ || forceCancel
+ || event.getActionMasked() == MotionEvent.ACTION_CANCEL;
+
+ @FlingTarget
+ int target = forceRecenter ? FlingTarget.CENTER : getFlingTarget(pointerY, vectorVel);
+
+ fling(vel, target, falseTouch);
+ onTrackingStopped();
+ } else {
+ onTrackingStopped();
+ setCurrentProgress(0);
+ onMoveEnded();
+ }
+
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ velocityTracker = null;
+ }
+ }
+
+ @FlingTarget
+ private int getFlingTarget(float pointerY, float vectorVel) {
+ float progress = pointerYToProgress(pointerY);
+
+ float minVelocityPxPerSecond = flingAnimationUtils.getMinVelocityPxPerSecond();
+ if (vectorVel > 0) {
+ minVelocityPxPerSecond *= REJECT_FLING_THRESHOLD_MODIFIER;
+ }
+ if (!flingEnabled || Math.abs(vectorVel) < minVelocityPxPerSecond) {
+ // Not a fling
+ if (Math.abs(progress) > PROGRESS_SWIPE_RECENTER) {
+ // Progress near one of the edges
+ return progress > 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
+ } else {
+ return FlingTarget.CENTER;
+ }
+ }
+
+ boolean sameDirection = vectorVel < 0 == progress > 0;
+ if (!sameDirection && Math.abs(progress) >= PROGRESS_FLING_RECENTER) {
+ // Being flung back toward center
+ return FlingTarget.CENTER;
+ }
+ // Flung toward an edge
+ return vectorVel < 0 ? FlingTarget.ACCEPT : FlingTarget.REJECT;
+ }
+
+ @FloatRange(from = -1f, to = 1f)
+ private float pointerYToProgress(float pointerY) {
+ boolean pointerAboveZero = pointerY > zeroY;
+ float nearestThreshold = pointerAboveZero ? rejectThresholdY : acceptThresholdY;
+
+ float absoluteProgress = (pointerY - zeroY) / (nearestThreshold - zeroY);
+ return MathUtil.clamp(absoluteProgress * (pointerAboveZero ? -1 : 1), -1f, 1f);
+ }
+
+ private boolean isFalseTouch() {
+ if (falsingManager != null && falsingManager.isEnabled()) {
+ if (falsingManager.isFalseTouch()) {
+ if (touchUsesFalsing) {
+ LogUtil.i("FlingUpDownTouchHandler.isFalseTouch", "rejecting false touch");
+ return true;
+ } else {
+ LogUtil.i(
+ "FlingUpDownTouchHandler.isFalseTouch",
+ "Suspected false touch, but not using false touch rejection for this gesture");
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return !touchAboveFalsingThreshold;
+ }
+
+ private void trackMovement(MotionEvent event) {
+ if (velocityTracker != null) {
+ velocityTracker.addMovement(event);
+ }
+ }
+
+ private void fling(float velocity, @FlingTarget int target, boolean centerBecauseOfFalsing) {
+ ValueAnimator animator = createProgressAnimator(target);
+ if (target == FlingTarget.CENTER) {
+ flingAnimationUtils.apply(animator, currentProgress, target, velocity);
+ } else {
+ flingAnimationUtils.applyDismissing(animator, currentProgress, target, velocity, 1);
+ }
+ if (target == FlingTarget.CENTER && centerBecauseOfFalsing) {
+ velocity = 0;
+ }
+ if (velocity == 0) {
+ animator.setDuration(350);
+ }
+
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ progressAnimator = null;
+ if (!canceled) {
+ onMoveEnded();
+ }
+ }
+ });
+ progressAnimator = animator;
+ animator.start();
+ }
+
+ private void onMoveEnded() {
+ if (currentProgress == 0) {
+ listener.onMoveReset(!hintDistanceExceeded);
+ } else {
+ listener.onMoveFinish(currentProgress > 0);
+ }
+ }
+
+ private ValueAnimator createProgressAnimator(float targetProgress) {
+ ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, targetProgress);
+ animator.addUpdateListener(
+ new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ setCurrentProgress((Float) animation.getAnimatedValue());
+ }
+ });
+ return animator;
+ }
+
+ private void initVelocityTracker() {
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ }
+ velocityTracker = VelocityTracker.obtain();
+ }
+
+ private void startMotion(float newY, boolean startTracking, float startProgress) {
+ initialTouchY = newY;
+ hintDistanceExceeded = false;
+
+ if (startProgress <= .25) {
+ acceptThresholdY = Math.max(0, initialTouchY - acceptThresholdPx);
+ rejectThresholdY = Math.min(target.getHeight(), initialTouchY + rejectThresholdPx);
+ zeroY = initialTouchY;
+ }
+
+ if (startTracking) {
+ touchSlopExceeded = true;
+ onTrackingStarted();
+ setCurrentProgress(startProgress);
+ }
+ }
+
+ private void onTrackingStarted() {
+ tracking = true;
+ listener.onTrackingStart();
+ }
+
+ private void onTrackingStopped() {
+ tracking = false;
+ listener.onTrackingStopped();
+ }
+
+ private void cancelProgressAnimator() {
+ if (progressAnimator != null) {
+ progressAnimator.cancel();
+ }
+ }
+
+ private void setCurrentProgress(float progress) {
+ if (Math.abs(progress) > HINT_MOVE_THRESHOLD_RATIO) {
+ hintDistanceExceeded = true;
+ }
+ currentProgress = progress;
+ listener.onProgressChanged(progress);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java b/java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java
new file mode 100644
index 000000000..67b1b9689
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/TwoButtonMethod.java
@@ -0,0 +1,268 @@
+/*
+ * 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.incallui.answer.impl.answermethod;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.os.Bundle;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.incallui.answer.impl.answermethod.FlingUpDownTouchHandler.OnProgressChangedListener;
+import com.android.incallui.util.AccessibilityUtil;
+
+/** Answer method that shows two buttons for answer/reject. */
+public class TwoButtonMethod extends AnswerMethod
+ implements OnClickListener, AnimatorUpdateListener {
+
+ private static final String STATE_HINT_TEXT = "hintText";
+ private static final String STATE_INCOMING_WILL_DISCONNECT = "incomingWillDisconnect";
+
+ private View answerButton;
+ private View answerLabel;
+ private View declineButton;
+ private View declineLabel;
+ private TextView hintTextView;
+ private boolean incomingWillDisconnect;
+ private boolean buttonClicked;
+ private CharSequence hintText;
+ @Nullable private FlingUpDownTouchHandler touchHandler;
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ if (bundle != null) {
+ incomingWillDisconnect = bundle.getBoolean(STATE_INCOMING_WILL_DISCONNECT);
+ hintText = bundle.getCharSequence(STATE_HINT_TEXT);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle bundle) {
+ super.onSaveInstanceState(bundle);
+ bundle.putBoolean(STATE_INCOMING_WILL_DISCONNECT, incomingWillDisconnect);
+ bundle.putCharSequence(STATE_HINT_TEXT, hintText);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = layoutInflater.inflate(R.layout.two_button_method, viewGroup, false);
+
+ hintTextView = (TextView) view.findViewById(R.id.two_button_hint_text);
+ updateHintText();
+
+ answerButton = view.findViewById(R.id.two_button_answer_button);
+ answerLabel = view.findViewById(R.id.two_button_answer_label);
+ declineButton = view.findViewById(R.id.two_button_decline_button);
+ declineLabel = view.findViewById(R.id.two_button_decline_label);
+
+ boolean showLabels = getResources().getBoolean(R.bool.two_button_show_button_labels);
+ answerLabel.setVisibility(showLabels ? View.VISIBLE : View.GONE);
+ declineLabel.setVisibility(showLabels ? View.VISIBLE : View.GONE);
+
+ answerButton.setOnClickListener(this);
+ declineButton.setOnClickListener(this);
+
+ if (AccessibilityUtil.isTouchExplorationEnabled(getContext())) {
+ /* Falsing already handled by AccessibilityManager */
+ touchHandler =
+ FlingUpDownTouchHandler.attach(
+ view,
+ new OnProgressChangedListener() {
+ @Override
+ public void onProgressChanged(@FloatRange(from = -1f, to = 1f) float progress) {}
+
+ @Override
+ public void onTrackingStart() {}
+
+ @Override
+ public void onTrackingStopped() {}
+
+ @Override
+ public void onMoveReset(boolean showHint) {}
+
+ @Override
+ public void onMoveFinish(boolean accept) {
+ if (accept) {
+ answerCall();
+ } else {
+ rejectCall();
+ }
+ }
+
+ @Override
+ public boolean shouldUseFalsing(@NonNull MotionEvent downEvent) {
+ return false;
+ }
+ },
+ null /* Falsing already handled by AccessibilityManager */);
+ touchHandler.setFlingEnabled(false);
+ }
+ return view;
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (touchHandler != null) {
+ touchHandler.detach();
+ touchHandler = null;
+ }
+ }
+
+ @Override
+ public void setHintText(@Nullable CharSequence hintText) {
+ this.hintText = hintText;
+ updateHintText();
+ }
+
+ @Override
+ public void setShowIncomingWillDisconnect(boolean incomingWillDisconnect) {
+ this.incomingWillDisconnect = incomingWillDisconnect;
+ updateHintText();
+ }
+
+ private void updateHintText() {
+ if (hintTextView == null) {
+ return;
+ }
+ hintTextView.setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ if (!TextUtils.isEmpty(hintText) && !buttonClicked) {
+ hintTextView.setText(hintText);
+ hintTextView.animate().alpha(1f).start();
+ } else if (incomingWillDisconnect && !buttonClicked) {
+ hintTextView.setText(R.string.call_incoming_will_disconnect);
+ hintTextView.animate().alpha(1f).start();
+ } else {
+ hintTextView.animate().alpha(0f).start();
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == answerButton) {
+ answerCall();
+ LogUtil.v("TwoButtonMethod.onClick", "Call answered");
+ } else if (view == declineButton) {
+ rejectCall();
+ LogUtil.v("TwoButtonMethod.onClick", "two_buttonMethod Call rejected");
+ } else {
+ Assert.fail("Unknown click from view: " + view);
+ }
+ buttonClicked = true;
+ }
+
+ private void answerCall() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
+ animator.addUpdateListener(this);
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ private boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!canceled) {
+ getParent().answerFromMethod();
+ }
+ }
+ });
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(animator).with(createViewHideAnimation());
+ animatorSet.start();
+ }
+
+ private void rejectCall() {
+ ValueAnimator animator = ValueAnimator.ofFloat(0, -1);
+ animator.addUpdateListener(this);
+ animator.addListener(
+ new AnimatorListenerAdapter() {
+ private boolean canceled;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ canceled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!canceled) {
+ getParent().rejectFromMethod();
+ }
+ }
+ });
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(animator).with(createViewHideAnimation());
+ animatorSet.start();
+ }
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ getParent().onAnswerProgressUpdate(((float) animation.getAnimatedValue()));
+ }
+
+ private Animator createViewHideAnimation() {
+ ObjectAnimator answerButtonHide =
+ ObjectAnimator.ofPropertyValuesHolder(
+ answerButton,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 0f),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f));
+
+ ObjectAnimator declineButtonHide =
+ ObjectAnimator.ofPropertyValuesHolder(
+ declineButton,
+ PropertyValuesHolder.ofFloat(View.SCALE_X, 0f),
+ PropertyValuesHolder.ofFloat(View.SCALE_Y, 0f));
+
+ ObjectAnimator answerLabelHide = ObjectAnimator.ofFloat(answerLabel, View.ALPHA, 0f);
+
+ ObjectAnimator declineLabelHide = ObjectAnimator.ofFloat(declineLabel, View.ALPHA, 0f);
+
+ ObjectAnimator hintHide = ObjectAnimator.ofFloat(hintTextView, View.ALPHA, 0f);
+
+ AnimatorSet hideSet = new AnimatorSet();
+ hideSet
+ .play(answerButtonHide)
+ .with(declineButtonHide)
+ .with(answerLabelHide)
+ .with(declineLabelHide)
+ .with(hintHide);
+ return hideSet;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml b/java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml
new file mode 100644
index 000000000..451c862fa
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/drawable/call_answer.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="24dp"
+ android:viewportHeight="32.0"
+ android:viewportWidth="32.0"
+ android:width="24dp">
+ <group
+ android:name="rotationGroup"
+ android:pivotX="12"
+ android:pivotY="12"
+ android:translateX="4"
+ android:translateY="4"
+ android:rotation="0"
+ >
+ <path
+ android:fillColor="#FFFFFFFF"
+ android:pathData="M6.62,10.79c1.44,2.83 3.76,5.14 6.59,6.59l2.2,-2.2c0.27,-0.27 0.67,-0.36 1.02,-0.24 1.12,0.37 2.33,0.57 3.57,0.57 0.55,0 1,0.45 1,1V20c0,0.55 -0.45,1 -1,1 -9.39,0 -17,-7.61 -17,-17 0,-0.55 0.45,-1 1,-1h3.5c0.55,0 1,0.45 1,1 0,1.25 0.2,2.45 0.57,3.57 0.11,0.35 0.03,0.74 -0.25,1.02l-2.2,2.2z"/>
+ </group>
+</vector>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml b/java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml
new file mode 100644
index 000000000..938ddc2be
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/drawable/circular_background.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="#FFFFFFFF"/>
+</shape>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml b/java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml
new file mode 100644
index 000000000..78e097958
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/layout/swipe_up_down_method.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginStart="@dimen/answer_swipe_dead_zone_sides"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:layout_marginEnd="@dimen/answer_swipe_dead_zone_sides">
+ <LinearLayout
+ android:id="@+id/incoming_swipe_to_answer_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:accessibilityLiveRegion="polite"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="center_horizontal|bottom"
+ android:orientation="vertical"
+ android:visibility="visible">
+ <TextView
+ android:id="@+id/incoming_will_disconnect_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="116dp"
+ android:layout_gravity="center_horizontal"
+ android:alpha="0"
+ android:text="@string/call_incoming_will_disconnect"
+ android:textColor="@color/blue_grey_100"
+ android:textSize="16sp"
+ tools:alpha="1"/>
+ <TextView
+ android:id="@+id/incoming_swipe_to_answer_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="18dp"
+ android:layout_gravity="center_horizontal"
+ android:focusable="false"
+ android:text="@string/call_incoming_swipe_to_answer"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Hint"/>
+
+ <FrameLayout
+ android:id="@+id/incoming_call_puck_container"
+ android:layout_width="@dimen/answer_contact_puck_size_photo"
+ android:layout_height="@dimen/answer_contact_puck_size_photo"
+ android:layout_marginBottom="10dp"
+ android:layout_gravity="center_horizontal"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:contentDescription="@string/a11y_incoming_call_swipe_to_answer">
+
+ <!-- Puck background and icon are hosted in the separated views to animate separately. -->
+ <ImageView
+ android:id="@+id/incoming_call_puck_bg"
+ android:layout_width="@dimen/answer_contact_puck_size_no_photo"
+ android:layout_height="@dimen/answer_contact_puck_size_no_photo"
+ android:layout_gravity="center"
+ android:background="@drawable/circular_background"
+ android:contentDescription="@null"
+ android:duplicateParentState="true"
+ android:elevation="8dp"
+ android:focusable="false"
+ android:stateListAnimator="@animator/activated_button_elevation"/>
+
+ <ImageView
+ android:id="@+id/incoming_call_puck_icon"
+ android:layout_width="30dp"
+ android:layout_height="30dp"
+ android:layout_gravity="center"
+ android:contentDescription="@null"
+ android:duplicateParentState="true"
+ android:elevation="16dp"
+ android:focusable="false"
+ android:outlineProvider="none"
+ android:src="@drawable/quantum_ic_call_white_24"
+ android:tint="@color/incoming_answer_icon"
+ android:tintMode="src_atop"
+ tools:outlineProvider="background"/>
+
+ </FrameLayout>
+ <TextView
+ android:id="@+id/incoming_swipe_to_reject_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:layout_gravity="center_horizontal"
+ android:alpha="0"
+ android:focusable="false"
+ android:text="@string/call_incoming_swipe_to_reject"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Hint"
+ tools:alpha="1"/>
+ </LinearLayout>
+ <FrameLayout
+ android:id="@+id/hint_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml b/java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml
new file mode 100644
index 000000000..f92f3c428
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/layout/two_button_method.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="bottom|center_horizontal"
+ android:orientation="vertical">
+ <TextView
+ android:id="@+id/two_button_hint_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="32dp"
+ android:accessibilityLiveRegion="polite"
+ android:alpha="0"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/two_button_bottom_padding"
+ android:gravity="bottom|center_horizontal"
+ android:orientation="horizontal">
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="88dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:padding="@dimen/incall_call_button_elevation"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageButton
+ android:id="@+id/two_button_decline_button"
+ style="@style/Answer.Button.Decline"
+ android:layout_width="@dimen/two_button_button_size"
+ android:layout_height="@dimen/two_button_button_size"
+ android:contentDescription="@string/a11y_call_incoming_decline_description"
+ android:src="@drawable/quantum_ic_call_end_white_24"/>
+
+ <TextView
+ android:id="@+id/two_button_decline_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/two_button_label_padding"
+ android:importantForAccessibility="no"
+ android:text="@string/call_incoming_decline"
+ android:textColor="#ffffffff"
+ android:textSize="@dimen/two_button_label_size"/>
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:padding="@dimen/incall_call_button_elevation"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageButton
+ android:id="@+id/two_button_answer_button"
+ style="@style/Answer.Button.Answer"
+ android:layout_width="@dimen/two_button_button_size"
+ android:layout_height="@dimen/two_button_button_size"
+ android:contentDescription="@string/a11y_call_incoming_answer_description"
+ android:src="@drawable/quantum_ic_call_white_24"/>
+
+ <TextView
+ android:id="@+id/two_button_answer_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/two_button_label_padding"
+ android:importantForAccessibility="no"
+ android:text="@string/call_incoming_answer"
+ android:textColor="#ffffffff"
+ android:textSize="@dimen/two_button_label_size"/>
+
+ </LinearLayout>
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml b/java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml
new file mode 100644
index 000000000..7d99b29aa
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values-h240dp/values.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <bool name="two_button_show_button_labels">true</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml b/java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml
new file mode 100644
index 000000000..e7e223d8c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values-h280dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="two_button_button_size">64dp</dimen>
+ <dimen name="two_button_label_padding">16dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml b/java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml
new file mode 100644
index 000000000..b7b4bd894
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values-h480dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="two_button_bottom_padding">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml
new file mode 100644
index 000000000..bf160f9ac
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="answer_contact_puck_size_photo">88dp</dimen>
+ <dimen name="answer_contact_puck_size_no_photo">72dp</dimen>
+ <dimen name="two_button_button_size">48dp</dimen>
+ <dimen name="two_button_label_size">12sp</dimen>
+ <dimen name="two_button_label_padding">8dp</dimen>
+ <dimen name="two_button_bottom_padding">24dp</dimen>
+ <dimen name="answer_swipe_dead_zone_sides">50dp</dimen>
+ <dimen name="answer_swipe_dead_zone_top">150dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml
new file mode 100644
index 000000000..fc03cacbd
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/ids.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="accessibility_action_answer" type="id"/>
+ <item name="accessibility_action_decline" type="id"/>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml
new file mode 100644
index 000000000..8b50dbf1a
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/strings.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="call_incoming_swipe_to_answer">Swipe up to answer</string>
+ <string name="call_incoming_swipe_to_reject">Swipe down to reject</string>
+ <string name="a11y_incoming_call_swipe_to_answer">Swipe up with two fingers to answer or down to reject the call</string>
+ <string name="call_incoming_will_disconnect">Answering this call will end your video call</string>
+
+ <string name="a11y_call_incoming_decline_description">Decline</string>
+ <string name="call_incoming_decline">Decline</string>
+
+ <string name="a11y_call_incoming_answer_description">Answer</string>
+ <string name="call_incoming_answer">Answer</string>
+
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml
new file mode 100644
index 000000000..fd3ca7ca0
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/styles.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="Dialer.Incall.TextAppearance.Hint">
+ <item name="android:textSize">14sp</item>
+ <item name="android:textStyle">italic</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/answermethod/res/values/values.xml b/java/com/android/incallui/answer/impl/answermethod/res/values/values.xml
new file mode 100644
index 000000000..43b2cd273
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/answermethod/res/values/values.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <color name="incoming_or_outgoing_call_screen_mask">@android:color/transparent</color>
+ <color name="call_hangup_background">#DF0000</color>
+ <color name="call_accept_background">#00C853</color>
+ <color name="incoming_answer_icon">#00C853</color>
+ <integer name="button_exit_fade_delay_ms">300</integer>
+ <bool name="two_button_show_button_labels">false</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java b/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java
new file mode 100644
index 000000000..ac504444e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AccelerationClassifier.java
@@ -0,0 +1,99 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.Map;
+
+/**
+ * A classifier which looks at the speed and distance between successive points of a Stroke. It
+ * looks at two consecutive speeds between two points and calculates the ratio between them. The
+ * final result is the maximum of these values. It does the same for distances. If some speed or
+ * distance is equal to zero then the ratio between this and the next part is not calculated. To the
+ * duration of each part there is added one nanosecond so that it is always possible to calculate
+ * the speed of a part.
+ */
+class AccelerationClassifier extends StrokeClassifier {
+ private final Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public AccelerationClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "ACC";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+ Point point = stroke.getPoints().get(stroke.getPoints().size() - 1);
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data(point));
+ } else {
+ mStrokeMap.get(stroke).addPoint(point);
+ }
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return 2 * SpeedRatioEvaluator.evaluate(data.maxSpeedRatio);
+ }
+
+ private static class Data {
+
+ static final float MILLIS_TO_NANOS = 1e6f;
+
+ Point previousPoint;
+ float previousSpeed = 0;
+ float maxSpeedRatio = 0;
+
+ public Data(Point point) {
+ previousPoint = point;
+ }
+
+ public void addPoint(Point point) {
+ float distance = previousPoint.dist(point);
+ float duration = (float) (point.timeOffsetNano - previousPoint.timeOffsetNano + 1);
+ float speed = distance / duration;
+
+ if (duration > 20 * MILLIS_TO_NANOS || duration < 5 * MILLIS_TO_NANOS) {
+ // reject this segment and ensure we won't use data about it in the next round.
+ previousSpeed = 0;
+ previousPoint = point;
+ return;
+ }
+ if (previousSpeed != 0.0f) {
+ maxSpeedRatio = Math.max(maxSpeedRatio, speed / previousSpeed);
+ }
+
+ previousSpeed = speed;
+ previousPoint = point;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java b/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java
new file mode 100644
index 000000000..dbfbcfc1c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesClassifier.java
@@ -0,0 +1,193 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A classifier which calculates the variance of differences between successive angles in a stroke.
+ * For each stroke it keeps its last three points. If some successive points are the same, it
+ * ignores the repetitions. If a new point is added, the classifier calculates the angle between the
+ * last three points. After that, it calculates the difference between this angle and the previously
+ * calculated angle. Then it calculates the variance of the differences from a stroke. To the
+ * differences there is artificially added value 0.0 and the difference between the first angle and
+ * PI (angles are in radians). It helps with strokes which have few points and punishes more strokes
+ * which are not smooth.
+ *
+ * <p>This classifier also tries to split the stroke into two parts in the place in which the
+ * biggest angle is. It calculates the angle variance of the two parts and sums them up. The reason
+ * the classifier is doing this, is because some human swipes at the beginning go for a moment in
+ * one direction and then they rapidly change direction for the rest of the stroke (like a tick).
+ * The final result is the minimum of angle variance of the whole stroke and the sum of angle
+ * variances of the two parts split up. The classifier tries the tick option only if the first part
+ * is shorter than the second part.
+ *
+ * <p>Additionally, the classifier classifies the angles as left angles (those angles which value is
+ * in [0.0, PI - ANGLE_DEVIATION) interval), straight angles ([PI - ANGLE_DEVIATION, PI +
+ * ANGLE_DEVIATION] interval) and right angles ((PI + ANGLE_DEVIATION, 2 * PI) interval) and then
+ * calculates the percentage of angles which are in the same direction (straight angles can be left
+ * angels or right angles)
+ */
+class AnglesClassifier extends StrokeClassifier {
+ private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public AnglesClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "ANG";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data());
+ }
+ mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return AnglesVarianceEvaluator.evaluate(data.getAnglesVariance())
+ + AnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
+ }
+
+ private static class Data {
+ private static final float ANGLE_DEVIATION = (float) Math.PI / 20.0f;
+ private static final float MIN_MOVE_DIST_DP = .01f;
+
+ private List<Point> mLastThreePoints = new ArrayList<>();
+ private float mFirstAngleVariance;
+ private float mPreviousAngle;
+ private float mBiggestAngle;
+ private float mSumSquares;
+ private float mSecondSumSquares;
+ private float mSum;
+ private float mSecondSum;
+ private float mCount;
+ private float mSecondCount;
+ private float mFirstLength;
+ private float mLength;
+ private float mAnglesCount;
+ private float mLeftAngles;
+ private float mRightAngles;
+ private float mStraightAngles;
+
+ public Data() {
+ mFirstAngleVariance = 0.0f;
+ mPreviousAngle = (float) Math.PI;
+ mBiggestAngle = 0.0f;
+ mSumSquares = mSecondSumSquares = 0.0f;
+ mSum = mSecondSum = 0.0f;
+ mCount = mSecondCount = 1.0f;
+ mLength = mFirstLength = 0.0f;
+ mAnglesCount = mLeftAngles = mRightAngles = mStraightAngles = 0.0f;
+ }
+
+ public void addPoint(Point point) {
+ // Checking if the added point is different than the previously added point
+ // Repetitions and short distances are being ignored so that proper angles are calculated.
+ if (mLastThreePoints.isEmpty()
+ || (!mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point)
+ && (mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point)
+ > MIN_MOVE_DIST_DP))) {
+ if (!mLastThreePoints.isEmpty()) {
+ mLength += mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point);
+ }
+ mLastThreePoints.add(point);
+ if (mLastThreePoints.size() == 4) {
+ mLastThreePoints.remove(0);
+
+ float angle =
+ mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
+
+ mAnglesCount++;
+ if (angle < Math.PI - ANGLE_DEVIATION) {
+ mLeftAngles++;
+ } else if (angle <= Math.PI + ANGLE_DEVIATION) {
+ mStraightAngles++;
+ } else {
+ mRightAngles++;
+ }
+
+ float difference = angle - mPreviousAngle;
+
+ // If this is the biggest angle of the stroke so then we save the value of
+ // the angle variance so far and start to count the values for the angle
+ // variance of the second part.
+ if (mBiggestAngle < angle) {
+ mBiggestAngle = angle;
+ mFirstLength = mLength;
+ mFirstAngleVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ mSecondSumSquares = 0.0f;
+ mSecondSum = 0.0f;
+ mSecondCount = 1.0f;
+ } else {
+ mSecondSum += difference;
+ mSecondSumSquares += difference * difference;
+ mSecondCount += 1.0f;
+ }
+
+ mSum += difference;
+ mSumSquares += difference * difference;
+ mCount += 1.0f;
+ mPreviousAngle = angle;
+ }
+ }
+ }
+
+ public float getAnglesVariance(float sumSquares, float sum, float count) {
+ return sumSquares / count - (sum / count) * (sum / count);
+ }
+
+ public float getAnglesVariance() {
+ float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
+ if (mFirstLength < mLength / 2f) {
+ anglesVariance =
+ Math.min(
+ anglesVariance,
+ mFirstAngleVariance
+ + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
+ }
+ return anglesVariance;
+ }
+
+ public float getAnglesPercentage() {
+ if (mAnglesCount == 0.0f) {
+ return 1.0f;
+ }
+ return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java b/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java
new file mode 100644
index 000000000..49a183596
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesPercentageEvaluator.java
@@ -0,0 +1,33 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class AnglesPercentageEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 1.00) {
+ evaluation++;
+ }
+ if (value < 0.90) {
+ evaluation++;
+ }
+ if (value < 0.70) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java b/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java
new file mode 100644
index 000000000..db4de6a3b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/AnglesVarianceEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class AnglesVarianceEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value > 0.05) {
+ evaluation++;
+ }
+ if (value > 0.10) {
+ evaluation++;
+ }
+ if (value > 0.20) {
+ evaluation++;
+ }
+ if (value > 0.40) {
+ evaluation++;
+ }
+ if (value > 0.80) {
+ evaluation++;
+ }
+ if (value > 1.50) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Classifier.java b/java/com/android/incallui/answer/impl/classifier/Classifier.java
new file mode 100644
index 000000000..c6fbff327
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Classifier.java
@@ -0,0 +1,35 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+
+/** An abstract class for classifiers for touch and sensor events. */
+abstract class Classifier {
+
+ /** Contains all the information about touch events from which the classifier can query */
+ protected ClassifierData mClassifierData;
+
+ /** Informs the classifier that a new touch event has occurred */
+ public void onTouchEvent(MotionEvent event) {}
+
+ /** Informs the classifier that a sensor change occurred */
+ public void onSensorChanged(SensorEvent event) {}
+
+ public abstract String getTag();
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ClassifierData.java b/java/com/android/incallui/answer/impl/classifier/ClassifierData.java
new file mode 100644
index 000000000..ae07d27a0
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ClassifierData.java
@@ -0,0 +1,96 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Contains data which is used to classify interaction sequences on the lockscreen. It does, for
+ * example, provide information on the current touch state.
+ */
+class ClassifierData {
+ private SparseArray<Stroke> mCurrentStrokes = new SparseArray<>();
+ private ArrayList<Stroke> mEndingStrokes = new ArrayList<>();
+ private final float mDpi;
+ private final float mScreenHeight;
+
+ public ClassifierData(float dpi, float screenHeight) {
+ mDpi = dpi;
+ mScreenHeight = screenHeight / dpi;
+ }
+
+ public void update(MotionEvent event) {
+ mEndingStrokes.clear();
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCurrentStrokes.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int id = event.getPointerId(i);
+ if (mCurrentStrokes.get(id) == null) {
+ // TODO (keyboardr): See if there's a way to use event.getEventTimeNanos() instead
+ mCurrentStrokes.put(
+ id, new Stroke(TimeUnit.MILLISECONDS.toNanos(event.getEventTime()), mDpi));
+ }
+ mCurrentStrokes
+ .get(id)
+ .addPoint(
+ event.getX(i), event.getY(i), TimeUnit.MILLISECONDS.toNanos(event.getEventTime()));
+
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mEndingStrokes.add(getStroke(id));
+ }
+ }
+ }
+
+ void cleanUp(MotionEvent event) {
+ mEndingStrokes.clear();
+ int action = event.getActionMasked();
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ int id = event.getPointerId(i);
+ if (action == MotionEvent.ACTION_UP
+ || action == MotionEvent.ACTION_CANCEL
+ || (action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mCurrentStrokes.remove(id);
+ }
+ }
+ }
+
+ /** @return the list of Strokes which are ending in the recently added MotionEvent */
+ public ArrayList<Stroke> getEndingStrokes() {
+ return mEndingStrokes;
+ }
+
+ /**
+ * @param id the id from MotionEvent
+ * @return the Stroke assigned to the id
+ */
+ public Stroke getStroke(int id) {
+ return mCurrentStrokes.get(id);
+ }
+
+ /** @return the height of the screen in inches */
+ public float getScreenHeight() {
+ return mScreenHeight;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java b/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java
new file mode 100644
index 000000000..068626859
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DirectionClassifier.java
@@ -0,0 +1,37 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the general direction of a stroke and evaluates it depending on the
+ * type of action that takes place.
+ */
+public class DirectionClassifier extends StrokeClassifier {
+ public DirectionClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "DIR";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Point firstPoint = stroke.getPoints().get(0);
+ Point lastPoint = stroke.getPoints().get(stroke.getPoints().size() - 1);
+ return DirectionEvaluator.evaluate(lastPoint.x - firstPoint.x, lastPoint.y - firstPoint.y);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java b/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java
new file mode 100644
index 000000000..cdc1cfe1e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DirectionEvaluator.java
@@ -0,0 +1,23 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class DirectionEvaluator {
+ public static float evaluate(float xDiff, float yDiff) {
+ return Math.abs(yDiff) < Math.abs(xDiff) ? 5.5f : 0.0f;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java
new file mode 100644
index 000000000..0b9f1138d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DurationCountClassifier.java
@@ -0,0 +1,35 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the duration of the stroke and its number of
+ * points.
+ */
+class DurationCountClassifier extends StrokeClassifier {
+ public DurationCountClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "DUR";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return DurationCountEvaluator.evaluate(stroke.getDurationSeconds() / stroke.getCount());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java
new file mode 100644
index 000000000..5b232fe95
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/DurationCountEvaluator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class DurationCountEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.0105) {
+ evaluation++;
+ }
+ if (value < 0.00909) {
+ evaluation++;
+ }
+ if (value < 0.00667) {
+ evaluation++;
+ }
+ if (value > 0.0333) {
+ evaluation++;
+ }
+ if (value > 0.0500) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java b/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.java
new file mode 100644
index 000000000..95b317638
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointLengthClassifier.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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the distance between the first and the last point from the stroke.
+ */
+class EndPointLengthClassifier extends StrokeClassifier {
+ public EndPointLengthClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "END_LNGTH";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return EndPointLengthEvaluator.evaluate(stroke.getEndPointLength());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java b/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java
new file mode 100644
index 000000000..74bfffba4
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointLengthEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class EndPointLengthEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.05) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.1) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.2) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.3) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.4) {
+ evaluation += 2.0f;
+ }
+ if (value < 0.5) {
+ evaluation += 2.0f;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java b/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java
new file mode 100644
index 000000000..01a35c126
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointRatioClassifier.java
@@ -0,0 +1,43 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the total length covered by the stroke and the
+ * distance between the first and last point from this stroke.
+ */
+class EndPointRatioClassifier extends StrokeClassifier {
+ public EndPointRatioClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "END_RTIO";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ float ratio;
+ if (stroke.getTotalLength() == 0.0f) {
+ ratio = 1.0f;
+ } else {
+ ratio = stroke.getEndPointLength() / stroke.getTotalLength();
+ }
+ return EndPointRatioEvaluator.evaluate(ratio);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java b/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java
new file mode 100644
index 000000000..1d64bea8e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/EndPointRatioEvaluator.java
@@ -0,0 +1,42 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class EndPointRatioEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.85) {
+ evaluation++;
+ }
+ if (value < 0.75) {
+ evaluation++;
+ }
+ if (value < 0.65) {
+ evaluation++;
+ }
+ if (value < 0.55) {
+ evaluation++;
+ }
+ if (value < 0.45) {
+ evaluation++;
+ }
+ if (value < 0.35) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/FalsingManager.java b/java/com/android/incallui/answer/impl/classifier/FalsingManager.java
new file mode 100644
index 000000000..fdcc0a3f9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/FalsingManager.java
@@ -0,0 +1,140 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.PowerManager;
+import android.view.MotionEvent;
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * When the phone is locked, listens to touch, sensor and phone events and sends them to
+ * HumanInteractionClassifier to determine if touches are coming from a human.
+ */
+public class FalsingManager implements SensorEventListener {
+ private static final int[] CLASSIFIER_SENSORS =
+ new int[] {
+ Sensor.TYPE_PROXIMITY,
+ };
+
+ private final SensorManager mSensorManager;
+ private final HumanInteractionClassifier mHumanInteractionClassifier;
+ private final AccessibilityManager mAccessibilityManager;
+
+ private boolean mSessionActive = false;
+ private boolean mScreenOn;
+
+ public FalsingManager(Context context) {
+ mSensorManager = context.getSystemService(SensorManager.class);
+ mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
+ mHumanInteractionClassifier = new HumanInteractionClassifier(context);
+ mScreenOn = context.getSystemService(PowerManager.class).isInteractive();
+ }
+
+ /** Returns {@code true} iff the FalsingManager is enabled and able to classify touches */
+ public boolean isEnabled() {
+ return mHumanInteractionClassifier.isEnabled();
+ }
+
+ /**
+ * Returns {@code true} iff the classifier determined that this is not a human interacting with
+ * the phone.
+ */
+ public boolean isFalseTouch() {
+ // Touch exploration triggers false positives in the classifier and
+ // already sufficiently prevents false unlocks.
+ return !mAccessibilityManager.isTouchExplorationEnabled()
+ && mHumanInteractionClassifier.isFalseTouch();
+ }
+
+ /**
+ * Should be called when the screen turns on and the related Views become visible. This will start
+ * tracking changes if the manager is enabled.
+ */
+ public void onScreenOn() {
+ mScreenOn = true;
+ sessionEntrypoint();
+ }
+
+ /**
+ * Should be called when the screen turns off or the related Views are no longer visible. This
+ * will cause the manager to stop tracking changes.
+ */
+ public void onScreenOff() {
+ mScreenOn = false;
+ sessionExitpoint();
+ }
+
+ /**
+ * Should be called when a new touch event has been received and should be classified.
+ *
+ * @param event MotionEvent to be classified as human or false.
+ */
+ public void onTouchEvent(MotionEvent event) {
+ if (mSessionActive) {
+ mHumanInteractionClassifier.onTouchEvent(event);
+ }
+ }
+
+ @Override
+ public synchronized void onSensorChanged(SensorEvent event) {
+ mHumanInteractionClassifier.onSensorChanged(event);
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {}
+
+ private boolean shouldSessionBeActive() {
+ return isEnabled() && mScreenOn;
+ }
+
+ private boolean sessionEntrypoint() {
+ if (!mSessionActive && shouldSessionBeActive()) {
+ onSessionStart();
+ return true;
+ }
+ return false;
+ }
+
+ private void sessionExitpoint() {
+ if (mSessionActive && !shouldSessionBeActive()) {
+ mSessionActive = false;
+ mSensorManager.unregisterListener(this);
+ }
+ }
+
+ private void onSessionStart() {
+ mSessionActive = true;
+
+ if (mHumanInteractionClassifier.isEnabled()) {
+ registerSensors(CLASSIFIER_SENSORS);
+ }
+ }
+
+ private void registerSensors(int[] sensors) {
+ for (int sensorType : sensors) {
+ Sensor s = mSensorManager.getDefaultSensor(sensorType);
+ if (s != null) {
+ mSensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_GAME);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java b/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java
new file mode 100644
index 000000000..afd7ea0e7
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/GestureClassifier.java
@@ -0,0 +1,31 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * An abstract class for classifiers which classify the whole gesture (all the strokes which
+ * occurred from DOWN event to UP/CANCEL event)
+ */
+abstract class GestureClassifier extends Classifier {
+
+ /**
+ * @return a non-negative value which is used to determine whether the most recent gesture is a
+ * false interaction; the bigger the value the greater the chance that this a false
+ * interaction.
+ */
+ public abstract float getFalseTouchEvaluation();
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java b/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java
new file mode 100644
index 000000000..3f302c65f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/HistoryEvaluator.java
@@ -0,0 +1,115 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+
+/**
+ * Holds the evaluations for ended strokes and gestures. These values are decreased through time.
+ */
+class HistoryEvaluator {
+ private static final float INTERVAL = 50.0f;
+ private static final float HISTORY_FACTOR = 0.9f;
+ private static final float EPSILON = 1e-5f;
+
+ private final ArrayList<Data> mStrokes = new ArrayList<>();
+ private final ArrayList<Data> mGestureWeights = new ArrayList<>();
+ private long mLastUpdate;
+
+ public HistoryEvaluator() {
+ mLastUpdate = SystemClock.elapsedRealtime();
+ }
+
+ public void addStroke(float evaluation) {
+ decayValue();
+ mStrokes.add(new Data(evaluation));
+ }
+
+ public void addGesture(float evaluation) {
+ decayValue();
+ mGestureWeights.add(new Data(evaluation));
+ }
+
+ /** Calculates the weighted average of strokes and adds to it the weighted average of gestures */
+ public float getEvaluation() {
+ return weightedAverage(mStrokes) + weightedAverage(mGestureWeights);
+ }
+
+ private float weightedAverage(ArrayList<Data> list) {
+ float sumValue = 0.0f;
+ float sumWeight = 0.0f;
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ Data data = list.get(i);
+ sumValue += data.evaluation * data.weight;
+ sumWeight += data.weight;
+ }
+
+ if (sumWeight == 0.0f) {
+ return 0.0f;
+ }
+
+ return sumValue / sumWeight;
+ }
+
+ private void decayValue() {
+ long time = SystemClock.elapsedRealtime();
+
+ if (time <= mLastUpdate) {
+ return;
+ }
+
+ // All weights are multiplied by HISTORY_FACTOR after each INTERVAL milliseconds.
+ float factor = (float) Math.pow(HISTORY_FACTOR, (time - mLastUpdate) / INTERVAL);
+
+ decayValue(mStrokes, factor);
+ decayValue(mGestureWeights, factor);
+ mLastUpdate = time;
+ }
+
+ private void decayValue(ArrayList<Data> list, float factor) {
+ int size = list.size();
+ for (int i = 0; i < size; i++) {
+ list.get(i).weight *= factor;
+ }
+
+ // Removing evaluations with such small weights that they do not matter anymore
+ while (!list.isEmpty() && isZero(list.get(0).weight)) {
+ list.remove(0);
+ }
+ }
+
+ private boolean isZero(float x) {
+ return x <= EPSILON && x >= -EPSILON;
+ }
+
+ /**
+ * For each stroke it holds its initial value and the current weight. Initially the weight is set
+ * to 1.0
+ */
+ private static class Data {
+ public float evaluation;
+ public float weight;
+
+ public Data(float evaluation) {
+ this.evaluation = evaluation;
+ weight = 1.0f;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java b/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java
new file mode 100644
index 000000000..1d3d7ef22
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/HumanInteractionClassifier.java
@@ -0,0 +1,142 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.content.Context;
+import android.hardware.SensorEvent;
+import android.util.DisplayMetrics;
+import android.view.MotionEvent;
+import com.android.dialer.common.ConfigProviderBindings;
+
+/** An classifier trying to determine whether it is a human interacting with the phone or not. */
+class HumanInteractionClassifier extends Classifier {
+
+ private static final String CONFIG_ANSWER_FALSE_TOUCH_DETECTION_ENABLED =
+ "answer_false_touch_detection_enabled";
+
+ private final StrokeClassifier[] mStrokeClassifiers;
+ private final GestureClassifier[] mGestureClassifiers;
+ private final HistoryEvaluator mHistoryEvaluator;
+ private final boolean mEnabled;
+
+ HumanInteractionClassifier(Context context) {
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+
+ // If the phone is rotated to landscape, the calculations would be wrong if xdpi and ydpi
+ // were to be used separately. Due negligible differences in xdpi and ydpi we can just
+ // take the average.
+ // Note that xdpi and ydpi are the physical pixels per inch and are not affected by scaling.
+ float dpi = (displayMetrics.xdpi + displayMetrics.ydpi) / 2.0f;
+ mClassifierData = new ClassifierData(dpi, displayMetrics.heightPixels);
+ mHistoryEvaluator = new HistoryEvaluator();
+ mEnabled =
+ ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_FALSE_TOUCH_DETECTION_ENABLED, true);
+
+ mStrokeClassifiers =
+ new StrokeClassifier[] {
+ new AnglesClassifier(mClassifierData),
+ new SpeedClassifier(mClassifierData),
+ new DurationCountClassifier(mClassifierData),
+ new EndPointRatioClassifier(mClassifierData),
+ new EndPointLengthClassifier(mClassifierData),
+ new AccelerationClassifier(mClassifierData),
+ new SpeedAnglesClassifier(mClassifierData),
+ new LengthCountClassifier(mClassifierData),
+ new DirectionClassifier(mClassifierData)
+ };
+
+ mGestureClassifiers =
+ new GestureClassifier[] {
+ new PointerCountClassifier(mClassifierData), new ProximityClassifier(mClassifierData)
+ };
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+
+ // If the user is dragging down the notification, they might want to drag it down
+ // enough to see the content, read it for a while and then lift the finger to open
+ // the notification. This kind of motion scores very bad in the Classifier so the
+ // MotionEvents which are close to the current position of the finger are not
+ // sent to the classifiers until the finger moves far enough. When the finger if lifted
+ // up, the last MotionEvent which was far enough from the finger is set as the final
+ // MotionEvent and sent to the Classifiers.
+ addTouchEvent(event);
+ }
+
+ private void addTouchEvent(MotionEvent event) {
+ mClassifierData.update(event);
+
+ for (StrokeClassifier c : mStrokeClassifiers) {
+ c.onTouchEvent(event);
+ }
+
+ for (GestureClassifier c : mGestureClassifiers) {
+ c.onTouchEvent(event);
+ }
+
+ int size = mClassifierData.getEndingStrokes().size();
+ for (int i = 0; i < size; i++) {
+ Stroke stroke = mClassifierData.getEndingStrokes().get(i);
+ float evaluation = 0.0f;
+ for (StrokeClassifier c : mStrokeClassifiers) {
+ float e = c.getFalseTouchEvaluation(stroke);
+ evaluation += e;
+ }
+
+ mHistoryEvaluator.addStroke(evaluation);
+ }
+
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ float evaluation = 0.0f;
+ for (GestureClassifier c : mGestureClassifiers) {
+ float e = c.getFalseTouchEvaluation();
+ evaluation += e;
+ }
+ mHistoryEvaluator.addGesture(evaluation);
+ }
+
+ mClassifierData.cleanUp(event);
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ for (Classifier c : mStrokeClassifiers) {
+ c.onSensorChanged(event);
+ }
+
+ for (Classifier c : mGestureClassifiers) {
+ c.onSensorChanged(event);
+ }
+ }
+
+ boolean isFalseTouch() {
+ float evaluation = mHistoryEvaluator.getEvaluation();
+ return evaluation >= 5.0f;
+ }
+
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ @Override
+ public String getTag() {
+ return "HIC";
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java
new file mode 100644
index 000000000..7dd2ab674
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/LengthCountClassifier.java
@@ -0,0 +1,39 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the length of the stroke and its number of points.
+ * The number of points is subtracted by 2 because the UP event comes in with some delay and it
+ * should not influence the ratio and also strokes which are long and have a small number of points
+ * are punished more (these kind of strokes are usually bad ones and they tend to score well in
+ * other classifiers).
+ */
+class LengthCountClassifier extends StrokeClassifier {
+ public LengthCountClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "LEN_CNT";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ return LengthCountEvaluator.evaluate(
+ stroke.getTotalLength() / Math.max(1.0f, stroke.getCount() - 2));
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java
new file mode 100644
index 000000000..2a2225a00
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/LengthCountEvaluator.java
@@ -0,0 +1,45 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier which looks at the ratio between the length of the stroke and its number of points.
+ */
+class LengthCountEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 0.09) {
+ evaluation++;
+ }
+ if (value < 0.05) {
+ evaluation++;
+ }
+ if (value < 0.02) {
+ evaluation++;
+ }
+ if (value > 0.6) {
+ evaluation++;
+ }
+ if (value > 0.9) {
+ evaluation++;
+ }
+ if (value > 1.2) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Point.java b/java/com/android/incallui/answer/impl/classifier/Point.java
new file mode 100644
index 000000000..5ea48b4ce
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Point.java
@@ -0,0 +1,95 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class Point {
+ public float x;
+ public float y;
+ public long timeOffsetNano;
+
+ public Point(float x, float y) {
+ this.x = x;
+ this.y = y;
+ this.timeOffsetNano = 0;
+ }
+
+ public Point(float x, float y, long timeOffsetNano) {
+ this.x = x;
+ this.y = y;
+ this.timeOffsetNano = timeOffsetNano;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Point)) {
+ return false;
+ }
+ Point otherPoint = ((Point) other);
+ return x == otherPoint.x && y == otherPoint.y;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = (x != +0.0f ? Float.floatToIntBits(x) : 0);
+ result = 31 * result + (y != +0.0f ? Float.floatToIntBits(y) : 0);
+ return result;
+ }
+
+ public float dist(Point a) {
+ return (float) Math.hypot(a.x - x, a.y - y);
+ }
+
+ /**
+ * Calculates the cross product of vec(this, a) and vec(this, b) where vec(x,y) is the vector from
+ * point x to point y
+ */
+ public float crossProduct(Point a, Point b) {
+ return (a.x - x) * (b.y - y) - (a.y - y) * (b.x - x);
+ }
+
+ /**
+ * Calculates the dot product of vec(this, a) and vec(this, b) where vec(x,y) is the vector from
+ * point x to point y
+ */
+ public float dotProduct(Point a, Point b) {
+ return (a.x - x) * (b.x - x) + (a.y - y) * (b.y - y);
+ }
+
+ /**
+ * Calculates the angle in radians created by points (a, this, b). If any two of these points are
+ * the same, the method will return 0.0f
+ *
+ * @return the angle in radians
+ */
+ public float getAngle(Point a, Point b) {
+ float dist1 = dist(a);
+ float dist2 = dist(b);
+
+ if (dist1 == 0.0f || dist2 == 0.0f) {
+ return 0.0f;
+ }
+
+ float crossProduct = crossProduct(a, b);
+ float dotProduct = dotProduct(a, b);
+ float cos = Math.min(1.0f, Math.max(-1.0f, dotProduct / dist1 / dist2));
+ float angle = (float) Math.acos(cos);
+ if (crossProduct < 0.0) {
+ angle = 2.0f * (float) Math.PI - angle;
+ }
+ return angle;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java b/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.java
new file mode 100644
index 000000000..070de6c9b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/PointerCountClassifier.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.incallui.answer.impl.classifier;
+
+import android.view.MotionEvent;
+
+/** A classifier which looks at the total number of traces in the whole gesture. */
+class PointerCountClassifier extends GestureClassifier {
+ private int mCount;
+
+ public PointerCountClassifier(ClassifierData classifierData) {
+ mCount = 0;
+ }
+
+ @Override
+ public String getTag() {
+ return "PTR_CNT";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mCount = 1;
+ }
+
+ if (action == MotionEvent.ACTION_POINTER_DOWN) {
+ ++mCount;
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation() {
+ return PointerCountEvaluator.evaluate(mCount);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java b/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java
new file mode 100644
index 000000000..aa972da8c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/PointerCountEvaluator.java
@@ -0,0 +1,23 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class PointerCountEvaluator {
+ public static float evaluate(int value) {
+ return (value - 1) * (value - 1);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java b/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.java
new file mode 100644
index 000000000..28701ea6d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ProximityClassifier.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.incallui.answer.impl.classifier;
+
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.view.MotionEvent;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A classifier which looks at the proximity sensor during the gesture. It calculates the percentage
+ * the proximity sensor showing the near state during the whole gesture
+ */
+class ProximityClassifier extends GestureClassifier {
+ private long mGestureStartTimeNano;
+ private long mNearStartTimeNano;
+ private long mNearDuration;
+ private boolean mNear;
+ private float mAverageNear;
+
+ public ProximityClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "PROX";
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent event) {
+ if (event.sensor.getType() == Sensor.TYPE_PROXIMITY) {
+ update(event.values[0] < event.sensor.getMaximumRange(), event.timestamp);
+ }
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mGestureStartTimeNano = TimeUnit.MILLISECONDS.toNanos(event.getEventTime());
+ mNearStartTimeNano = TimeUnit.MILLISECONDS.toNanos(event.getEventTime());
+ mNearDuration = 0;
+ }
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ update(mNear, TimeUnit.MILLISECONDS.toNanos(event.getEventTime()));
+ long duration = TimeUnit.MILLISECONDS.toNanos(event.getEventTime()) - mGestureStartTimeNano;
+
+ if (duration == 0) {
+ mAverageNear = mNear ? 1.0f : 0.0f;
+ } else {
+ mAverageNear = (float) mNearDuration / (float) duration;
+ }
+ }
+ }
+
+ /**
+ * @param near is the sensor showing the near state right now
+ * @param timestampNano time of this event in nanoseconds
+ */
+ private void update(boolean near, long timestampNano) {
+ // This if is necessary because MotionEvents and SensorEvents do not come in
+ // chronological order
+ if (timestampNano > mNearStartTimeNano) {
+ // if the state before was near then add the difference of the current time and
+ // mNearStartTimeNano to mNearDuration.
+ if (mNear) {
+ mNearDuration += timestampNano - mNearStartTimeNano;
+ }
+
+ // if the new state is near, set mNearStartTimeNano equal to this moment.
+ if (near) {
+ mNearStartTimeNano = timestampNano;
+ }
+ }
+ mNear = near;
+ }
+
+ @Override
+ public float getFalseTouchEvaluation() {
+ return ProximityEvaluator.evaluate(mAverageNear);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java b/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java
new file mode 100644
index 000000000..14636c644
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/ProximityEvaluator.java
@@ -0,0 +1,28 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class ProximityEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ float threshold = 0.1f;
+ if (value >= threshold) {
+ evaluation += 2.0f;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java
new file mode 100644
index 000000000..36ae3ad7c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesClassifier.java
@@ -0,0 +1,147 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import android.util.ArrayMap;
+import android.view.MotionEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A classifier which for each point from a stroke, it creates a point on plane with coordinates
+ * (timeOffsetNano, distanceCoveredUpToThisPoint) (scaled by DURATION_SCALE and LENGTH_SCALE) and
+ * then it calculates the angle variance of these points like the class {@link AnglesClassifier}
+ * (without splitting it into two parts). The classifier ignores the last point of a stroke because
+ * the UP event comes in with some delay and this ruins the smoothness of this curve. Additionally,
+ * the classifier classifies calculates the percentage of angles which value is in [PI -
+ * ANGLE_DEVIATION, 2* PI) interval. The reason why the classifier does that is because the speed of
+ * a good stroke is most often increases, so most of these angels should be in this interval.
+ */
+class SpeedAnglesClassifier extends StrokeClassifier {
+ private Map<Stroke, Data> mStrokeMap = new ArrayMap<>();
+
+ public SpeedAnglesClassifier(ClassifierData classifierData) {
+ mClassifierData = classifierData;
+ }
+
+ @Override
+ public String getTag() {
+ return "SPD_ANG";
+ }
+
+ @Override
+ public void onTouchEvent(MotionEvent event) {
+ int action = event.getActionMasked();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ mStrokeMap.clear();
+ }
+
+ for (int i = 0; i < event.getPointerCount(); i++) {
+ Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
+
+ if (mStrokeMap.get(stroke) == null) {
+ mStrokeMap.put(stroke, new Data());
+ }
+
+ if (action != MotionEvent.ACTION_UP
+ && action != MotionEvent.ACTION_CANCEL
+ && !(action == MotionEvent.ACTION_POINTER_UP && i == event.getActionIndex())) {
+ mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
+ }
+ }
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ Data data = mStrokeMap.get(stroke);
+ return SpeedVarianceEvaluator.evaluate(data.getAnglesVariance())
+ + SpeedAnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
+ }
+
+ private static class Data {
+ private static final float DURATION_SCALE = 1e8f;
+ private static final float LENGTH_SCALE = 1.0f;
+ private static final float ANGLE_DEVIATION = (float) Math.PI / 10.0f;
+
+ private List<Point> mLastThreePoints = new ArrayList<>();
+ private Point mPreviousPoint;
+ private float mPreviousAngle;
+ private float mSumSquares;
+ private float mSum;
+ private float mCount;
+ private float mDist;
+ private float mAnglesCount;
+ private float mAcceleratingAngles;
+
+ public Data() {
+ mPreviousPoint = null;
+ mPreviousAngle = (float) Math.PI;
+ mSumSquares = 0.0f;
+ mSum = 0.0f;
+ mCount = 1.0f;
+ mDist = 0.0f;
+ mAnglesCount = mAcceleratingAngles = 0.0f;
+ }
+
+ public void addPoint(Point point) {
+ if (mPreviousPoint != null) {
+ mDist += mPreviousPoint.dist(point);
+ }
+
+ mPreviousPoint = point;
+ Point speedPoint =
+ new Point((float) point.timeOffsetNano / DURATION_SCALE, mDist / LENGTH_SCALE);
+
+ // Checking if the added point is different than the previously added point
+ // Repetitions are being ignored so that proper angles are calculated.
+ if (mLastThreePoints.isEmpty()
+ || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(speedPoint)) {
+ mLastThreePoints.add(speedPoint);
+ if (mLastThreePoints.size() == 4) {
+ mLastThreePoints.remove(0);
+
+ float angle =
+ mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0), mLastThreePoints.get(2));
+
+ mAnglesCount++;
+ if (angle >= (float) Math.PI - ANGLE_DEVIATION) {
+ mAcceleratingAngles++;
+ }
+
+ float difference = angle - mPreviousAngle;
+ mSum += difference;
+ mSumSquares += difference * difference;
+ mCount += 1.0f;
+ mPreviousAngle = angle;
+ }
+ }
+ }
+
+ public float getAnglesVariance() {
+ return mSumSquares / mCount - (mSum / mCount) * (mSum / mCount);
+ }
+
+ public float getAnglesPercentage() {
+ if (mAnglesCount == 0.0f) {
+ return 1.0f;
+ }
+ return (mAcceleratingAngles) / mAnglesCount;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java
new file mode 100644
index 000000000..5a8bc3556
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedAnglesPercentageEvaluator.java
@@ -0,0 +1,33 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class SpeedAnglesPercentageEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 1.00) {
+ evaluation++;
+ }
+ if (value < 0.90) {
+ evaluation++;
+ }
+ if (value < 0.70) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java b/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java
new file mode 100644
index 000000000..f3ade3f49
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedClassifier.java
@@ -0,0 +1,40 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/**
+ * A classifier that looks at the speed of the stroke. It calculates the speed of a stroke in inches
+ * per second.
+ */
+class SpeedClassifier extends StrokeClassifier {
+
+ public SpeedClassifier(ClassifierData classifierData) {}
+
+ @Override
+ public String getTag() {
+ return "SPD";
+ }
+
+ @Override
+ public float getFalseTouchEvaluation(Stroke stroke) {
+ float duration = stroke.getDurationSeconds();
+ if (duration == 0.0f) {
+ return SpeedEvaluator.evaluate(0.0f);
+ }
+ return SpeedEvaluator.evaluate(stroke.getTotalLength() / duration);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.java
new file mode 100644
index 000000000..4f9aace0e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedEvaluator.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.incallui.answer.impl.classifier;
+
+class SpeedEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value < 4.0) {
+ evaluation++;
+ }
+ if (value < 2.2) {
+ evaluation++;
+ }
+ if (value > 35.0) {
+ evaluation++;
+ }
+ if (value > 50.0) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java
new file mode 100644
index 000000000..7ae111313
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedRatioEvaluator.java
@@ -0,0 +1,39 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+class SpeedRatioEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value == 0) {
+ return 0;
+ }
+ if (value <= 1.0) {
+ evaluation++;
+ }
+ if (value <= 0.5) {
+ evaluation++;
+ }
+ if (value > 9.0) {
+ evaluation++;
+ }
+ if (value > 18.0) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java b/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.java
new file mode 100644
index 000000000..211650cbb
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/SpeedVarianceEvaluator.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.incallui.answer.impl.classifier;
+
+class SpeedVarianceEvaluator {
+ public static float evaluate(float value) {
+ float evaluation = 0.0f;
+ if (value > 0.06) {
+ evaluation++;
+ }
+ if (value > 0.15) {
+ evaluation++;
+ }
+ if (value > 0.3) {
+ evaluation++;
+ }
+ if (value > 0.6) {
+ evaluation++;
+ }
+ return evaluation;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/Stroke.java b/java/com/android/incallui/answer/impl/classifier/Stroke.java
new file mode 100644
index 000000000..c542d0f7c
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/Stroke.java
@@ -0,0 +1,72 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+import java.util.ArrayList;
+
+/**
+ * Contains data about a stroke (a single trace, all the events from a given id from the
+ * DOWN/POINTER_DOWN event till the UP/POINTER_UP/CANCEL event.)
+ */
+class Stroke {
+
+ private static final float NANOS_TO_SECONDS = 1e9f;
+
+ private ArrayList<Point> mPoints = new ArrayList<>();
+ private long mStartTimeNano;
+ private long mEndTimeNano;
+ private float mLength;
+ private final float mDpi;
+
+ public Stroke(long eventTimeNano, float dpi) {
+ mDpi = dpi;
+ mStartTimeNano = mEndTimeNano = eventTimeNano;
+ }
+
+ public void addPoint(float x, float y, long eventTimeNano) {
+ mEndTimeNano = eventTimeNano;
+ Point point = new Point(x / mDpi, y / mDpi, eventTimeNano - mStartTimeNano);
+ if (!mPoints.isEmpty()) {
+ mLength += mPoints.get(mPoints.size() - 1).dist(point);
+ }
+ mPoints.add(point);
+ }
+
+ public int getCount() {
+ return mPoints.size();
+ }
+
+ public float getTotalLength() {
+ return mLength;
+ }
+
+ public float getEndPointLength() {
+ return mPoints.get(0).dist(mPoints.get(mPoints.size() - 1));
+ }
+
+ public long getDurationNanos() {
+ return mEndTimeNano - mStartTimeNano;
+ }
+
+ public float getDurationSeconds() {
+ return (float) getDurationNanos() / NANOS_TO_SECONDS;
+ }
+
+ public ArrayList<Point> getPoints() {
+ return mPoints;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java b/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java
new file mode 100644
index 000000000..8abd7e2ec
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/classifier/StrokeClassifier.java
@@ -0,0 +1,28 @@
+/*
+ * 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.incallui.answer.impl.classifier;
+
+/** An abstract class for classifiers which classify each stroke separately. */
+abstract class StrokeClassifier extends Classifier {
+
+ /**
+ * @param stroke the stroke for which the evaluation will be calculated
+ * @return a non-negative value which is used to determine whether this a false touch; the bigger
+ * the value the greater the chance that this a false touch
+ */
+ public abstract float getFalseTouchEvaluation(Stroke stroke);
+}
diff --git a/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
new file mode 100644
index 000000000..b5fa6da8f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/AndroidManifest.xml
@@ -0,0 +1,13 @@
+<manifest
+ package="com.android.incallui.answer.impl.hint"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <receiver android:name=".EventSecretCodeListener">
+ <intent-filter>
+ <action android:name="android.provider.Telephony.SECRET_CODE" />
+ <data android:scheme="android_secret_code" />
+ </intent-filter>
+ </receiver>
+ </application>
+</manifest>
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHint.java b/java/com/android/incallui/answer/impl/hint/AnswerHint.java
new file mode 100644
index 000000000..dd3b8228a
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHint.java
@@ -0,0 +1,46 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/** Interface to overlay a hint of how to answer the call. */
+public interface AnswerHint {
+
+ /**
+ * Inflates the hint's layout into the container.
+ *
+ * <p>TODO: if the hint becomes more dependent on other UI elements of the AnswerFragment,
+ * should put put and hintText into another data structure.
+ */
+ void onCreateView(LayoutInflater inflater, ViewGroup container, View puck, TextView hintText);
+
+ /** Called when the puck bounce animation begins. */
+ void onBounceStart();
+
+ /**
+ * Called when the bounce animation has ended (transitioned into other animations). The hint
+ * should reset itself.
+ */
+ void onBounceEnd();
+
+ /** Called when the call is accepted or rejected through user interaction. */
+ void onAnswered();
+}
diff --git a/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
new file mode 100644
index 000000000..45395a71f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/AnswerHintFactory.java
@@ -0,0 +1,133 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProvider;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.util.AccessibilityUtil;
+import java.util.Calendar;
+
+/**
+ * Selects a AnswerHint to show. If there's no suitable hints {@link EmptyAnswerHint} will be used,
+ * which does nothing.
+ */
+public class AnswerHintFactory {
+
+ private static final String CONFIG_ANSWER_HINT_ANSWERED_THRESHOLD_KEY =
+ "answer_hint_answered_threshold";
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_ANSWER_HINT_WHITELISTED_DEVICES_KEY =
+ "answer_hint_whitelisted_devices";
+ // Most popular devices released before NDR1 is whitelisted. Their user are likely to have seen
+ // the legacy UI.
+ private static final String DEFAULT_WHITELISTED_DEVICES_CSV =
+ "/hammerhead//bullhead//angler//shamu//gm4g//gm4g_s//AQ4501//gce_x86_phone//gm4gtkc_s/"
+ + "/Sparkle_V//Mi-498//AQ4502//imobileiq2//A65//H940//m8_google//m0xx//A10//ctih220/"
+ + "/Mi438S//bacon/";
+
+ @VisibleForTesting
+ static final String ANSWERED_COUNT_PREFERENCE_KEY = "answer_hint_answered_count";
+
+ private final EventPayloadLoader eventPayloadLoader;
+
+ public AnswerHintFactory(@NonNull EventPayloadLoader eventPayloadLoader) {
+ this.eventPayloadLoader = Assert.isNotNull(eventPayloadLoader);
+ }
+
+ @NonNull
+ public AnswerHint create(Context context, long puckUpDuration, long puckUpDelay) {
+
+ if (shouldShowAnswerHint(
+ context,
+ ConfigProviderBindings.get(context),
+ getDeviceProtectedPreferences(context),
+ Build.PRODUCT)) {
+ return new DotAnswerHint(context, puckUpDuration, puckUpDelay);
+ }
+
+ // Display the event answer hint if the payload is available.
+ Drawable eventPayload =
+ eventPayloadLoader.loadPayload(
+ context, System.currentTimeMillis(), Calendar.getInstance().getTimeZone());
+ if (eventPayload != null) {
+ return new EventAnswerHint(context, eventPayload, puckUpDuration, puckUpDelay);
+ }
+
+ return new EmptyAnswerHint();
+ }
+
+ public static void increaseAnsweredCount(Context context) {
+ SharedPreferences sharedPreferences = getDeviceProtectedPreferences(context);
+ int answeredCount = sharedPreferences.getInt(ANSWERED_COUNT_PREFERENCE_KEY, 0);
+ sharedPreferences.edit().putInt(ANSWERED_COUNT_PREFERENCE_KEY, answeredCount + 1).apply();
+ }
+
+ @VisibleForTesting
+ static boolean shouldShowAnswerHint(
+ Context context,
+ ConfigProvider configProvider,
+ SharedPreferences sharedPreferences,
+ String device) {
+ if (AccessibilityUtil.isTouchExplorationEnabled(context)) {
+ return false;
+ }
+ // Devices that has the legacy dialer installed are whitelisted as they are likely to go through
+ // a UX change during updates.
+ if (!isDeviceWhitelisted(device, configProvider)) {
+ return false;
+ }
+
+ // If the user has gone through the process a few times we can assume they have learnt the
+ // method.
+ int answeredCount = sharedPreferences.getInt(ANSWERED_COUNT_PREFERENCE_KEY, 0);
+ long threshold = configProvider.getLong(CONFIG_ANSWER_HINT_ANSWERED_THRESHOLD_KEY, 3);
+ LogUtil.i(
+ "AnswerHintFactory.shouldShowAnswerHint",
+ "answerCount: %d, threshold: %d",
+ answeredCount,
+ threshold);
+ return answeredCount < threshold;
+ }
+
+ /**
+ * @param device should be the value of{@link Build#PRODUCT}.
+ * @param configProvider should provide a list of devices quoted with '/' concatenated to a
+ * string.
+ */
+ private static boolean isDeviceWhitelisted(String device, ConfigProvider configProvider) {
+ return configProvider
+ .getString(CONFIG_ANSWER_HINT_WHITELISTED_DEVICES_KEY, DEFAULT_WHITELISTED_DEVICES_CSV)
+ .contains("/" + device + "/");
+ }
+
+ private static SharedPreferences getDeviceProtectedPreferences(Context context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+ return PreferenceManager.getDefaultSharedPreferences(context);
+ }
+ return PreferenceManager.getDefaultSharedPreferences(
+ context.createDeviceProtectedStorageContext());
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java b/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java
new file mode 100644
index 000000000..394fe5808
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/DotAnswerHint.java
@@ -0,0 +1,283 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.support.annotation.DimenRes;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.TextView;
+
+/** An Answer hint that uses a green swiping dot. */
+public class DotAnswerHint implements AnswerHint {
+
+ private static final float ANSWER_HINT_SMALL_ALPHA = 0.8f;
+ private static final float ANSWER_HINT_MID_ALPHA = 0.5f;
+ private static final float ANSWER_HINT_LARGE_ALPHA = 0.2f;
+
+ private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
+ private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
+ private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340;
+ private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50;
+
+ private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500;
+
+ private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90;
+ private static final long FADE_OUT_DELAY_SCALE_MID_MILLIS = 70;
+ private static final long FADE_OUT_DELAY_SCALE_LARGE_MILLIS = 10;
+ private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100;
+ private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
+ private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
+
+ private final Context context;
+ private final long puckUpDurationMillis;
+ private final long puckUpDelayMillis;
+
+ private View puck;
+
+ private View answerHintSmall;
+ private View answerHintMid;
+ private View answerHintLarge;
+ private View answerHintContainer;
+ private AnimatorSet answerGestureHintAnim;
+
+ public DotAnswerHint(Context context, long puckUpDurationMillis, long puckUpDelayMillis) {
+ this.context = context;
+ this.puckUpDurationMillis = puckUpDurationMillis;
+ this.puckUpDelayMillis = puckUpDelayMillis;
+ }
+
+ @Override
+ public void onCreateView(
+ LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
+ this.puck = puck;
+ View view = inflater.inflate(R.layout.dot_hint, container, true);
+ answerHintContainer = view.findViewById(R.id.answer_hint_container);
+ answerHintSmall = view.findViewById(R.id.answer_hint_small);
+ answerHintMid = view.findViewById(R.id.answer_hint_mid);
+ answerHintLarge = view.findViewById(R.id.answer_hint_large);
+ hintText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
+ }
+
+ @Override
+ public void onBounceStart() {
+ if (answerGestureHintAnim == null) {
+ answerGestureHintAnim = new AnimatorSet();
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+
+ Animator fadeIn = createFadeIn();
+
+ Animator swipeUp =
+ ObjectAnimator.ofFloat(
+ answerHintContainer,
+ View.TRANSLATION_Y,
+ puck.getY() - getDimension(R.dimen.hint_offset));
+ swipeUp.setInterpolator(new FastOutSlowInInterpolator());
+ swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS);
+
+ Animator fadeOut = createFadeOut();
+
+ answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis);
+ answerGestureHintAnim.play(swipeUp).after(fadeIn);
+ // The fade out should start fading the alpha just as the puck is dropping. Scaling will start
+ // a bit earlier.
+ answerGestureHintAnim
+ .play(fadeOut)
+ .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS);
+
+ fadeIn.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ answerHintSmall.setAlpha(0);
+ answerHintSmall.setScaleX(1);
+ answerHintSmall.setScaleY(1);
+ answerHintMid.setAlpha(0);
+ answerHintMid.setScaleX(1);
+ answerHintMid.setScaleY(1);
+ answerHintLarge.setAlpha(0);
+ answerHintLarge.setScaleX(1);
+ answerHintLarge.setScaleY(1);
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+ answerHintContainer.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ answerGestureHintAnim.start();
+ }
+
+ private Animator createFadeIn() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(
+ createFadeInScaleAndAlpha(
+ answerHintSmall,
+ R.dimen.hint_small_begin_size,
+ R.dimen.hint_small_end_size,
+ ANSWER_HINT_SMALL_ALPHA))
+ .with(
+ createFadeInScaleAndAlpha(
+ answerHintMid,
+ R.dimen.hint_mid_begin_size,
+ R.dimen.hint_mid_end_size,
+ ANSWER_HINT_MID_ALPHA))
+ .with(
+ createFadeInScaleAndAlpha(
+ answerHintLarge,
+ R.dimen.hint_large_begin_size,
+ R.dimen.hint_large_end_size,
+ ANSWER_HINT_LARGE_ALPHA));
+ return set;
+ }
+
+ private Animator createFadeInScaleAndAlpha(
+ View target, @DimenRes int beginSize, @DimenRes int endSize, float endAlpha) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ getDimension(beginSize),
+ getDimension(beginSize),
+ getDimension(endSize),
+ FADE_IN_DURATION_SCALE_MILLIS,
+ FADE_IN_DELAY_SCALE_MILLIS,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ 0f,
+ endAlpha,
+ FADE_IN_DURATION_ALPHA_MILLIS,
+ FADE_IN_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ private Animator createFadeOut() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(
+ createFadeOutScaleAndAlpha(
+ answerHintSmall,
+ R.dimen.hint_small_begin_size,
+ R.dimen.hint_small_end_size,
+ FADE_OUT_DELAY_SCALE_SMALL_MILLIS,
+ ANSWER_HINT_SMALL_ALPHA))
+ .with(
+ createFadeOutScaleAndAlpha(
+ answerHintMid,
+ R.dimen.hint_mid_begin_size,
+ R.dimen.hint_mid_end_size,
+ FADE_OUT_DELAY_SCALE_MID_MILLIS,
+ ANSWER_HINT_MID_ALPHA))
+ .with(
+ createFadeOutScaleAndAlpha(
+ answerHintLarge,
+ R.dimen.hint_large_begin_size,
+ R.dimen.hint_large_end_size,
+ FADE_OUT_DELAY_SCALE_LARGE_MILLIS,
+ ANSWER_HINT_LARGE_ALPHA));
+ return set;
+ }
+
+ private Animator createFadeOutScaleAndAlpha(
+ View target,
+ @DimenRes int beginSize,
+ @DimenRes int endSize,
+ long scaleDelay,
+ float endAlpha) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ getDimension(beginSize),
+ getDimension(endSize),
+ getDimension(beginSize),
+ FADE_OUT_DURATION_SCALE_MILLIS,
+ scaleDelay,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ endAlpha,
+ 0.0f,
+ FADE_OUT_DURATION_ALPHA_MILLIS,
+ FADE_OUT_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ @Override
+ public void onBounceEnd() {
+ if (answerGestureHintAnim != null) {
+ answerGestureHintAnim.end();
+ answerGestureHintAnim = null;
+ answerHintContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onAnswered() {
+ AnswerHintFactory.increaseAnsweredCount(context);
+ }
+
+ private float getDimension(@DimenRes int id) {
+ return context.getResources().getDimension(id);
+ }
+
+ private static Animator createUniformScaleAnimator(
+ View target,
+ float original,
+ float begin,
+ float end,
+ long duration,
+ long delay,
+ Interpolator interpolator) {
+ float scaleBegin = begin / original;
+ float scaleEnd = end / original;
+ Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd);
+ Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd);
+ scaleX.setDuration(duration);
+ scaleY.setDuration(duration);
+ scaleX.setInterpolator(interpolator);
+ scaleY.setInterpolator(interpolator);
+ AnimatorSet set = new AnimatorSet();
+ set.play(scaleX).with(scaleY).after(delay);
+ return set;
+ }
+
+ private static Animator createAlphaAnimator(
+ View target, float begin, float end, long duration, long delay, Interpolator interpolator) {
+ Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end);
+ alpha.setDuration(duration);
+ alpha.setInterpolator(interpolator);
+ alpha.setStartDelay(delay);
+ return alpha;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java
new file mode 100644
index 000000000..e52b4ee36
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EmptyAnswerHint.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.incallui.answer.impl.hint;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+/** Does nothing. Used to avoid null checks on AnswerHint. */
+public class EmptyAnswerHint implements AnswerHint {
+
+ @Override
+ public void onCreateView(
+ LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {}
+
+ @Override
+ public void onBounceStart() {}
+
+ @Override
+ public void onBounceEnd() {}
+
+ @Override
+ public void onAnswered() {}
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
new file mode 100644
index 000000000..7ee327d50
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventAnswerHint.java
@@ -0,0 +1,235 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DimenRes;
+import android.support.annotation.NonNull;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+
+/**
+ * An Answer hint that animates a {@link Drawable} payload with animation similar to {@link
+ * DotAnswerHint}.
+ */
+public final class EventAnswerHint implements AnswerHint {
+
+ private static final long FADE_IN_DELAY_SCALE_MILLIS = 380;
+ private static final long FADE_IN_DURATION_SCALE_MILLIS = 200;
+ private static final long FADE_IN_DELAY_ALPHA_MILLIS = 340;
+ private static final long FADE_IN_DURATION_ALPHA_MILLIS = 50;
+
+ private static final long SWIPE_UP_DURATION_ALPHA_MILLIS = 500;
+
+ private static final long FADE_OUT_DELAY_SCALE_SMALL_MILLIS = 90;
+ private static final long FADE_OUT_DURATION_SCALE_MILLIS = 100;
+ private static final long FADE_OUT_DELAY_ALPHA_MILLIS = 130;
+ private static final long FADE_OUT_DURATION_ALPHA_MILLIS = 170;
+
+ private static final float FADE_SCALE = 1.2f;
+
+ private final Context context;
+ private final Drawable payload;
+ private final long puckUpDurationMillis;
+ private final long puckUpDelayMillis;
+
+ private View puck;
+ private View payloadView;
+ private View answerHintContainer;
+ private AnimatorSet answerGestureHintAnim;
+
+ public EventAnswerHint(
+ @NonNull Context context,
+ @NonNull Drawable payload,
+ long puckUpDurationMillis,
+ long puckUpDelayMillis) {
+ this.context = Assert.isNotNull(context);
+ this.payload = Assert.isNotNull(payload);
+ this.puckUpDurationMillis = puckUpDurationMillis;
+ this.puckUpDelayMillis = puckUpDelayMillis;
+ }
+
+ @Override
+ public void onCreateView(
+ LayoutInflater inflater, ViewGroup container, View puck, TextView hintText) {
+ this.puck = puck;
+ View view = inflater.inflate(R.layout.event_hint, container, true);
+ answerHintContainer = view.findViewById(R.id.answer_hint_container);
+ payloadView = view.findViewById(R.id.payload);
+ hintText.setTextSize(
+ TypedValue.COMPLEX_UNIT_PX, context.getResources().getDimension(R.dimen.hint_text_size));
+ ((ImageView) payloadView).setImageDrawable(payload);
+ }
+
+ @Override
+ public void onBounceStart() {
+ if (answerGestureHintAnim == null) {
+
+ answerGestureHintAnim = new AnimatorSet();
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+
+ Animator fadeIn = createFadeIn();
+
+ Animator swipeUp =
+ ObjectAnimator.ofFloat(
+ answerHintContainer,
+ View.TRANSLATION_Y,
+ puck.getY() - getDimension(R.dimen.hint_offset));
+ swipeUp.setInterpolator(new FastOutSlowInInterpolator());
+ swipeUp.setDuration(SWIPE_UP_DURATION_ALPHA_MILLIS);
+
+ Animator fadeOut = createFadeOut();
+
+ answerGestureHintAnim.play(fadeIn).after(puckUpDelayMillis);
+ answerGestureHintAnim.play(swipeUp).after(fadeIn);
+ // The fade out should start fading the alpha just as the puck is dropping. Scaling will start
+ // a bit earlier.
+ answerGestureHintAnim
+ .play(fadeOut)
+ .after(puckUpDelayMillis + puckUpDurationMillis - FADE_OUT_DELAY_ALPHA_MILLIS);
+
+ fadeIn.addListener(
+ new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ payloadView.setAlpha(0);
+ payloadView.setScaleX(1);
+ payloadView.setScaleY(1);
+ answerHintContainer.setY(puck.getY() + getDimension(R.dimen.hint_initial_offset));
+ answerHintContainer.setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ answerGestureHintAnim.start();
+ }
+
+ private Animator createFadeIn() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(createFadeInScaleAndAlpha(payloadView));
+ return set;
+ }
+
+ private static Animator createFadeInScaleAndAlpha(View target) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ FADE_SCALE,
+ 1.0f,
+ FADE_IN_DURATION_SCALE_MILLIS,
+ FADE_IN_DELAY_SCALE_MILLIS,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ 0f,
+ 1.0f,
+ FADE_IN_DURATION_ALPHA_MILLIS,
+ FADE_IN_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ private Animator createFadeOut() {
+ AnimatorSet set = new AnimatorSet();
+ set.play(createFadeOutScaleAndAlpha(payloadView, FADE_OUT_DELAY_SCALE_SMALL_MILLIS));
+ return set;
+ }
+
+ private static Animator createFadeOutScaleAndAlpha(View target, long scaleDelay) {
+ Animator scale =
+ createUniformScaleAnimator(
+ target,
+ 1.0f,
+ FADE_SCALE,
+ FADE_OUT_DURATION_SCALE_MILLIS,
+ scaleDelay,
+ new LinearInterpolator());
+ Animator alpha =
+ createAlphaAnimator(
+ target,
+ 01.0f,
+ 0.0f,
+ FADE_OUT_DURATION_ALPHA_MILLIS,
+ FADE_OUT_DELAY_ALPHA_MILLIS,
+ new LinearInterpolator());
+ AnimatorSet set = new AnimatorSet();
+ set.play(scale).with(alpha);
+ return set;
+ }
+
+ @Override
+ public void onBounceEnd() {
+ if (answerGestureHintAnim != null) {
+ answerGestureHintAnim.end();
+ answerGestureHintAnim = null;
+ answerHintContainer.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void onAnswered() {
+ // Do nothing
+ }
+
+ private float getDimension(@DimenRes int id) {
+ return context.getResources().getDimension(id);
+ }
+
+ private static Animator createUniformScaleAnimator(
+ View target,
+ float scaleBegin,
+ float scaleEnd,
+ long duration,
+ long delay,
+ Interpolator interpolator) {
+ Animator scaleX = ObjectAnimator.ofFloat(target, View.SCALE_X, scaleBegin, scaleEnd);
+ Animator scaleY = ObjectAnimator.ofFloat(target, View.SCALE_Y, scaleBegin, scaleEnd);
+ scaleX.setDuration(duration);
+ scaleY.setDuration(duration);
+ scaleX.setInterpolator(interpolator);
+ scaleY.setInterpolator(interpolator);
+ AnimatorSet set = new AnimatorSet();
+ set.play(scaleX).with(scaleY).after(delay);
+ return set;
+ }
+
+ private static Animator createAlphaAnimator(
+ View target, float begin, float end, long duration, long delay, Interpolator interpolator) {
+ Animator alpha = ObjectAnimator.ofFloat(target, View.ALPHA, begin, end);
+ alpha.setDuration(duration);
+ alpha.setInterpolator(interpolator);
+ alpha.setStartDelay(delay);
+ return alpha;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.java
new file mode 100644
index 000000000..09e3bedf2
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventPayloadLoader.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.incallui.answer.impl.hint;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import java.util.TimeZone;
+
+/** Loads a {@link Drawable} payload for the {@link EventAnswerHint} if it should be displayed. */
+public interface EventPayloadLoader {
+ @Nullable
+ Drawable loadPayload(
+ @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone);
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
new file mode 100644
index 000000000..bd8d73645
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventPayloadLoaderImpl.java
@@ -0,0 +1,118 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.preference.PreferenceManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProvider;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import java.io.InputStream;
+import java.util.TimeZone;
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
+/** Decrypt the event payload to be shown if in a specific time range and the key is received. */
+@TargetApi(VERSION_CODES.M)
+public final class EventPayloadLoaderImpl implements EventPayloadLoader {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_KEY = "event_key";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_BINARY = "event_binary";
+
+ // Time is stored as a UTC UNIX timestamp in milliseconds, but interpreted as local time.
+ // For example, 946684800 (2000/1/1 00:00:00 @UTC) is the new year midnight at every timezone.
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS = "event_time_start_millis";
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS = "event_time_end_millis";
+
+ @Override
+ @Nullable
+ public Drawable loadPayload(
+ @NonNull Context context, long currentTimeUtcMillis, @NonNull TimeZone timeZone) {
+ Assert.isNotNull(context);
+ Assert.isNotNull(timeZone);
+ ConfigProvider configProvider = ConfigProviderBindings.get(context);
+
+ String pbeKey = configProvider.getString(CONFIG_EVENT_KEY, null);
+ if (pbeKey == null) {
+ return null;
+ }
+ long timeRangeStart = configProvider.getLong(CONFIG_EVENT_START_UTC_AS_LOCAL_MILLIS, 0);
+ long timeRangeEnd = configProvider.getLong(CONFIG_EVENT_TIME_END_UTC_AS_LOCAL_MILLIS, 0);
+
+ String eventBinary = configProvider.getString(CONFIG_EVENT_BINARY, null);
+ if (eventBinary == null) {
+ return null;
+ }
+
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ if (!preferences.getBoolean(
+ EventSecretCodeListener.EVENT_ENABLED_WITH_SECRET_CODE_KEY, false)) {
+ long localTimestamp = currentTimeUtcMillis + timeZone.getRawOffset();
+
+ if (localTimestamp < timeRangeStart) {
+ return null;
+ }
+
+ if (localTimestamp > timeRangeEnd) {
+ return null;
+ }
+ }
+
+ // Use openssl aes-128-cbc -in <input> -out <output> -pass <PBEKey> to generate the asset
+ try (InputStream input = context.getAssets().open(eventBinary)) {
+ byte[] encryptedFile = new byte[input.available()];
+ input.read(encryptedFile);
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding", "BC");
+
+ byte[] salt = new byte[8];
+ System.arraycopy(encryptedFile, 8, salt, 0, 8);
+ SecretKey key =
+ SecretKeyFactory.getInstance("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC")
+ .generateSecret(new PBEKeySpec(pbeKey.toCharArray(), salt, 100));
+ cipher.init(Cipher.DECRYPT_MODE, key);
+
+ byte[] decryptedFile = cipher.doFinal(encryptedFile, 16, encryptedFile.length - 16);
+
+ return new BitmapDrawable(
+ context.getResources(),
+ BitmapFactory.decodeByteArray(decryptedFile, 0, decryptedFile.length));
+ } catch (Exception e) {
+ // Avoid crashing dialer for any reason.
+ LogUtil.e("EventPayloadLoader.loadPayload", "error decrypting payload:", e);
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
new file mode 100644
index 000000000..7cf4054a9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/EventSecretCodeListener.java
@@ -0,0 +1,67 @@
+/*
+ * 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.incallui.answer.impl.hint;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.widget.Toast;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+
+/**
+ * Listen to the broadcast when the user dials "*#*#[number]#*#*" to toggle the event answer hint.
+ */
+public class EventSecretCodeListener extends BroadcastReceiver {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ static final String CONFIG_EVENT_SECRET_CODE = "event_secret_code";
+
+ public static final String EVENT_ENABLED_WITH_SECRET_CODE_KEY = "event_enabled_with_secret_code";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String host = intent.getData().getHost();
+ String secretCode =
+ ConfigProviderBindings.get(context).getString(CONFIG_EVENT_SECRET_CODE, null);
+ if (secretCode == null) {
+ return;
+ }
+ if (!TextUtils.equals(secretCode, host)) {
+ return;
+ }
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean wasEnabled = preferences.getBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false);
+ if (wasEnabled) {
+ preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, false).apply();
+ Toast.makeText(context, R.string.event_deactivated, Toast.LENGTH_SHORT).show();
+ Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_DEACTIVATED);
+ LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint disabled");
+ } else {
+ preferences.edit().putBoolean(EVENT_ENABLED_WITH_SECRET_CODE_KEY, true).apply();
+ Toast.makeText(context, R.string.event_activated, Toast.LENGTH_SHORT).show();
+ Logger.get(context).logImpression(DialerImpression.Type.EVENT_ANSWER_HINT_ACTIVATED);
+ LogUtil.i("EventSecretCodeListener.onReceive", "EventAnswerHint enabled");
+ }
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml
new file mode 100644
index 000000000..f585ce5c9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_large.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="#00C853"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml
new file mode 100644
index 000000000..f585ce5c9
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_mid.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="#00C853"/>
+</shape> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml
new file mode 100644
index 000000000..6a24d6a5f
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/drawable/answer_hint_small.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="#00C853"/>
+ <stroke android:color="#00C853" android:width="2dp"/>
+ </shape> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml
new file mode 100644
index 000000000..84b10e736
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/dot_hint.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/answer_hint_container"
+ android:layout_width="160dp"
+ android:layout_height="160dp"
+ android:layout_gravity="center_horizontal"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/answer_hint_large"
+ android:layout_width="@dimen/hint_large_begin_size"
+ android:layout_height="@dimen/hint_large_begin_size"
+ android:layout_gravity="center"
+ android:alpha="0"
+ android:src="@drawable/answer_hint_large"/>
+ <ImageView
+ android:id="@+id/answer_hint_mid"
+ android:layout_width="@dimen/hint_mid_begin_size"
+ android:layout_height="@dimen/hint_mid_begin_size"
+ android:src="@drawable/answer_hint_mid"
+ android:alpha="0"
+ android:layout_gravity="center"/>
+ <ImageView
+ android:id="@+id/answer_hint_small"
+ android:layout_width="@dimen/hint_small_begin_size"
+ android:layout_height="@dimen/hint_small_begin_size"
+ android:src="@drawable/answer_hint_small"
+ android:alpha="0"
+ android:layout_gravity="center" />
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
new file mode 100644
index 000000000..d505014c1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/layout/event_hint.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/answer_hint_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:visibility="gone">
+ <ImageView
+ android:id="@+id/payload"
+ android:layout_width="191dp"
+ android:layout_height="773dp"
+ android:layout_gravity="center"
+ android:alpha="0"
+ android:rotation="-30"
+ android:transformPivotY="90dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml b/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml
new file mode 100644
index 000000000..d86084b74
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/values/dimens.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="hint_text_size">18sp</dimen>
+ <dimen name="hint_initial_offset">-100dp</dimen>
+ <dimen name="hint_offset">300dp</dimen>
+ <dimen name="hint_small_begin_size">50dp</dimen>
+ <dimen name="hint_small_end_size">42dp</dimen>
+ <dimen name="hint_mid_begin_size">56dp</dimen>
+ <dimen name="hint_mid_end_size">64dp</dimen>
+ <dimen name="hint_large_begin_size">64dp</dimen>
+ <dimen name="hint_large_end_size">160dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/hint/res/values/strings.xml b/java/com/android/incallui/answer/impl/hint/res/values/strings.xml
new file mode 100644
index 000000000..d76021ae1
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/hint/res/values/strings.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="event_activated">Event Activated</string>
+ <string name="event_deactivated">Event Deactvated</string>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml
new file mode 100644
index 000000000..6490bbc5b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_icon_entry.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:ordering="together">
+ <alpha
+ android:duration="583"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:startOffset="167"
+ android:toAlpha="1.0"/>
+ <scale
+ android:duration="600"
+ android:fromXScale="0px"
+ android:fromYScale="0px"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:pivotX="50%"
+ android:pivotY="50%"
+ android:toXScale="100%"
+ android:toYScale="100%"/>
+</set>
diff --git a/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml
new file mode 100644
index 000000000..9d3195a79
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/anim/incoming_unlocked_text_entry.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <alpha
+ android:duration="583"
+ android:fromAlpha="0.0"
+ android:interpolator="@android:anim/accelerate_interpolator"
+ android:startOffset="167"
+ android:toAlpha="1.0"/>
+</set>
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml
new file mode 100644
index 000000000..d656ceb4e
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_avatar.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+
+<ImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@id/contactgrid_avatar"
+ android:layout_width="@dimen/answer_avatar_size"
+ android:layout_height="@dimen/answer_avatar_size"
+ android:layout_marginTop="20dp"
+ android:layout_gravity="center_horizontal"
+ android:elevation="@dimen/answer_data_elevation"/>
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml
new file mode 100644
index 000000000..c36386ead
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_custom_sms_dialog.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp">
+
+ <EditText
+ android:id="@+id/custom_sms_input"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
new file mode 100644
index 000000000..aa153dd4b
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/layout/fragment_incoming_call.xml
@@ -0,0 +1,152 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<com.android.incallui.answer.impl.AffordanceHolderLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/incoming_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:keepScreenOn="true">
+
+ <TextureView
+ android:id="@+id/incoming_preview_texture_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no"
+ android:visibility="gone"/>
+
+ <View
+ android:id="@+id/incoming_preview_texture_view_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/videocall_overlay_background_color"
+ android:visibility="gone"/>
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <TextView
+ android:id="@+id/videocall_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:padding="64dp"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:gravity="center"
+ android:text="@string/call_incoming_video_is_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"/>
+
+ <LinearLayout
+ android:id="@+id/incall_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginTop="24dp"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="top|center_horizontal"
+ android:orientation="vertical">
+
+ <include
+ android:id="@id/contactgrid_top_row"
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ android:textSize="@dimen/answer_contact_name_text_size"
+ app:autoResizeText_minTextSize="@dimen/answer_contact_name_min_size"
+ tools:ignore="Deprecated"
+ tools:text="Jake Peralta"/>
+
+ <include
+ android:id="@id/contactgrid_bottom_row"
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <TextView
+ android:id="@+id/incall_important_call_badge"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="@dimen/answer_importance_margin_bottom"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center"
+ android:singleLine="true"
+ android:text="@string/call_incoming_important"
+ android:textAllCaps="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:textColor="@android:color/black"/>
+
+ <FrameLayout
+ android:id="@+id/incall_location_holder"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <FrameLayout
+ android:id="@+id/incall_data_container"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/answer_data_size"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/answer_method_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clipChildren="false"
+ android:clipToPadding="false"/>
+
+ </FrameLayout>
+
+ <com.android.incallui.answer.impl.affordance.SwipeButtonView
+ android:id="@+id/incoming_secondary_button"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:layout_gravity="bottom|start"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_message_white_24"
+ android:visibility="invisible"
+ tools:visibility="visible"/>
+
+</com.android.incallui.answer.impl.AffordanceHolderLayout>
diff --git a/java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml
new file mode 100644
index 000000000..ca384ef8d
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h240dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="answer_contact_name_text_size">36sp</dimen>
+ <dimen name="answer_contact_name_min_size">32sp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml
new file mode 100644
index 000000000..fdecbb7bf
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h300dp/dimens.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="answer_contact_name_text_size">54sp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml
new file mode 100644
index 000000000..5dc3f2ac5
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h480dp/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <dimen name="answer_data_size">150dp</dimen>
+ <dimen name="answer_avatar_size">100dp</dimen>
+ <dimen name="answer_importance_margin_bottom">8dp</dimen>
+ <bool name="answer_important_call_allowed">true</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml b/java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml
new file mode 100644
index 000000000..69716e0bd
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values-h540dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <dimen name="answer_data_size">258dp</dimen>
+ <dimen name="answer_avatar_size">172dp</dimen>
+ <dimen name="answer_importance_margin_bottom">8dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values/dimens.xml b/java/com/android/incallui/answer/impl/res/values/dimens.xml
new file mode 100644
index 000000000..c48b68f93
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="answer_contact_name_text_size">24sp</dimen>
+ <dimen name="answer_contact_name_min_size">24sp</dimen>
+ <dimen name="answer_data_size">0dp</dimen>
+ <dimen name="answer_avatar_size">0dp</dimen>
+ <dimen name="answer_importance_margin_bottom">0dp</dimen>
+ <bool name="answer_important_call_allowed">false</bool>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/res/values/strings.xml b/java/com/android/incallui/answer/impl/res/values/strings.xml
new file mode 100644
index 000000000..7fc91fce4
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/res/values/strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="call_incoming_swipe_to_decline_with_message">Swipe from icon to decline with message</string>
+ <string name="call_incoming_swipe_to_answer_video_as_audio">Swipe from icon to answer as an audio call</string>
+ <string name="call_incoming_message_custom">Write your own…</string>
+ <string name="call_incoming_audio_handset">Handset</string>
+ <string name="call_incoming_audio_speakerphone">Speakerphone</string>
+ <!-- "Respond via SMS" option that lets you compose a custom response. [CHAR LIMIT=30] -->
+ <string name="call_incoming_respond_via_sms_custom_message">Write your own…</string>
+ <!-- "Custom Message" Cancel alert dialog button -->
+ <string name="call_incoming_custom_message_cancel">Cancel</string>
+ <!-- "Custom Message" Send alert dialog button -->
+ <string name="call_incoming_custom_message_send">Send</string>
+ <string name="a11y_incoming_call_reject_with_sms">Reject this call with a message</string>
+ <string name="a11y_incoming_call_answer_video_as_audio">Answer as audio call</string>
+ <string name="a11y_description_incoming_call_reject_with_sms">Reject with message</string>
+ <string name="a11y_description_incoming_call_answer_video_as_audio">Answer as audio call</string>
+
+ <!-- Text indicates the video local camera is off. [CHAR LIMIT=40] -->
+ <string name="call_incoming_video_is_off">Video is off</string>
+
+ <!-- Voice prompt of swipe gesture when accessibility is turned on. -->
+ <string description="The message announced to accessibility assistance on incoming call."
+ name="a11y_incoming_call_swipe_gesture_prompt">Two finger swipe up to answer. Two finger swipe down to decline.</string>
+ <string name="call_incoming_important">Important call</string>
+</resources>
diff --git a/java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java b/java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java
new file mode 100644
index 000000000..3acb2a205
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/utils/FlingAnimationUtils.java
@@ -0,0 +1,293 @@
+/*
+ * 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.incallui.answer.impl.utils;
+
+import android.animation.Animator;
+import android.content.Context;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/** Utility class to calculate general fling animation when the finger is released. */
+public class FlingAnimationUtils {
+
+ private static final float LINEAR_OUT_SLOW_IN_X2 = 0.35f;
+ private static final float LINEAR_OUT_FASTER_IN_X2 = 0.5f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MIN = 0.4f;
+ private static final float LINEAR_OUT_FASTER_IN_Y2_MAX = 0.5f;
+ private static final float MIN_VELOCITY_DP_PER_SECOND = 250;
+ private static final float HIGH_VELOCITY_DP_PER_SECOND = 3000;
+
+ /** Crazy math. http://en.wikipedia.org/wiki/B%C3%A9zier_curve */
+ private static final float LINEAR_OUT_SLOW_IN_START_GRADIENT = 1.0f / LINEAR_OUT_SLOW_IN_X2;
+
+ private Interpolator linearOutSlowIn;
+
+ private float minVelocityPxPerSecond;
+ private float maxLengthSeconds;
+ private float highVelocityPxPerSecond;
+
+ private AnimatorProperties mAnimatorProperties = new AnimatorProperties();
+
+ public FlingAnimationUtils(Context ctx, float maxLengthSeconds) {
+ this.maxLengthSeconds = maxLengthSeconds;
+ linearOutSlowIn = new PathInterpolator(0, 0, LINEAR_OUT_SLOW_IN_X2, 1);
+ minVelocityPxPerSecond =
+ MIN_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+ highVelocityPxPerSecond =
+ HIGH_VELOCITY_DP_PER_SECOND * ctx.getResources().getDisplayMetrics().density;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(Animator animator, float currValue, float endValue, float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ */
+ public void apply(
+ ViewPropertyAnimator animator, float currValue, float endValue, float velocity) {
+ apply(animator, currValue, endValue, velocity, Math.abs(endValue - currValue));
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(
+ Animator animator, float currValue, float endValue, float velocity, float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void apply(
+ ViewPropertyAnimator animator,
+ float currValue,
+ float endValue,
+ float velocity,
+ float maxDistance) {
+ AnimatorProperties properties = getProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ private AnimatorProperties getProperties(
+ float currValue, float endValue, float velocity, float maxDistance) {
+ float maxLengthSeconds =
+ (float) (this.maxLengthSeconds * Math.sqrt(Math.abs(endValue - currValue) / maxDistance));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float durationSeconds = LINEAR_OUT_SLOW_IN_START_GRADIENT * diff / velAbs;
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.interpolator = linearOutSlowIn;
+ } else if (velAbs >= minVelocityPxPerSecond) {
+
+ // Cross fade between fast-out-slow-in and linear interpolator with current velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator =
+ new VelocityInterpolator(durationSeconds, velAbs, diff);
+ mAnimatorProperties.interpolator =
+ new InterpolatorInterpolator(velocityInterpolator, linearOutSlowIn, linearOutSlowIn);
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.interpolator = Interpolators.FAST_OUT_SLOW_IN;
+ }
+ mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(
+ Animator animator, float currValue, float endValue, float velocity, float maxDistance) {
+ AnimatorProperties properties =
+ getDismissingProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ /**
+ * Applies the interpolator and length to the animator, such that the fling animation is
+ * consistent with the finger motion for the case when the animation is making something
+ * disappear.
+ *
+ * @param animator the animator to apply
+ * @param currValue the current value
+ * @param endValue the end value of the animator
+ * @param velocity the current velocity of the motion
+ * @param maxDistance the maximum distance for this interaction; the maximum animation length gets
+ * multiplied by the ratio between the actual distance and this value
+ */
+ public void applyDismissing(
+ ViewPropertyAnimator animator,
+ float currValue,
+ float endValue,
+ float velocity,
+ float maxDistance) {
+ AnimatorProperties properties =
+ getDismissingProperties(currValue, endValue, velocity, maxDistance);
+ animator.setDuration(properties.duration);
+ animator.setInterpolator(properties.interpolator);
+ }
+
+ private AnimatorProperties getDismissingProperties(
+ float currValue, float endValue, float velocity, float maxDistance) {
+ float maxLengthSeconds =
+ (float)
+ (this.maxLengthSeconds * Math.pow(Math.abs(endValue - currValue) / maxDistance, 0.5f));
+ float diff = Math.abs(endValue - currValue);
+ float velAbs = Math.abs(velocity);
+ float y2 = calculateLinearOutFasterInY2(velAbs);
+
+ float startGradient = y2 / LINEAR_OUT_FASTER_IN_X2;
+ Interpolator mLinearOutFasterIn = new PathInterpolator(0, 0, LINEAR_OUT_FASTER_IN_X2, y2);
+ float durationSeconds = startGradient * diff / velAbs;
+ if (durationSeconds <= maxLengthSeconds) {
+ mAnimatorProperties.interpolator = mLinearOutFasterIn;
+ } else if (velAbs >= minVelocityPxPerSecond) {
+
+ // Cross fade between linear-out-faster-in and linear interpolator with current
+ // velocity.
+ durationSeconds = maxLengthSeconds;
+ VelocityInterpolator velocityInterpolator =
+ new VelocityInterpolator(durationSeconds, velAbs, diff);
+ InterpolatorInterpolator superInterpolator =
+ new InterpolatorInterpolator(velocityInterpolator, mLinearOutFasterIn, linearOutSlowIn);
+ mAnimatorProperties.interpolator = superInterpolator;
+ } else {
+
+ // Just use a normal interpolator which doesn't take the velocity into account.
+ durationSeconds = maxLengthSeconds;
+ mAnimatorProperties.interpolator = Interpolators.FAST_OUT_LINEAR_IN;
+ }
+ mAnimatorProperties.duration = (long) (durationSeconds * 1000);
+ return mAnimatorProperties;
+ }
+
+ /**
+ * Calculates the y2 control point for a linear-out-faster-in path interpolator depending on the
+ * velocity. The faster the velocity, the more "linear" the interpolator gets.
+ *
+ * @param velocity the velocity of the gesture.
+ * @return the y2 control point for a cubic bezier path interpolator
+ */
+ private float calculateLinearOutFasterInY2(float velocity) {
+ float t =
+ (velocity - minVelocityPxPerSecond) / (highVelocityPxPerSecond - minVelocityPxPerSecond);
+ t = Math.max(0, Math.min(1, t));
+ return (1 - t) * LINEAR_OUT_FASTER_IN_Y2_MIN + t * LINEAR_OUT_FASTER_IN_Y2_MAX;
+ }
+
+ /** @return the minimum velocity a gesture needs to have to be considered a fling */
+ public float getMinVelocityPxPerSecond() {
+ return minVelocityPxPerSecond;
+ }
+
+ /** An interpolator which interpolates two interpolators with an interpolator. */
+ private static final class InterpolatorInterpolator implements Interpolator {
+
+ private Interpolator mInterpolator1;
+ private Interpolator mInterpolator2;
+ private Interpolator mCrossfader;
+
+ InterpolatorInterpolator(
+ Interpolator interpolator1, Interpolator interpolator2, Interpolator crossfader) {
+ mInterpolator1 = interpolator1;
+ mInterpolator2 = interpolator2;
+ mCrossfader = crossfader;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float t = mCrossfader.getInterpolation(input);
+ return (1 - t) * mInterpolator1.getInterpolation(input)
+ + t * mInterpolator2.getInterpolation(input);
+ }
+ }
+
+ /** An interpolator which interpolates with a fixed velocity. */
+ private static final class VelocityInterpolator implements Interpolator {
+
+ private float mDurationSeconds;
+ private float mVelocity;
+ private float mDiff;
+
+ private VelocityInterpolator(float durationSeconds, float velocity, float diff) {
+ mDurationSeconds = durationSeconds;
+ mVelocity = velocity;
+ mDiff = diff;
+ }
+
+ @Override
+ public float getInterpolation(float input) {
+ float time = input * mDurationSeconds;
+ return time * mVelocity / mDiff;
+ }
+ }
+
+ private static class AnimatorProperties {
+
+ Interpolator interpolator;
+ long duration;
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/utils/Interpolators.java b/java/com/android/incallui/answer/impl/utils/Interpolators.java
new file mode 100644
index 000000000..efc68f78a
--- /dev/null
+++ b/java/com/android/incallui/answer/impl/utils/Interpolators.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.incallui.answer.impl.utils;
+
+import android.view.animation.Interpolator;
+import android.view.animation.PathInterpolator;
+
+/**
+ * Common interpolators used in answer methods.
+ */
+public class Interpolators {
+
+ public static final Interpolator FAST_OUT_LINEAR_IN = new PathInterpolator(0.4f, 0f, 1f, 1f);
+ public static final Interpolator LINEAR_OUT_SLOW_IN = new PathInterpolator(0f, 0f, 0.2f, 1f);
+ public static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
+}
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreen.java b/java/com/android/incallui/answer/protocol/AnswerScreen.java
new file mode 100644
index 000000000..0c374eb7f
--- /dev/null
+++ b/java/com/android/incallui/answer/protocol/AnswerScreen.java
@@ -0,0 +1,38 @@
+/*
+ * 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.incallui.answer.protocol;
+
+import android.support.v4.app.Fragment;
+import java.util.List;
+
+/** Interface for the answer module. */
+public interface AnswerScreen {
+
+ String getCallId();
+
+ int getVideoState();
+
+ boolean isVideoUpgradeRequest();
+
+ void setTextResponses(List<String> textResponses);
+
+ boolean hasPendingDialogs();
+
+ void dismissPendingDialogs();
+
+ Fragment getAnswerScreenFragment();
+}
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
new file mode 100644
index 000000000..9934497cf
--- /dev/null
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegate.java
@@ -0,0 +1,44 @@
+/*
+ * 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.incallui.answer.protocol;
+
+import android.support.annotation.FloatRange;
+
+/** Callbacks implemented by the container app for this module. */
+public interface AnswerScreenDelegate {
+
+ void onAnswerScreenUnready();
+
+ void onDismissDialog();
+
+ void onRejectCallWithMessage(String message);
+
+ void onAnswer(int videoState);
+
+ void onReject();
+
+ /**
+ * Sets the window background color based on foreground call's theme and the given progress. This
+ * is called from the answer UI to animate the accept and reject action.
+ *
+ * <p>When the user is rejecting we animate the background color to a mostly transparent gray. The
+ * end effect is that the home screen shows through.
+ *
+ * @param progress float from -1 to 1. -1 is fully rejected, 1 is fully accepted, and 0 is neutral
+ */
+ void updateWindowBackgroundColor(@FloatRange(from = -1f, to = 1.0f) float progress);
+}
diff --git a/java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java b/java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java
new file mode 100644
index 000000000..a09cb1a40
--- /dev/null
+++ b/java/com/android/incallui/answer/protocol/AnswerScreenDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.incallui.answer.protocol;
+
+/** Used to create an instance of the delegate, should be implemented by the container activity. */
+public interface AnswerScreenDelegateFactory {
+
+ AnswerScreenDelegate newAnswerScreenDelegate(AnswerScreen answerScreen);
+}
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
new file mode 100644
index 000000000..edc3db34b
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -0,0 +1,150 @@
+/*
+ * 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.incallui.answerproximitysensor;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.PowerManager;
+import android.view.Display;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.DialerCallListener;
+
+/**
+ * This class prevents users from accidentally answering calls by keeping the screen off until the
+ * proximity sensor is unblocked. If the screen is already on or if this is a call waiting call then
+ * nothing is done.
+ */
+public class AnswerProximitySensor
+ implements DialerCallListener, AnswerProximityWakeLock.ScreenOnListener {
+
+ private static final String CONFIG_ANSWER_PROXIMITY_SENSOR_ENABLED =
+ "answer_proximity_sensor_enabled";
+ private static final String CONFIG_ANSWER_PSEUDO_PROXIMITY_WAKE_LOCK_ENABLED =
+ "answer_pseudo_proximity_wake_lock_enabled";
+
+ private final DialerCall call;
+ private final AnswerProximityWakeLock answerProximityWakeLock;
+
+ public static boolean shouldUse(Context context, DialerCall call) {
+ // Don't use the AnswerProximitySensor for call waiting and other states. Those states are
+ // handled by the general ProximitySensor code.
+ if (call.getState() != State.INCOMING) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "call state is not incoming");
+ return false;
+ }
+
+ if (!ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_PROXIMITY_SENSOR_ENABLED, true)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "disabled by config");
+ return false;
+ }
+
+ if (!context
+ .getSystemService(PowerManager.class)
+ .isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "wake lock level not supported");
+ return false;
+ }
+
+ if (isDefaultDisplayOn(context)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "display is already on");
+ return false;
+ }
+
+ return true;
+ }
+
+ public AnswerProximitySensor(
+ Context context, DialerCall call, PseudoScreenState pseudoScreenState) {
+ this.call = call;
+
+ LogUtil.i("AnswerProximitySensor.constructor", "acquiring lock");
+ if (ConfigProviderBindings.get(context)
+ .getBoolean(CONFIG_ANSWER_PSEUDO_PROXIMITY_WAKE_LOCK_ENABLED, true)) {
+ answerProximityWakeLock = new PseudoProximityWakeLock(context, pseudoScreenState);
+ } else {
+ // TODO: choose a wake lock implementation base on framework/device.
+ // These bugs requires the PseudoProximityWakeLock workaround:
+ // b/30439151 Proximity sensor not working on M
+ // b/31499931 fautly touch input when screen is off on marlin/sailfish
+ answerProximityWakeLock = new SystemProximityWakeLock(context);
+ }
+ answerProximityWakeLock.setScreenOnListener(this);
+ answerProximityWakeLock.acquire();
+
+ call.addListener(this);
+ }
+
+ private void cleanup() {
+ call.removeListener(this);
+ releaseProximityWakeLock();
+ }
+
+ private void releaseProximityWakeLock() {
+ if (answerProximityWakeLock.isHeld()) {
+ LogUtil.i("AnswerProximitySensor.releaseProximityWakeLock", "releasing lock");
+ answerProximityWakeLock.release();
+ }
+ }
+
+ private static boolean isDefaultDisplayOn(Context context) {
+ Display display =
+ context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY);
+ return display.getState() == Display.STATE_ON;
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {
+ LogUtil.i("AnswerProximitySensor.onDialerCallDisconnect", null);
+ cleanup();
+ }
+
+ @Override
+ public void onDialerCallUpdate() {
+ if (call.getState() != State.INCOMING) {
+ LogUtil.i("AnswerProximitySensor.onDialerCallUpdate", "no longer incoming, cleaning up");
+ cleanup();
+ }
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {}
+
+ @Override
+ public void onWiFiToLteHandover() {}
+
+ @Override
+ public void onHandoverToWifiFailure() {}
+
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {}
+
+ @Override
+ public void onScreenOn() {
+ cleanup();
+ }
+}
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java b/java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java
new file mode 100644
index 000000000..94abe9c85
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximityWakeLock.java
@@ -0,0 +1,37 @@
+/*
+ * 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.incallui.answerproximitysensor;
+
+/**
+ * Interface to wrap around the {@link android.os.PowerManager.WakeLock} for custom implementations.
+ */
+public interface AnswerProximityWakeLock {
+
+ /** Called when the wake lock turned the screen back on. */
+ interface ScreenOnListener {
+
+ void onScreenOn();
+ }
+
+ void acquire();
+
+ void release();
+
+ boolean isHeld();
+
+ void setScreenOnListener(ScreenOnListener listener);
+}
diff --git a/java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java b/java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java
new file mode 100644
index 000000000..c7844d47d
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/PseudoProximityWakeLock.java
@@ -0,0 +1,85 @@
+/*
+ * 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.incallui.answerproximitysensor;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+
+/**
+ * A fake PROXIMITY_SCREEN_OFF_WAKE_LOCK implemented by the app. It will use {@link
+ * PseudoScreenState} to fake a black screen when the proximity sensor is near.
+ */
+public class PseudoProximityWakeLock implements AnswerProximityWakeLock, SensorEventListener {
+
+ private final Context context;
+ private final PseudoScreenState pseudoScreenState;
+ private final Sensor proximitySensor;
+
+ @Nullable private ScreenOnListener listener;
+ private boolean isHeld;
+
+ public PseudoProximityWakeLock(Context context, PseudoScreenState pseudoScreenState) {
+ this.context = context;
+ this.pseudoScreenState = pseudoScreenState;
+ pseudoScreenState.setOn(true);
+ proximitySensor =
+ context.getSystemService(SensorManager.class).getDefaultSensor(Sensor.TYPE_PROXIMITY);
+ }
+
+ @Override
+ public void acquire() {
+ isHeld = true;
+ context
+ .getSystemService(SensorManager.class)
+ .registerListener(this, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL);
+ }
+
+ @Override
+ public void release() {
+ isHeld = false;
+ context.getSystemService(SensorManager.class).unregisterListener(this);
+ pseudoScreenState.setOn(true);
+ }
+
+ @Override
+ public boolean isHeld() {
+ return isHeld;
+ }
+
+ @Override
+ public void setScreenOnListener(ScreenOnListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void onSensorChanged(SensorEvent sensorEvent) {
+ boolean near = sensorEvent.values[0] < sensorEvent.sensor.getMaximumRange();
+ LogUtil.i("AnswerProximitySensor.PseudoProximityWakeLock.onSensorChanged", "near: " + near);
+ pseudoScreenState.setOn(!near);
+ if (!near && listener != null) {
+ listener.onScreenOn();
+ }
+ }
+
+ @Override
+ public void onAccuracyChanged(Sensor sensor, int i) {}
+}
diff --git a/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
new file mode 100644
index 000000000..eda0ee720
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/PseudoScreenState.java
@@ -0,0 +1,66 @@
+/*
+ * 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.incallui.answerproximitysensor;
+
+import android.util.ArraySet;
+import java.util.Set;
+
+/**
+ * Stores a fake screen on/off state for the {@link InCallActivity}. If InCallActivity see the state
+ * is off, it will draw a black view over the activity pretending the screen is off.
+ *
+ * <p>If the screen is already touched when the screen is turned on, the OS behavior is sending a
+ * new DOWN event once the point started moving and then behave as a normal gesture. To prevent
+ * accidental answer/rejects, touches that started when the screen is off should be ignored.
+ *
+ * <p>b/31499931 on certain devices with N-DR1, if the screen is already touched when the screen is
+ * turned on, a "DOWN MOVE UP" will be sent for each movement before the touch is actually released.
+ * These events is hard to discern from other normal events, and keeping the screen on reduces its'
+ * probability.
+ */
+public class PseudoScreenState {
+
+ /** Notifies when the on state has changed. */
+ public interface StateChangedListener {
+ void onPseudoScreenStateChanged(boolean isOn);
+ }
+
+ private final Set<StateChangedListener> listeners = new ArraySet<>();
+
+ private boolean on = true;
+
+ public boolean isOn() {
+ return on;
+ }
+
+ public void setOn(boolean value) {
+ if (on != value) {
+ on = value;
+ for (StateChangedListener listener : listeners) {
+ listener.onPseudoScreenStateChanged(on);
+ }
+ }
+ }
+
+ public void addListener(StateChangedListener listener) {
+ listeners.add(listener);
+ }
+
+ public void removeListener(StateChangedListener listener) {
+ listeners.remove(listener);
+ }
+}
diff --git a/java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java b/java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.java
new file mode 100644
index 000000000..776e9a42d
--- /dev/null
+++ b/java/com/android/incallui/answerproximitysensor/SystemProximityWakeLock.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.incallui.answerproximitysensor;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.DisplayManager.DisplayListener;
+import android.os.PowerManager;
+import android.support.annotation.Nullable;
+import android.view.Display;
+import com.android.dialer.common.LogUtil;
+
+/** The normal PROXIMITY_SCREEN_OFF_WAKE_LOCK provided by the OS. */
+public class SystemProximityWakeLock implements AnswerProximityWakeLock, DisplayListener {
+
+ private static final String TAG = "SystemProximityWakeLock";
+
+ private final Context context;
+ private final PowerManager.WakeLock wakeLock;
+
+ @Nullable private ScreenOnListener listener;
+
+ public SystemProximityWakeLock(Context context) {
+ this.context = context;
+ wakeLock =
+ context
+ .getSystemService(PowerManager.class)
+ .newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, TAG);
+ }
+
+ @Override
+ public void acquire() {
+ wakeLock.acquire();
+ context.getSystemService(DisplayManager.class).registerDisplayListener(this, null);
+ }
+
+ @Override
+ public void release() {
+ wakeLock.release();
+ context.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
+ }
+
+ @Override
+ public boolean isHeld() {
+ return wakeLock.isHeld();
+ }
+
+ @Override
+ public void setScreenOnListener(ScreenOnListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void onDisplayAdded(int displayId) {}
+
+ @Override
+ public void onDisplayRemoved(int displayId) {}
+
+ @Override
+ public void onDisplayChanged(int displayId) {
+ if (displayId == Display.DEFAULT_DISPLAY) {
+ if (isDefaultDisplayOn(context)) {
+ LogUtil.i("SystemProximityWakeLock.onDisplayChanged", "display turned on");
+ if (listener != null) {
+ listener.onScreenOn();
+ }
+ }
+ }
+ }
+
+ private static boolean isDefaultDisplayOn(Context context) {
+ Display display =
+ context.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY);
+ return display.getState() != Display.STATE_OFF;
+ }
+}
diff --git a/java/com/android/incallui/async/PausableExecutor.java b/java/com/android/incallui/async/PausableExecutor.java
new file mode 100644
index 000000000..e10757e67
--- /dev/null
+++ b/java/com/android/incallui/async/PausableExecutor.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.incallui.async;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Executor that can be used to easily synchronize testing and production code. Production code
+ * should call {@link #milestone()} at points in the code where the state of the system is worthy of
+ * testing. In a test scenario, this method will pause execution until the test acknowledges the
+ * milestone through the use of {@link #ackMilestoneForTesting()}.
+ */
+public interface PausableExecutor extends Executor {
+
+ /**
+ * Method called from asynchronous production code to inform this executor that it has reached a
+ * point that puts the system into a state worth testing. TestableExecutors intended for use in a
+ * testing environment should cause the calling thread to block. In the production environment
+ * this should be a no-op.
+ */
+ void milestone();
+
+ /**
+ * Method called from the test code to inform this executor that the state of the production
+ * system at the current milestone has been sufficiently tested. Every milestone must be
+ * acknowledged.
+ */
+ void ackMilestoneForTesting();
+
+ /**
+ * Method called from the test code to inform this executor that the tests are finished with all
+ * milestones. Future calls to {@link #milestone()} or {@link #awaitMilestoneForTesting()} should
+ * return immediately.
+ */
+ void ackAllMilestonesForTesting();
+
+ /**
+ * Method called from the test code to block until a milestone has been reached in the production
+ * code.
+ */
+ void awaitMilestoneForTesting() throws InterruptedException;
+}
diff --git a/java/com/android/incallui/async/PausableExecutorImpl.java b/java/com/android/incallui/async/PausableExecutorImpl.java
new file mode 100644
index 000000000..687606129
--- /dev/null
+++ b/java/com/android/incallui/async/PausableExecutorImpl.java
@@ -0,0 +1,40 @@
+/*
+ * 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.incallui.async;
+
+import java.util.concurrent.Executors;
+
+/** {@link PausableExecutor} intended for use in production environments. */
+public class PausableExecutorImpl implements PausableExecutor {
+
+ @Override
+ public void milestone() {}
+
+ @Override
+ public void ackMilestoneForTesting() {}
+
+ @Override
+ public void ackAllMilestonesForTesting() {}
+
+ @Override
+ public void awaitMilestoneForTesting() {}
+
+ @Override
+ public void execute(Runnable command) {
+ Executors.newSingleThreadExecutor().execute(command);
+ }
+}
diff --git a/java/com/android/incallui/audioroute/AndroidManifest.xml b/java/com/android/incallui/audioroute/AndroidManifest.xml
new file mode 100644
index 000000000..36431f1ee
--- /dev/null
+++ b/java/com/android/incallui/audioroute/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.audioroute">
+</manifest>
diff --git a/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
new file mode 100644
index 000000000..c757477f1
--- /dev/null
+++ b/java/com/android/incallui/audioroute/AudioRouteSelectorDialogFragment.java
@@ -0,0 +1,114 @@
+/*
+ * 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.incallui.audioroute;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.PorterDuff.Mode;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetDialogFragment;
+import android.telecom.CallAudioState;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.TextView;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+
+/** Shows picker for audio routes */
+public class AudioRouteSelectorDialogFragment extends BottomSheetDialogFragment {
+
+ private static final String ARG_AUDIO_STATE = "audio_state";
+
+ /** Called when an audio route is picked */
+ public interface AudioRouteSelectorPresenter {
+ void onAudioRouteSelected(int audioRoute);
+ }
+
+ public static AudioRouteSelectorDialogFragment newInstance(CallAudioState audioState) {
+ AudioRouteSelectorDialogFragment fragment = new AudioRouteSelectorDialogFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_AUDIO_STATE, audioState);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ FragmentUtils.checkParent(this, AudioRouteSelectorPresenter.class);
+ }
+
+ @Override
+ public Dialog onCreateDialog(final Bundle savedInstanceState) {
+ LogUtil.i("AudioRouteSelectorDialogFragment.onCreateDialog", null);
+ Dialog dialog = super.onCreateDialog(savedInstanceState);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+ return dialog;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ View view = layoutInflater.inflate(R.layout.audioroute_selector, viewGroup, false);
+ CallAudioState audioState = getArguments().getParcelable(ARG_AUDIO_STATE);
+
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_bluetooth),
+ CallAudioState.ROUTE_BLUETOOTH,
+ audioState);
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_speaker),
+ CallAudioState.ROUTE_SPEAKER,
+ audioState);
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_headset),
+ CallAudioState.ROUTE_WIRED_HEADSET,
+ audioState);
+ initItem(
+ (TextView) view.findViewById(R.id.audioroute_earpiece),
+ CallAudioState.ROUTE_EARPIECE,
+ audioState);
+ return view;
+ }
+
+ private void initItem(TextView item, final int itemRoute, CallAudioState audioState) {
+ int selectedColor = getResources().getColor(R.color.dialer_theme_color);
+ if ((audioState.getSupportedRouteMask() & itemRoute) == 0) {
+ item.setVisibility(View.GONE);
+ } else if (audioState.getRoute() == itemRoute) {
+ item.setTextColor(selectedColor);
+ item.setCompoundDrawableTintList(ColorStateList.valueOf(selectedColor));
+ item.setCompoundDrawableTintMode(Mode.SRC_ATOP);
+ }
+ item.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ FragmentUtils.getParentUnsafe(
+ AudioRouteSelectorDialogFragment.this, AudioRouteSelectorPresenter.class)
+ .onAudioRouteSelected(itemRoute);
+ }
+ });
+ }
+}
diff --git a/java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..4ea921a3e
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-hdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..acef550ac
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-mdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..a30aa5c0c
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-xhdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png b/java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png
new file mode 100644
index 000000000..beb85a80a
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/drawable-xxhdpi/ic_phone_audio_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
new file mode 100644
index 000000000..ef2220e8f
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/layout/audioroute_selector.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:layout_gravity="bottom">
+ <TextView
+ android:id="@+id/audioroute_bluetooth"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_bluetooth_audio_grey600_24"
+ android:text="@string/audioroute_bluetooth"/>
+ <TextView
+ android:id="@+id/audioroute_speaker"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_volume_up_grey600_24"
+ android:text="@string/audioroute_speaker"/>
+ <TextView
+ android:id="@+id/audioroute_earpiece"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/ic_phone_audio_grey600_24dp"
+ android:text="@string/audioroute_phone"/>
+ <TextView
+ android:id="@+id/audioroute_headset"
+ style="@style/AudioRouteItem"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_headset_grey600_24"
+ android:text="@string/audioroute_headset"/>
+
+</LinearLayout>
diff --git a/java/com/android/incallui/audioroute/res/values/strings.xml b/java/com/android/incallui/audioroute/res/values/strings.xml
new file mode 100644
index 000000000..b16639354
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/values/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="audioroute_bluetooth">Bluetooth</string>
+ <string name="audioroute_speaker">Speaker</string>
+ <string name="audioroute_phone">Phone</string>
+ <string name="audioroute_headset">Wired headset</string>
+</resources>
diff --git a/java/com/android/incallui/audioroute/res/values/styles.xml b/java/com/android/incallui/audioroute/res/values/styles.xml
new file mode 100644
index 000000000..4484b7092
--- /dev/null
+++ b/java/com/android/incallui/audioroute/res/values/styles.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="AudioRouteItem">
+ <item name="android:padding">16dp</item>
+ <item name="android:background">?android:selectableItemBackground</item>
+ <item name="android:drawablePadding">24dp</item>
+ <item name="android:gravity">center_vertical</item>
+ <item name="android:textAppearance">
+ @style/TextAppearance.AppCompat.Light.Widget.PopupMenu.Large
+ </item>
+ <item name="android:textColor">?android:textColorSecondary</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/autoresizetext/AndroidManifest.xml b/java/com/android/incallui/autoresizetext/AndroidManifest.xml
new file mode 100644
index 000000000..53a8961e4
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/AndroidManifest.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.incallui.autoresizetext">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <application />
+</manifest>
diff --git a/java/com/android/incallui/autoresizetext/AutoResizeTextView.java b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
new file mode 100644
index 000000000..eedcbe5bb
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/AutoResizeTextView.java
@@ -0,0 +1,316 @@
+/*
+ * 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.incallui.autoresizetext;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.RectF;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.text.Layout.Alignment;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.widget.TextView;
+import javax.annotation.Nullable;
+
+/**
+ * A TextView that automatically scales its text to completely fill its allotted width.
+ *
+ * <p>Note: In some edge cases, the binary search algorithm to find the best fit may slightly
+ * overshoot / undershoot its constraints. See b/26704434. No minimal repro case has been
+ * found yet. A known workaround is the solution provided on StackOverflow:
+ * http://stackoverflow.com/a/5535672
+ */
+public class AutoResizeTextView extends TextView {
+ private static final int NO_LINE_LIMIT = -1;
+ private static final float DEFAULT_MIN_TEXT_SIZE = 16.0f;
+ private static final int DEFAULT_RESIZE_STEP_UNIT = TypedValue.COMPLEX_UNIT_PX;
+
+ private final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ private final RectF availableSpaceRect = new RectF();
+ private final SparseIntArray textSizesCache = new SparseIntArray();
+ private final TextPaint textPaint = new TextPaint();
+ private int resizeStepUnit = DEFAULT_RESIZE_STEP_UNIT;
+ private float minTextSize = DEFAULT_MIN_TEXT_SIZE;
+ private float maxTextSize;
+ private int maxWidth;
+ private int maxLines;
+ private float lineSpacingMultiplier = 1.0f;
+ private float lineSpacingExtra = 0.0f;
+
+ public AutoResizeTextView(Context context) {
+ super(context, null, 0);
+ initialize(context, null, 0, 0);
+ }
+
+ public AutoResizeTextView(Context context, AttributeSet attrs) {
+ super(context, attrs, 0);
+ initialize(context, attrs, 0, 0);
+ }
+
+ public AutoResizeTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ initialize(context, attrs, defStyleAttr, 0);
+ }
+
+ public AutoResizeTextView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ initialize(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ private void initialize(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ TypedArray typedArray = context.getTheme().obtainStyledAttributes(
+ attrs, R.styleable.AutoResizeTextView, defStyleAttr, defStyleRes);
+ readAttrs(typedArray);
+ textPaint.set(getPaint());
+ }
+
+ /** Overridden because getMaxLines is only defined in JB+. */
+ @Override
+ public final int getMaxLines() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getMaxLines();
+ } else {
+ return maxLines;
+ }
+ }
+
+ /** Overridden because getMaxLines is only defined in JB+. */
+ @Override
+ public final void setMaxLines(int maxLines) {
+ super.setMaxLines(maxLines);
+ this.maxLines = maxLines;
+ }
+
+ /** Overridden because getLineSpacingMultiplier is only defined in JB+. */
+ @Override
+ public final float getLineSpacingMultiplier() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getLineSpacingMultiplier();
+ } else {
+ return lineSpacingMultiplier;
+ }
+ }
+
+ /** Overridden because getLineSpacingExtra is only defined in JB+. */
+ @Override
+ public final float getLineSpacingExtra() {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
+ return super.getLineSpacingExtra();
+ } else {
+ return lineSpacingExtra;
+ }
+ }
+
+ /**
+ * Overridden because getLineSpacingMultiplier and getLineSpacingExtra are only defined in JB+.
+ */
+ @Override
+ public final void setLineSpacing(float add, float mult) {
+ super.setLineSpacing(add, mult);
+ lineSpacingMultiplier = mult;
+ lineSpacingExtra = add;
+ }
+
+ /**
+ * Although this overrides the setTextSize method from the TextView base class, it changes the
+ * semantics a bit: Calling setTextSize now specifies the maximum text size to be used by this
+ * view. If the text can't fit with that text size, the text size will be scaled down, up to the
+ * minimum text size specified in {@link #setMinTextSize}.
+ *
+ * <p>Note that the final size unit will be truncated to the nearest integer value of the
+ * specified unit.
+ */
+ @Override
+ public final void setTextSize(int unit, float size) {
+ float maxTextSize = TypedValue.applyDimension(unit, size, displayMetrics);
+ if (this.maxTextSize != maxTextSize) {
+ this.maxTextSize = maxTextSize;
+ // TODO: It's not actually necessary to clear the whole cache here. To optimize cache
+ // deletion we'd have to delete all entries in the cache with a value equal or larger than
+ // MIN(old_max_size, new_max_size) when changing maxTextSize; and all entries with a value
+ // equal or smaller than MAX(old_min_size, new_min_size) when changing minTextSize.
+ textSizesCache.clear();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the lower text size limit and invalidate the view.
+ *
+ * <p>The parameters follow the same behavior as they do in {@link #setTextSize}.
+ *
+ * <p>Note that the final size unit will be truncated to the nearest integer value of the
+ * specified unit.
+ */
+ public final void setMinTextSize(int unit, float size) {
+ float minTextSize = TypedValue.applyDimension(unit, size, displayMetrics);
+ if (this.minTextSize != minTextSize) {
+ this.minTextSize = minTextSize;
+ textSizesCache.clear();
+ requestLayout();
+ }
+ }
+
+ /**
+ * Sets the unit to use as step units when computing the resized font size. This view's text
+ * contents will always be rendered as a whole integer value in the unit specified here. For
+ * example, if the unit is {@link TypedValue#COMPLEX_UNIT_SP}, then the text size may end up
+ * being 13sp or 14sp, but never 13.5sp.
+ *
+ * <p>By default, the AutoResizeTextView uses the unit {@link TypedValue#COMPLEX_UNIT_PX}.
+ *
+ * @param unit the unit type to use; must be a known unit type from {@link TypedValue}.
+ */
+ public final void setResizeStepUnit(int unit) {
+ if (resizeStepUnit != unit) {
+ resizeStepUnit = unit;
+ requestLayout();
+ }
+ }
+
+ private void readAttrs(TypedArray typedArray) {
+ resizeStepUnit = typedArray.getInt(
+ R.styleable.AutoResizeTextView_autoResizeText_resizeStepUnit, DEFAULT_RESIZE_STEP_UNIT);
+ minTextSize = (int) typedArray.getDimension(
+ R.styleable.AutoResizeTextView_autoResizeText_minTextSize, DEFAULT_MIN_TEXT_SIZE);
+ maxTextSize = (int) getTextSize();
+ }
+
+ private void adjustTextSize() {
+ int maxWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
+ int maxHeight = getMeasuredHeight() - getPaddingBottom() - getPaddingTop();
+
+ if (maxWidth <= 0 || maxHeight <= 0) {
+ return;
+ }
+
+ this.maxWidth = maxWidth;
+ availableSpaceRect.right = maxWidth;
+ availableSpaceRect.bottom = maxHeight;
+ int minSizeInStepSizeUnits = (int) Math.ceil(convertToResizeStepUnits(minTextSize));
+ int maxSizeInStepSizeUnits = (int) Math.floor(convertToResizeStepUnits(maxTextSize));
+ float textSize = computeTextSize(
+ minSizeInStepSizeUnits, maxSizeInStepSizeUnits, availableSpaceRect);
+ super.setTextSize(resizeStepUnit, textSize);
+ }
+
+ private boolean suggestedSizeFitsInSpace(float suggestedSizeInPx, RectF availableSpace) {
+ textPaint.setTextSize(suggestedSizeInPx);
+ String text = getText().toString();
+ int maxLines = getMaxLines();
+ if (maxLines == 1) {
+ // If single line, check the line's height and width.
+ return textPaint.getFontSpacing() <= availableSpace.bottom
+ && textPaint.measureText(text) <= availableSpace.right;
+ } else {
+ // If multiline, lay the text out, then check the number of lines, the layout's height,
+ // and each line's width.
+ StaticLayout layout = new StaticLayout(text,
+ textPaint,
+ maxWidth,
+ Alignment.ALIGN_NORMAL,
+ getLineSpacingMultiplier(),
+ getLineSpacingExtra(),
+ true);
+
+ // Return false if we need more than maxLines. The text is obviously too big in this case.
+ if (maxLines != NO_LINE_LIMIT && layout.getLineCount() > maxLines) {
+ return false;
+ }
+ // Return false if the height of the layout is too big.
+ return layout.getHeight() <= availableSpace.bottom;
+ }
+ }
+
+ /**
+ * Computes the final text size to use for this text view, factoring in any previously
+ * cached computations.
+ *
+ * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit}
+ * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit}
+ */
+ private float computeTextSize(int minSize, int maxSize, RectF availableSpace) {
+ CharSequence text = getText();
+ if (text != null && textSizesCache.get(text.hashCode()) != 0) {
+ return textSizesCache.get(text.hashCode());
+ }
+ int size = binarySearchSizes(minSize, maxSize, availableSpace);
+ textSizesCache.put(text == null ? 0 : text.hashCode(), size);
+ return size;
+ }
+
+ /**
+ * Performs a binary search to find the largest font size that will still fit within the size
+ * available to this view.
+ * @param minSize the minimum text size to allow, in units of {@link #resizeStepUnit}
+ * @param maxSize the maximum text size to allow, in units of {@link #resizeStepUnit}
+ */
+ private int binarySearchSizes(int minSize, int maxSize, RectF availableSpace) {
+ int bestSize = minSize;
+ int low = minSize + 1;
+ int high = maxSize;
+ int sizeToTry;
+ while (low <= high) {
+ sizeToTry = (low + high) / 2;
+ float dimension = TypedValue.applyDimension(resizeStepUnit, sizeToTry, displayMetrics);
+ if (suggestedSizeFitsInSpace(dimension, availableSpace)) {
+ bestSize = low;
+ low = sizeToTry + 1;
+ } else {
+ high = sizeToTry - 1;
+ bestSize = high;
+ }
+ }
+ return bestSize;
+ }
+
+ private float convertToResizeStepUnits(float dimension) {
+ // To figure out the multiplier between a raw dimension and the resizeStepUnit, we invert the
+ // conversion of 1 resizeStepUnit to a raw dimension.
+ float multiplier = 1 / TypedValue.applyDimension(resizeStepUnit, 1, displayMetrics);
+ return dimension * multiplier;
+ }
+
+ @Override
+ protected final void onTextChanged(
+ final CharSequence text, final int start, final int before, final int after) {
+ super.onTextChanged(text, start, before, after);
+ adjustTextSize();
+ }
+
+ @Override
+ protected final void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+ if (width != oldWidth || height != oldHeight) {
+ textSizesCache.clear();
+ adjustTextSize();
+ }
+ }
+
+ @Override
+ protected final void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ adjustTextSize();
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+}
diff --git a/java/com/android/incallui/autoresizetext/res/values/attrs.xml b/java/com/android/incallui/autoresizetext/res/values/attrs.xml
new file mode 100644
index 000000000..e62feb9c8
--- /dev/null
+++ b/java/com/android/incallui/autoresizetext/res/values/attrs.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<resources>
+ <declare-styleable name="AutoResizeTextView">
+ <!--
+ The unit to use when computing step increments for the resize operation. That is, the
+ resized text will be guaranteed to be a whole number (integer) value in the unit
+ specified. For example, if the unit is scaled pixels (sp), then the font size might be
+ 13sp or 14sp, but not 13.5sp.
+
+ The enum values must match the values from android.util.TypedValue.
+ -->
+ <attr name="autoResizeText_resizeStepUnit" format="enum">
+ <!-- Must match TypedValue.COMPLEX_UNIT_PX. -->
+ <enum name="unitPx" value="0" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_DIP. -->
+ <enum name="unitDip" value="1" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_SP. -->
+ <enum name="unitSp" value="2" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_PT. -->
+ <enum name="unitPt" value="3" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_IN. -->
+ <enum name="unitIn" value="4" />
+ <!-- Must match TypedValue.COMPLEX_UNIT_MM. -->
+ <enum name="unitMm" value="5" />
+ </attr>
+ <!--
+ The minimum text size to use in this view. Text size will be scale down to fit the text
+ in this view, but no smaller than the minimum size specified in this attribute.
+ -->
+ <attr name="autoResizeText_minTextSize" format="dimension" />
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/baseui/BaseFragment.java b/java/com/android/incallui/baseui/BaseFragment.java
new file mode 100644
index 000000000..58b8c6f8d
--- /dev/null
+++ b/java/com/android/incallui/baseui/BaseFragment.java
@@ -0,0 +1,75 @@
+/*
+ * 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.incallui.baseui;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+/** Parent for all fragments that use Presenters and Ui design. */
+public abstract class BaseFragment<T extends Presenter<U>, U extends Ui> extends Fragment {
+
+ private static final String KEY_FRAGMENT_HIDDEN = "key_fragment_hidden";
+
+ private T mPresenter;
+
+ protected BaseFragment() {
+ mPresenter = createPresenter();
+ }
+
+ public abstract T createPresenter();
+
+ public abstract U getUi();
+
+ /**
+ * Presenter will be available after onActivityCreated().
+ *
+ * @return The presenter associated with this fragment.
+ */
+ public T getPresenter() {
+ return mPresenter;
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ mPresenter.onUiReady(getUi());
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mPresenter.onRestoreInstanceState(savedInstanceState);
+ if (savedInstanceState.getBoolean(KEY_FRAGMENT_HIDDEN)) {
+ getFragmentManager().beginTransaction().hide(this).commit();
+ }
+ }
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mPresenter.onUiDestroy(getUi());
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mPresenter.onSaveInstanceState(outState);
+ outState.putBoolean(KEY_FRAGMENT_HIDDEN, isHidden());
+ }
+}
diff --git a/java/com/android/incallui/baseui/Presenter.java b/java/com/android/incallui/baseui/Presenter.java
new file mode 100644
index 000000000..581ad47c7
--- /dev/null
+++ b/java/com/android/incallui/baseui/Presenter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.incallui.baseui;
+
+import android.os.Bundle;
+
+/** Base class for Presenters. */
+public abstract class Presenter<U extends Ui> {
+
+ private U mUi;
+
+ /**
+ * Called after the UI view has been created. That is when fragment.onViewCreated() is called.
+ *
+ * @param ui The Ui implementation that is now ready to be used.
+ */
+ public void onUiReady(U ui) {
+ mUi = ui;
+ }
+
+ /** Called when the UI view is destroyed in Fragment.onDestroyView(). */
+ public final void onUiDestroy(U ui) {
+ onUiUnready(ui);
+ mUi = null;
+ }
+
+ /**
+ * To be overriden by Presenter implementations. Called when the fragment is being destroyed but
+ * before ui is set to null.
+ */
+ public void onUiUnready(U ui) {}
+
+ public void onSaveInstanceState(Bundle outState) {}
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {}
+
+ public U getUi() {
+ return mUi;
+ }
+}
diff --git a/java/com/android/incallui/baseui/Ui.java b/java/com/android/incallui/baseui/Ui.java
new file mode 100644
index 000000000..439e41550
--- /dev/null
+++ b/java/com/android/incallui/baseui/Ui.java
@@ -0,0 +1,20 @@
+/*
+ * 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.incallui.baseui;
+
+/** Base class for all presenter ui. */
+public interface Ui {}
diff --git a/java/com/android/incallui/bindings/ContactUtils.java b/java/com/android/incallui/bindings/ContactUtils.java
new file mode 100644
index 000000000..d2d365d81
--- /dev/null
+++ b/java/com/android/incallui/bindings/ContactUtils.java
@@ -0,0 +1,33 @@
+/*
+ * 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.incallui.bindings;
+
+import android.location.Address;
+import android.util.Pair;
+import java.util.Calendar;
+import java.util.List;
+
+/** Utility functions to help manipulate contact data. */
+public interface ContactUtils {
+
+ boolean retrieveContactInteractionsFromLookupKey(String lookupKey, Listener listener);
+
+ interface Listener {
+
+ void onContactInteractionsFound(Address address, List<Pair<Calendar, Calendar>> openingHours);
+ }
+}
diff --git a/java/com/android/incallui/bindings/DistanceHelper.java b/java/com/android/incallui/bindings/DistanceHelper.java
new file mode 100644
index 000000000..6b2200dca
--- /dev/null
+++ b/java/com/android/incallui/bindings/DistanceHelper.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.incallui.bindings;
+
+import android.location.Address;
+
+/** Superclass for a helper class to get the current location and distance to other locations. */
+public interface DistanceHelper {
+
+ float DISTANCE_NOT_FOUND = -1;
+ float MILES_PER_METER = (float) 0.000621371192;
+ float KILOMETERS_PER_METER = (float) 0.001;
+
+ void cleanUp();
+
+ float calculateDistance(Address address);
+
+ interface Listener {
+
+ void onLocationReady();
+ }
+}
diff --git a/java/com/android/incallui/bindings/InCallUiBindings.java b/java/com/android/incallui/bindings/InCallUiBindings.java
new file mode 100644
index 000000000..d3d3a8b37
--- /dev/null
+++ b/java/com/android/incallui/bindings/InCallUiBindings.java
@@ -0,0 +1,48 @@
+/*
+ * 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.incallui.bindings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.ConfigProvider;
+
+/** This interface allows the container application to customize the in call UI. */
+public interface InCallUiBindings {
+
+ @Nullable
+ PhoneNumberService newPhoneNumberService(Context context);
+
+ /** @return An {@link Intent} to be broadcast when the InCallUI is visible. */
+ @Nullable
+ Intent getUiReadyBroadcastIntent(Context context);
+
+ /**
+ * @return An {@link Intent} to be broadcast when the call state button in the InCallUI is touched
+ * while in a call.
+ */
+ @Nullable
+ Intent getCallStateButtonBroadcastIntent(Context context);
+
+ @Nullable
+ DistanceHelper newDistanceHelper(Context context, DistanceHelper.Listener listener);
+
+ @Nullable
+ ContactUtils getContactUtilsInstance(Context context);
+
+ ConfigProvider getConfigProvider();
+}
diff --git a/java/com/android/incallui/bindings/InCallUiBindingsFactory.java b/java/com/android/incallui/bindings/InCallUiBindingsFactory.java
new file mode 100644
index 000000000..57c186d90
--- /dev/null
+++ b/java/com/android/incallui/bindings/InCallUiBindingsFactory.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.incallui.bindings;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the in call UI
+ * module to get references to the InCallUiBindings.
+ */
+public interface InCallUiBindingsFactory {
+
+ InCallUiBindings newInCallUiBindings();
+}
diff --git a/java/com/android/incallui/bindings/InCallUiBindingsStub.java b/java/com/android/incallui/bindings/InCallUiBindingsStub.java
new file mode 100644
index 000000000..7b42fb375
--- /dev/null
+++ b/java/com/android/incallui/bindings/InCallUiBindingsStub.java
@@ -0,0 +1,81 @@
+/*
+ * 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.incallui.bindings;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.ConfigProvider;
+
+/** Default implementation for InCallUi bindings. */
+public class InCallUiBindingsStub implements InCallUiBindings {
+ private ConfigProvider configProvider;
+
+ @Override
+ @Nullable
+ public PhoneNumberService newPhoneNumberService(Context context) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Intent getUiReadyBroadcastIntent(Context context) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public Intent getCallStateButtonBroadcastIntent(Context context) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public DistanceHelper newDistanceHelper(Context context, DistanceHelper.Listener listener) {
+ return null;
+ }
+
+ @Override
+ @Nullable
+ public ContactUtils getContactUtilsInstance(Context context) {
+ return null;
+ }
+
+ @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/incallui/bindings/PhoneNumberService.java b/java/com/android/incallui/bindings/PhoneNumberService.java
new file mode 100644
index 000000000..bd2741a1d
--- /dev/null
+++ b/java/com/android/incallui/bindings/PhoneNumberService.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.incallui.bindings;
+
+import android.graphics.Bitmap;
+
+/** Provides phone number lookup services. */
+public interface PhoneNumberService {
+
+ /**
+ * Get a phone number number asynchronously.
+ *
+ * @param phoneNumber The phone number to lookup.
+ * @param listener The listener to notify when the phone number lookup is complete.
+ * @param imageListener The listener to notify when the image lookup is complete.
+ */
+ void getPhoneNumberInfo(
+ String phoneNumber,
+ NumberLookupListener listener,
+ ImageLookupListener imageListener,
+ boolean isIncoming);
+
+ interface NumberLookupListener {
+
+ /**
+ * Callback when a phone number has been looked up.
+ *
+ * @param info The looked up information. Or (@literal null} if there are no results.
+ */
+ void onPhoneNumberInfoComplete(PhoneNumberInfo info);
+ }
+
+ interface ImageLookupListener {
+
+ /**
+ * Callback when a image has been fetched.
+ *
+ * @param bitmap The fetched image.
+ */
+ void onImageFetchComplete(Bitmap bitmap);
+ }
+
+ interface PhoneNumberInfo {
+
+ String getDisplayName();
+
+ String getNumber();
+
+ int getPhoneType();
+
+ String getPhoneLabel();
+
+ String getNormalizedNumber();
+
+ String getImageUrl();
+
+ String getLookupKey();
+
+ boolean isBusiness();
+
+ int getLookupSource();
+ }
+}
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
new file mode 100644
index 000000000..862c71cf9
--- /dev/null
+++ b/java/com/android/incallui/call/CallList.java
@@ -0,0 +1,763 @@
+/*
+ * 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.incallui.call;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Trace;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.os.BuildCompat;
+import android.telecom.Call;
+import android.telecom.DisconnectCause;
+import android.telecom.PhoneAccount;
+import android.util.ArrayMap;
+import com.android.contacts.common.GeoUtil;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.shortcuts.ShortcutUsageReporter;
+import com.android.dialer.spam.Spam;
+import com.android.dialer.spam.SpamBindings;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Maintains the list of active calls and notifies interested classes of changes to the call list as
+ * they are received from the telephony stack. Primary listener of changes to this class is
+ * InCallPresenter.
+ */
+public class CallList implements DialerCallDelegate {
+
+ private static final int DISCONNECTED_CALL_SHORT_TIMEOUT_MS = 200;
+ private static final int DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS = 2000;
+ private static final int DISCONNECTED_CALL_LONG_TIMEOUT_MS = 5000;
+
+ private static final int EVENT_DISCONNECTED_TIMEOUT = 1;
+
+ private static CallList sInstance = new CallList();
+
+ private final Map<String, DialerCall> mCallById = new ArrayMap<>();
+ private final Map<android.telecom.Call, DialerCall> mCallByTelecomCall = new ArrayMap<>();
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<Listener> mListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
+
+ private final Set<DialerCall> mPendingDisconnectCalls =
+ Collections.newSetFromMap(new ConcurrentHashMap<DialerCall, Boolean>(8, 0.9f, 1));
+ /** Handles the timeout for destroying disconnected calls. */
+ private final Handler mHandler =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_DISCONNECTED_TIMEOUT:
+ LogUtil.d("CallList.handleMessage", "EVENT_DISCONNECTED_TIMEOUT ", msg.obj);
+ finishDisconnectedCall((DialerCall) msg.obj);
+ break;
+ default:
+ LogUtil.e("CallList.handleMessage", "Message not expected: " + msg.what);
+ break;
+ }
+ }
+ };
+
+ /**
+ * USED ONLY FOR TESTING Testing-only constructor. Instance should only be acquired through
+ * getInstance().
+ */
+ @VisibleForTesting
+ public CallList() {}
+
+ /** Static singleton accessor method. */
+ public static CallList getInstance() {
+ return sInstance;
+ }
+
+ public void onCallAdded(
+ final Context context, final android.telecom.Call telecomCall, LatencyReport latencyReport) {
+ Trace.beginSection("onCallAdded");
+ final DialerCall call =
+ new DialerCall(context, this, telecomCall, latencyReport, true /* registerCallback */);
+ final DialerCallListenerImpl dialerCallListener = new DialerCallListenerImpl(call);
+ call.addListener(dialerCallListener);
+ LogUtil.d("CallList.onCallAdded", "callState=" + call.getState());
+ if (Spam.get(context).isSpamEnabled()) {
+ String number = TelecomCallUtil.getNumber(telecomCall);
+ Spam.get(context)
+ .checkSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isSpam) {
+ if (isSpam) {
+ if (call.getState() != DialerCall.State.INCOMING
+ && call.getState() != DialerCall.State.CALL_WAITING) {
+ LogUtil.i(
+ "CallList.onCallAdded",
+ "marking spam call as not spam because it's not an incoming call");
+ isSpam = false;
+ } else if (isPotentialEmergencyCallback(context, call)) {
+ LogUtil.i(
+ "CallList.onCallAdded",
+ "marking spam call as not spam because an emergency call was made on this"
+ + " device recently");
+ isSpam = false;
+ }
+ }
+
+ Logger.get(context)
+ .logCallImpression(
+ isSpam
+ ? DialerImpression.Type.INCOMING_SPAM_CALL
+ : DialerImpression.Type.INCOMING_NON_SPAM_CALL,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ call.setSpam(isSpam);
+ dialerCallListener.onDialerCallUpdate();
+ }
+ });
+
+ updateUserMarkedSpamStatus(call, context, number, dialerCallListener);
+ }
+
+ FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler =
+ new FilteredNumberAsyncQueryHandler(context);
+
+ filteredNumberAsyncQueryHandler.isBlockedNumber(
+ new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
+ @Override
+ public void onCheckComplete(Integer id) {
+ if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) {
+ call.setBlockedStatus(true);
+ dialerCallListener.onDialerCallUpdate();
+ }
+ }
+ },
+ call.getNumber(),
+ GeoUtil.getCurrentCountryIso(context));
+
+ if (call.getState() == DialerCall.State.INCOMING
+ || call.getState() == DialerCall.State.CALL_WAITING) {
+ onIncoming(call);
+ } else {
+ dialerCallListener.onDialerCallUpdate();
+ }
+
+ if (call.getState() != State.INCOMING) {
+ // Only report outgoing calls
+ ShortcutUsageReporter.onOutgoingCallAdded(context, call.getNumber());
+ }
+
+ Trace.endSection();
+ }
+
+ private static boolean isPotentialEmergencyCallback(Context context, DialerCall call) {
+ if (BuildCompat.isAtLeastO()) {
+ return call.isPotentialEmergencyCallback();
+ } else {
+ long timestampMillis = FilteredNumbersUtil.getLastEmergencyCallTimeMillis(context);
+ return call.isInEmergencyCallbackWindow(timestampMillis);
+ }
+ }
+
+ @Override
+ public DialerCall getDialerCallFromTelecomCall(Call telecomCall) {
+ return mCallByTelecomCall.get(telecomCall);
+ }
+
+ public void updateUserMarkedSpamStatus(
+ final DialerCall call,
+ final Context context,
+ String number,
+ final DialerCallListenerImpl dialerCallListener) {
+
+ Spam.get(context)
+ .checkUserMarkedNonSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInUserWhiteList) {
+ call.setIsInUserWhiteList(isInUserWhiteList);
+ }
+ });
+
+ Spam.get(context)
+ .checkGlobalSpamListStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInGlobalSpamList) {
+ call.setIsInGlobalSpamList(isInGlobalSpamList);
+ }
+ });
+
+ Spam.get(context)
+ .checkUserMarkedSpamStatus(
+ number,
+ null,
+ new SpamBindings.Listener() {
+ @Override
+ public void onComplete(boolean isInUserSpamList) {
+ call.setIsInUserSpamList(isInUserSpamList);
+ }
+ });
+ }
+
+ public void onCallRemoved(Context context, android.telecom.Call telecomCall) {
+ if (mCallByTelecomCall.containsKey(telecomCall)) {
+ DialerCall call = mCallByTelecomCall.get(telecomCall);
+ Assert.checkArgument(!call.isExternalCall());
+
+ // Don't log an already logged call. logCall() might be called multiple times
+ // for the same call due to b/24109437.
+ if (call.getLogState() != null && !call.getLogState().isLogged) {
+ getLegacyBindings(context).logCall(call);
+ call.getLogState().isLogged = true;
+ }
+
+ if (updateCallInMap(call)) {
+ LogUtil.w(
+ "CallList.onCallRemoved", "Removing call not previously disconnected " + call.getId());
+ }
+ }
+ }
+
+ InCallUiLegacyBindings getLegacyBindings(Context context) {
+ Objects.requireNonNull(context);
+
+ Context application = context.getApplicationContext();
+ InCallUiLegacyBindings legacyInstance = null;
+ if (application instanceof InCallUiLegacyBindingsFactory) {
+ legacyInstance = ((InCallUiLegacyBindingsFactory) application).newInCallUiLegacyBindings();
+ }
+
+ if (legacyInstance == null) {
+ legacyInstance = new InCallUiLegacyBindingsStub();
+ }
+ return legacyInstance;
+ }
+
+ /**
+ * Handles the case where an internal call has become an exteral call. We need to
+ *
+ * @param context
+ * @param telecomCall
+ */
+ public void onInternalCallMadeExternal(Context context, android.telecom.Call telecomCall) {
+
+ if (mCallByTelecomCall.containsKey(telecomCall)) {
+ DialerCall call = mCallByTelecomCall.get(telecomCall);
+
+ // Don't log an already logged call. logCall() might be called multiple times
+ // for the same call due to b/24109437.
+ if (call.getLogState() != null && !call.getLogState().isLogged) {
+ getLegacyBindings(context).logCall(call);
+ call.getLogState().isLogged = true;
+ }
+
+ // When removing a call from the call list because it became an external call, we need to
+ // ensure the callback is unregistered -- this is normally only done when calls disconnect.
+ // However, the call won't be disconnected in this case. Also, logic in updateCallInMap
+ // would just re-add the call anyways.
+ call.unregisterCallback();
+ mCallById.remove(call.getId());
+ mCallByTelecomCall.remove(telecomCall);
+ }
+ }
+
+ /** Called when a single call has changed. */
+ private void onIncoming(DialerCall call) {
+ if (updateCallInMap(call)) {
+ LogUtil.i("CallList.onIncoming", String.valueOf(call));
+ }
+
+ for (Listener listener : mListeners) {
+ listener.onIncomingCall(call);
+ }
+ }
+
+ public void addListener(@NonNull Listener listener) {
+ Objects.requireNonNull(listener);
+
+ mListeners.add(listener);
+
+ // Let the listener know about the active calls immediately.
+ listener.onCallListChange(this);
+ }
+
+ public void removeListener(@Nullable Listener listener) {
+ if (listener != null) {
+ mListeners.remove(listener);
+ }
+ }
+
+ /**
+ * TODO: Change so that this function is not needed. Instead of assuming there is an active call,
+ * the code should rely on the status of a specific DialerCall and allow the presenters to update
+ * the DialerCall object when the active call changes.
+ */
+ public DialerCall getIncomingOrActive() {
+ DialerCall retval = getIncomingCall();
+ if (retval == null) {
+ retval = getActiveCall();
+ }
+ return retval;
+ }
+
+ public DialerCall getOutgoingOrActive() {
+ DialerCall retval = getOutgoingCall();
+ if (retval == null) {
+ retval = getActiveCall();
+ }
+ return retval;
+ }
+
+ /** A call that is waiting for {@link PhoneAccount} selection */
+ public DialerCall getWaitingForAccountCall() {
+ return getFirstCallWithState(DialerCall.State.SELECT_PHONE_ACCOUNT);
+ }
+
+ public DialerCall getPendingOutgoingCall() {
+ return getFirstCallWithState(DialerCall.State.CONNECTING);
+ }
+
+ public DialerCall getOutgoingCall() {
+ DialerCall call = getFirstCallWithState(DialerCall.State.DIALING);
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.REDIALING);
+ }
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.PULLING);
+ }
+ return call;
+ }
+
+ public DialerCall getActiveCall() {
+ return getFirstCallWithState(DialerCall.State.ACTIVE);
+ }
+
+ public DialerCall getSecondActiveCall() {
+ return getCallWithState(DialerCall.State.ACTIVE, 1);
+ }
+
+ public DialerCall getBackgroundCall() {
+ return getFirstCallWithState(DialerCall.State.ONHOLD);
+ }
+
+ public DialerCall getDisconnectedCall() {
+ return getFirstCallWithState(DialerCall.State.DISCONNECTED);
+ }
+
+ public DialerCall getDisconnectingCall() {
+ return getFirstCallWithState(DialerCall.State.DISCONNECTING);
+ }
+
+ public DialerCall getSecondBackgroundCall() {
+ return getCallWithState(DialerCall.State.ONHOLD, 1);
+ }
+
+ public DialerCall getActiveOrBackgroundCall() {
+ DialerCall call = getActiveCall();
+ if (call == null) {
+ call = getBackgroundCall();
+ }
+ return call;
+ }
+
+ public DialerCall getIncomingCall() {
+ DialerCall call = getFirstCallWithState(DialerCall.State.INCOMING);
+ if (call == null) {
+ call = getFirstCallWithState(DialerCall.State.CALL_WAITING);
+ }
+
+ return call;
+ }
+
+ public DialerCall getFirstCall() {
+ DialerCall result = getIncomingCall();
+ if (result == null) {
+ result = getPendingOutgoingCall();
+ }
+ if (result == null) {
+ result = getOutgoingCall();
+ }
+ if (result == null) {
+ result = getFirstCallWithState(DialerCall.State.ACTIVE);
+ }
+ if (result == null) {
+ result = getDisconnectingCall();
+ }
+ if (result == null) {
+ result = getDisconnectedCall();
+ }
+ return result;
+ }
+
+ public boolean hasLiveCall() {
+ DialerCall call = getFirstCall();
+ return call != null && call != getDisconnectingCall() && call != getDisconnectedCall();
+ }
+
+ /**
+ * Returns the first call found in the call map with the upgrade to video modification state.
+ *
+ * @return The first call with the upgrade to video state.
+ */
+ public DialerCall getVideoUpgradeRequestCall() {
+ for (DialerCall call : mCallById.values()) {
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ return call;
+ }
+ }
+ return null;
+ }
+
+ public DialerCall getCallById(String callId) {
+ return mCallById.get(callId);
+ }
+
+ /** Returns first call found in the call map with the specified state. */
+ public DialerCall getFirstCallWithState(int state) {
+ return getCallWithState(state, 0);
+ }
+
+ /**
+ * Returns the [position]th call found in the call map with the specified state. TODO: Improve
+ * this logic to sort by call time.
+ */
+ public DialerCall getCallWithState(int state, int positionToFind) {
+ DialerCall retval = null;
+ int position = 0;
+ for (DialerCall call : mCallById.values()) {
+ if (call.getState() == state) {
+ if (position >= positionToFind) {
+ retval = call;
+ break;
+ } else {
+ position++;
+ }
+ }
+ }
+
+ return retval;
+ }
+
+ /**
+ * This is called when the service disconnects, either expectedly or unexpectedly. For the
+ * expected case, it's because we have no calls left. For the unexpected case, it is likely a
+ * crash of phone and we need to clean up our calls manually. Without phone, there can be no
+ * active calls, so this is relatively safe thing to do.
+ */
+ public void clearOnDisconnect() {
+ for (DialerCall call : mCallById.values()) {
+ final int state = call.getState();
+ if (state != DialerCall.State.IDLE
+ && state != DialerCall.State.INVALID
+ && state != DialerCall.State.DISCONNECTED) {
+
+ call.setState(DialerCall.State.DISCONNECTED);
+ call.setDisconnectCause(new DisconnectCause(DisconnectCause.UNKNOWN));
+ updateCallInMap(call);
+ }
+ }
+ notifyGenericListeners();
+ }
+
+ /**
+ * Called when the user has dismissed an error dialog. This indicates acknowledgement of the
+ * disconnect cause, and that any pending disconnects should immediately occur.
+ */
+ public void onErrorDialogDismissed() {
+ final Iterator<DialerCall> iterator = mPendingDisconnectCalls.iterator();
+ while (iterator.hasNext()) {
+ DialerCall call = iterator.next();
+ iterator.remove();
+ finishDisconnectedCall(call);
+ }
+ }
+
+ /**
+ * Processes an update for a single call.
+ *
+ * @param call The call to update.
+ */
+ private void onUpdateCall(DialerCall call) {
+ LogUtil.d("CallList.onUpdateCall", String.valueOf(call));
+ if (!mCallById.containsKey(call.getId()) && call.isExternalCall()) {
+ // When a regular call becomes external, it is removed from the call list, and there may be
+ // pending updates to Telecom which are queued up on the Telecom call's handler which we no
+ // longer wish to cause updates to the call in the CallList. Bail here if the list of tracked
+ // calls doesn't contain the call which received the update.
+ return;
+ }
+
+ if (updateCallInMap(call)) {
+ LogUtil.i("CallList.onUpdateCall", String.valueOf(call));
+ }
+ }
+
+ /**
+ * Sends a generic notification to all listeners that something has changed. It is up to the
+ * listeners to call back to determine what changed.
+ */
+ private void notifyGenericListeners() {
+ for (Listener listener : mListeners) {
+ listener.onCallListChange(this);
+ }
+ }
+
+ private void notifyListenersOfDisconnect(DialerCall call) {
+ for (Listener listener : mListeners) {
+ listener.onDisconnect(call);
+ }
+ }
+
+ /**
+ * Updates the call entry in the local map.
+ *
+ * @return false if no call previously existed and no call was added, otherwise true.
+ */
+ private boolean updateCallInMap(DialerCall call) {
+ Objects.requireNonNull(call);
+
+ boolean updated = false;
+
+ if (call.getState() == DialerCall.State.DISCONNECTED) {
+ // update existing (but do not add!!) disconnected calls
+ if (mCallById.containsKey(call.getId())) {
+ // For disconnected calls, we want to keep them alive for a few seconds so that the
+ // UI has a chance to display anything it needs when a call is disconnected.
+
+ // Set up a timer to destroy the call after X seconds.
+ final Message msg = mHandler.obtainMessage(EVENT_DISCONNECTED_TIMEOUT, call);
+ mHandler.sendMessageDelayed(msg, getDelayForDisconnect(call));
+ mPendingDisconnectCalls.add(call);
+
+ mCallById.put(call.getId(), call);
+ mCallByTelecomCall.put(call.getTelecomCall(), call);
+ updated = true;
+ }
+ } else if (!isCallDead(call)) {
+ mCallById.put(call.getId(), call);
+ mCallByTelecomCall.put(call.getTelecomCall(), call);
+ updated = true;
+ } else if (mCallById.containsKey(call.getId())) {
+ mCallById.remove(call.getId());
+ mCallByTelecomCall.remove(call.getTelecomCall());
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private int getDelayForDisconnect(DialerCall call) {
+ if (call.getState() != DialerCall.State.DISCONNECTED) {
+ throw new IllegalStateException();
+ }
+
+ final int cause = call.getDisconnectCause().getCode();
+ final int delay;
+ switch (cause) {
+ case DisconnectCause.LOCAL:
+ delay = DISCONNECTED_CALL_SHORT_TIMEOUT_MS;
+ break;
+ case DisconnectCause.REMOTE:
+ case DisconnectCause.ERROR:
+ delay = DISCONNECTED_CALL_MEDIUM_TIMEOUT_MS;
+ break;
+ case DisconnectCause.REJECTED:
+ case DisconnectCause.MISSED:
+ case DisconnectCause.CANCELED:
+ // no delay for missed/rejected incoming calls and canceled outgoing calls.
+ delay = 0;
+ break;
+ default:
+ delay = DISCONNECTED_CALL_LONG_TIMEOUT_MS;
+ break;
+ }
+
+ return delay;
+ }
+
+ private boolean isCallDead(DialerCall call) {
+ final int state = call.getState();
+ return DialerCall.State.IDLE == state || DialerCall.State.INVALID == state;
+ }
+
+ /** Sets up a call for deletion and notifies listeners of change. */
+ private void finishDisconnectedCall(DialerCall call) {
+ if (mPendingDisconnectCalls.contains(call)) {
+ mPendingDisconnectCalls.remove(call);
+ }
+ call.setState(DialerCall.State.IDLE);
+ updateCallInMap(call);
+ notifyGenericListeners();
+ }
+
+ /**
+ * Notifies all video calls of a change in device orientation.
+ *
+ * @param rotation The new rotation angle (in degrees).
+ */
+ public void notifyCallsOfDeviceRotation(int rotation) {
+ for (DialerCall call : mCallById.values()) {
+ // First, ensure that the call videoState has video enabled (there is no need to set
+ // device orientation on a voice call which has not yet been upgraded to video).
+ // Second, ensure a VideoCall is set on the call so that the change can be sent to the
+ // provider (a VideoCall can be present for a call that does not currently have video,
+ // but can be upgraded to video).
+
+ // NOTE: is it necessary to use this order because getVideoCall references the class
+ // VideoProfile which is not available on APIs <23 (M).
+ if (VideoUtils.isVideoCall(call) && call.getVideoCall() != null) {
+ call.getVideoCall().setDeviceOrientation(rotation);
+ }
+ }
+ }
+
+ public void onInCallUiShown(boolean forFullScreenIntent) {
+ for (DialerCall call : mCallById.values()) {
+ call.getLatencyReport().onInCallUiShown(forFullScreenIntent);
+ }
+ }
+
+ /** Listener interface for any class that wants to be notified of changes to the call list. */
+ public interface Listener {
+
+ /**
+ * Called when a new incoming call comes in. This is the only method that gets called for
+ * incoming calls. Listeners that want to perform an action on incoming call should respond in
+ * this method because {@link #onCallListChange} does not automatically get called for incoming
+ * calls.
+ */
+ void onIncomingCall(DialerCall call);
+
+ /**
+ * Called when a new modify call request comes in This is the only method that gets called for
+ * modify requests.
+ */
+ void onUpgradeToVideo(DialerCall call);
+
+ /** Called when the session modification state of a call changes. */
+ void onSessionModificationStateChange(@SessionModificationState int newState);
+
+ /**
+ * Called anytime there are changes to the call list. The change can be switching call states,
+ * updating information, etc. This method will NOT be called for new incoming calls and for
+ * calls that switch to disconnected state. Listeners must add actions to those method
+ * implementations if they want to deal with those actions.
+ */
+ void onCallListChange(CallList callList);
+
+ /**
+ * Called when a call switches to the disconnected state. This is the only method that will get
+ * called upon disconnection.
+ */
+ void onDisconnect(DialerCall call);
+
+ void onWiFiToLteHandover(DialerCall call);
+
+ /**
+ * Called when a user is in a video call and the call is unable to be handed off successfully to
+ * WiFi
+ */
+ void onHandoverToWifiFailed(DialerCall call);
+ }
+
+ private class DialerCallListenerImpl implements DialerCallListener {
+
+ private final DialerCall mCall;
+
+ DialerCallListenerImpl(DialerCall call) {
+ Assert.isNotNull(call);
+ mCall = call;
+ }
+
+ @Override
+ public void onDialerCallDisconnect() {
+ if (updateCallInMap(mCall)) {
+ LogUtil.i("DialerCallListenerImpl.onDialerCallDisconnect", String.valueOf(mCall));
+ // notify those listening for all disconnects
+ notifyListenersOfDisconnect(mCall);
+ }
+ }
+
+ @Override
+ public void onDialerCallUpdate() {
+ Trace.beginSection("onUpdate");
+ onUpdateCall(mCall);
+ notifyGenericListeners();
+ Trace.endSection();
+ }
+
+ @Override
+ public void onDialerCallChildNumberChange() {}
+
+ @Override
+ public void onDialerCallLastForwardedNumberChange() {}
+
+ @Override
+ public void onDialerCallUpgradeToVideo() {
+ for (Listener listener : mListeners) {
+ listener.onUpgradeToVideo(mCall);
+ }
+ }
+
+ @Override
+ public void onWiFiToLteHandover() {
+ for (Listener listener : mListeners) {
+ listener.onWiFiToLteHandover(mCall);
+ }
+ }
+
+ @Override
+ public void onHandoverToWifiFailure() {
+ for (Listener listener : mListeners) {
+ listener.onHandoverToWifiFailed(mCall);
+ }
+ }
+
+ @Override
+ public void onDialerCallSessionModificationStateChange(@SessionModificationState int state) {
+ for (Listener listener : mListeners) {
+ listener.onSessionModificationStateChange(state);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
new file mode 100644
index 000000000..bd8f006dd
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -0,0 +1,1401 @@
+/*
+ * 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.incallui.call;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Trace;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.telecom.Call;
+import android.telecom.Call.Details;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.GatewayInfo;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.VideoProfile;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
+import com.android.dialer.callintent.CallIntentParser;
+import com.android.dialer.callintent.nano.CallInitiationType;
+import com.android.dialer.callintent.nano.CallSpecificAppData;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.ConfigProviderBindings;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.util.CallUtil;
+import com.android.incallui.latencyreport.LatencyReport;
+import com.android.incallui.util.TelecomCallUtil;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+
+/** Describes a single call and its state. */
+public class DialerCall {
+
+ public static final int CALL_HISTORY_STATUS_UNKNOWN = 0;
+ public static final int CALL_HISTORY_STATUS_PRESENT = 1;
+ public static final int CALL_HISTORY_STATUS_NOT_PRESENT = 2;
+ private static final String ID_PREFIX = "DialerCall_";
+ private static final String CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS =
+ "emergency_callback_window_millis";
+ private static int sIdCounter = 0;
+
+ /**
+ * The unique call ID for every call. This will help us to identify each call and allow us the
+ * ability to stitch impressions to calls if needed.
+ */
+ private final String uniqueCallId = UUID.randomUUID().toString();
+
+ private final Call mTelecomCall;
+ private final LatencyReport mLatencyReport;
+ private final String mId;
+ private final List<String> mChildCallIds = new ArrayList<>();
+ private final VideoSettings mVideoSettings = new VideoSettings();
+ private final LogState mLogState = new LogState();
+ private final Context mContext;
+ private final DialerCallDelegate mDialerCallDelegate;
+ private final List<DialerCallListener> mListeners = new CopyOnWriteArrayList<>();
+ private final List<CannedTextResponsesLoadedListener> mCannedTextResponsesLoadedListeners =
+ new CopyOnWriteArrayList<>();
+
+ private boolean mIsEmergencyCall;
+ private Uri mHandle;
+ private int mState = State.INVALID;
+ private DisconnectCause mDisconnectCause;
+
+ private boolean hasShownWiFiToLteHandoverToast;
+ private boolean doNotShowDialogForHandoffToWifiFailure;
+
+ @SessionModificationState private int mSessionModificationState;
+ private int mVideoState;
+ /** mRequestedVideoState is used to store requested upgrade / downgrade video state */
+ private int mRequestedVideoState = VideoProfile.STATE_AUDIO_ONLY;
+
+ private InCallVideoCallCallback mVideoCallCallback;
+ private boolean mIsVideoCallCallbackRegistered;
+ private String mChildNumber;
+ private String mLastForwardedNumber;
+ private String mCallSubject;
+ private PhoneAccountHandle mPhoneAccountHandle;
+ @CallHistoryStatus private int mCallHistoryStatus = CALL_HISTORY_STATUS_UNKNOWN;
+ private boolean mIsSpam;
+ private boolean mIsBlocked;
+ private boolean isInUserSpamList;
+ private boolean isInUserWhiteList;
+ private boolean isInGlobalSpamList;
+ private boolean didShowCameraPermission;
+ private String callProviderLabel;
+ private String callbackNumber;
+
+ public static String getNumberFromHandle(Uri handle) {
+ return handle == null ? "" : handle.getSchemeSpecificPart();
+ }
+
+ /**
+ * Whether the call is put on hold by remote party. This is different than the {@link
+ * State.ONHOLD} state which indicates that the call is being held locally on the device.
+ */
+ private boolean isRemotelyHeld;
+
+ /**
+ * Indicates whether the phone account associated with this call supports specifying a call
+ * subject.
+ */
+ private boolean mIsCallSubjectSupported;
+
+ private final Call.Callback mTelecomCallCallback =
+ new Call.Callback() {
+ @Override
+ public void onStateChanged(Call call, int newState) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call + " newState=" + newState);
+ update();
+ }
+
+ @Override
+ public void onParentChanged(Call call, Call newParent) {
+ LogUtil.v(
+ "TelecomCallCallback.onParentChanged", "call=" + call + " newParent=" + newParent);
+ update();
+ }
+
+ @Override
+ public void onChildrenChanged(Call call, List<Call> children) {
+ update();
+ }
+
+ @Override
+ public void onDetailsChanged(Call call, Call.Details details) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", " call=" + call + " details=" + details);
+ update();
+ }
+
+ @Override
+ public void onCannedTextResponsesLoaded(Call call, List<String> cannedTextResponses) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged",
+ "call=" + call + " cannedTextResponses=" + cannedTextResponses);
+ for (CannedTextResponsesLoadedListener listener : mCannedTextResponsesLoadedListeners) {
+ listener.onCannedTextResponsesLoaded(DialerCall.this);
+ }
+ }
+
+ @Override
+ public void onPostDialWait(Call call, String remainingPostDialSequence) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged",
+ "call=" + call + " remainingPostDialSequence=" + remainingPostDialSequence);
+ update();
+ }
+
+ @Override
+ public void onVideoCallChanged(Call call, VideoCall videoCall) {
+ LogUtil.v(
+ "TelecomCallCallback.onStateChanged", "call=" + call + " videoCall=" + videoCall);
+ update();
+ }
+
+ @Override
+ public void onCallDestroyed(Call call) {
+ LogUtil.v("TelecomCallCallback.onStateChanged", "call=" + call);
+ call.unregisterCallback(this);
+ }
+
+ @Override
+ public void onConferenceableCallsChanged(Call call, List<Call> conferenceableCalls) {
+ LogUtil.v(
+ "DialerCall.onConferenceableCallsChanged",
+ "call %s, conferenceable calls: %d",
+ call,
+ conferenceableCalls.size());
+ update();
+ }
+
+ @Override
+ public void onConnectionEvent(android.telecom.Call call, String event, Bundle extras) {
+ LogUtil.v(
+ "DialerCall.onConnectionEvent",
+ "Call: " + call + ", Event: " + event + ", Extras: " + extras);
+ switch (event) {
+ // The Previous attempt to Merge two calls together has failed in Telecom. We must
+ // now update the UI to possibly re-enable the Merge button based on the number of
+ // currently conferenceable calls available or Connection Capabilities.
+ case android.telecom.Connection.EVENT_CALL_MERGE_FAILED:
+ update();
+ break;
+ case TelephonyManagerCompat.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE:
+ notifyWiFiToLteHandover();
+ break;
+ case TelephonyManagerCompat.EVENT_HANDOVER_TO_WIFI_FAILED:
+ notifyHandoverToWifiFailed();
+ break;
+ case TelephonyManagerCompat.EVENT_CALL_REMOTELY_HELD:
+ isRemotelyHeld = true;
+ update();
+ break;
+ case TelephonyManagerCompat.EVENT_CALL_REMOTELY_UNHELD:
+ isRemotelyHeld = false;
+ update();
+ break;
+ default:
+ break;
+ }
+ }
+ };
+ private long mTimeAddedMs;
+
+ public DialerCall(
+ Context context,
+ DialerCallDelegate dialerCallDelegate,
+ Call telecomCall,
+ LatencyReport latencyReport,
+ boolean registerCallback) {
+ Assert.isNotNull(context);
+ mContext = context;
+ mDialerCallDelegate = dialerCallDelegate;
+ mTelecomCall = telecomCall;
+ mLatencyReport = latencyReport;
+ mId = ID_PREFIX + Integer.toString(sIdCounter++);
+
+ updateFromTelecomCall(registerCallback);
+
+ if (registerCallback) {
+ mTelecomCall.registerCallback(mTelecomCallCallback);
+ }
+
+ mTimeAddedMs = System.currentTimeMillis();
+ parseCallSpecificAppData();
+ }
+
+ private static int translateState(int state) {
+ switch (state) {
+ case Call.STATE_NEW:
+ case Call.STATE_CONNECTING:
+ return DialerCall.State.CONNECTING;
+ case Call.STATE_SELECT_PHONE_ACCOUNT:
+ return DialerCall.State.SELECT_PHONE_ACCOUNT;
+ case Call.STATE_DIALING:
+ return DialerCall.State.DIALING;
+ case Call.STATE_PULLING_CALL:
+ return DialerCall.State.PULLING;
+ case Call.STATE_RINGING:
+ return DialerCall.State.INCOMING;
+ case Call.STATE_ACTIVE:
+ return DialerCall.State.ACTIVE;
+ case Call.STATE_HOLDING:
+ return DialerCall.State.ONHOLD;
+ case Call.STATE_DISCONNECTED:
+ return DialerCall.State.DISCONNECTED;
+ case Call.STATE_DISCONNECTING:
+ return DialerCall.State.DISCONNECTING;
+ default:
+ return DialerCall.State.INVALID;
+ }
+ }
+
+ public static boolean areSame(DialerCall call1, DialerCall call2) {
+ if (call1 == null && call2 == null) {
+ return true;
+ } else if (call1 == null || call2 == null) {
+ return false;
+ }
+
+ // otherwise compare call Ids
+ return call1.getId().equals(call2.getId());
+ }
+
+ public static boolean areSameNumber(DialerCall call1, DialerCall call2) {
+ if (call1 == null && call2 == null) {
+ return true;
+ } else if (call1 == null || call2 == null) {
+ return false;
+ }
+
+ // otherwise compare call Numbers
+ return TextUtils.equals(call1.getNumber(), call2.getNumber());
+ }
+
+ public void addListener(DialerCallListener listener) {
+ Assert.isMainThread();
+ mListeners.add(listener);
+ }
+
+ public void removeListener(DialerCallListener listener) {
+ Assert.isMainThread();
+ mListeners.remove(listener);
+ }
+
+ public void addCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
+ Assert.isMainThread();
+ mCannedTextResponsesLoadedListeners.add(listener);
+ }
+
+ public void removeCannedTextResponsesLoadedListener(CannedTextResponsesLoadedListener listener) {
+ Assert.isMainThread();
+ mCannedTextResponsesLoadedListeners.remove(listener);
+ }
+
+ public void notifyWiFiToLteHandover() {
+ LogUtil.i("DialerCall.notifyWiFiToLteHandover", "");
+ for (DialerCallListener listener : mListeners) {
+ listener.onWiFiToLteHandover();
+ }
+ }
+
+ public void notifyHandoverToWifiFailed() {
+ LogUtil.i("DialerCall.notifyHandoverToWifiFailed", "");
+ for (DialerCallListener listener : mListeners) {
+ listener.onHandoverToWifiFailure();
+ }
+ }
+
+ /* package-private */ Call getTelecomCall() {
+ return mTelecomCall;
+ }
+
+ public StatusHints getStatusHints() {
+ return mTelecomCall.getDetails().getStatusHints();
+ }
+
+ /**
+ * @return video settings of the call, null if the call is not a video call.
+ * @see VideoProfile
+ */
+ public VideoSettings getVideoSettings() {
+ return mVideoSettings;
+ }
+
+ private void update() {
+ Trace.beginSection("Update");
+ int oldState = getState();
+ // We want to potentially register a video call callback here.
+ updateFromTelecomCall(true /* registerCallback */);
+ if (oldState != getState() && getState() == DialerCall.State.DISCONNECTED) {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallDisconnect();
+ }
+ } else {
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpdate();
+ }
+ }
+ Trace.endSection();
+ }
+
+ private void updateFromTelecomCall(boolean registerCallback) {
+ LogUtil.v("DialerCall.updateFromTelecomCall", mTelecomCall.toString());
+ final int translatedState = translateState(mTelecomCall.getState());
+ if (mState != State.BLOCKED) {
+ setState(translatedState);
+ setDisconnectCause(mTelecomCall.getDetails().getDisconnectCause());
+ maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState());
+ }
+
+ if (registerCallback && mTelecomCall.getVideoCall() != null) {
+ if (mVideoCallCallback == null) {
+ mVideoCallCallback = new InCallVideoCallCallback(this);
+ }
+ mTelecomCall.getVideoCall().registerCallback(mVideoCallCallback);
+ mIsVideoCallCallbackRegistered = true;
+ }
+
+ mChildCallIds.clear();
+ final int numChildCalls = mTelecomCall.getChildren().size();
+ for (int i = 0; i < numChildCalls; i++) {
+ mChildCallIds.add(
+ mDialerCallDelegate
+ .getDialerCallFromTelecomCall(mTelecomCall.getChildren().get(i))
+ .getId());
+ }
+
+ // The number of conferenced calls can change over the course of the call, so use the
+ // maximum number of conferenced child calls as the metric for conference call usage.
+ mLogState.conferencedCalls = Math.max(numChildCalls, mLogState.conferencedCalls);
+
+ updateFromCallExtras(mTelecomCall.getDetails().getExtras());
+
+ // If the handle of the call has changed, update state for the call determining if it is an
+ // emergency call.
+ Uri newHandle = mTelecomCall.getDetails().getHandle();
+ if (!Objects.equals(mHandle, newHandle)) {
+ mHandle = newHandle;
+ updateEmergencyCallState();
+ }
+
+ // If the phone account handle of the call is set, cache capability bit indicating whether
+ // the phone account supports call subjects.
+ PhoneAccountHandle newPhoneAccountHandle = mTelecomCall.getDetails().getAccountHandle();
+ if (!Objects.equals(mPhoneAccountHandle, newPhoneAccountHandle)) {
+ mPhoneAccountHandle = newPhoneAccountHandle;
+
+ if (mPhoneAccountHandle != null) {
+ PhoneAccount phoneAccount =
+ mContext.getSystemService(TelecomManager.class).getPhoneAccount(mPhoneAccountHandle);
+ if (phoneAccount != null) {
+ mIsCallSubjectSupported =
+ phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_CALL_SUBJECT);
+ }
+ }
+ }
+
+ if (mSessionModificationState
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ && isVideoCall()) {
+ // We find out in {@link InCallVideoCallCallback.onSessionModifyResponseReceived}
+ // whether the video upgrade request was accepted. We don't clear the session modification
+ // state right away though to avoid having the UI switch from video to voice to video.
+ // Once the underlying telecom call updates to video mode it's safe to clear the state.
+ LogUtil.i(
+ "DialerCall.updateFromTelecomCall",
+ "upgraded to video, clearing session modification state");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ }
+
+ /**
+ * Tests corruption of the {@code callExtras} bundle by calling {@link
+ * Bundle#containsKey(String)}. If the bundle is corrupted a {@link IllegalArgumentException} will
+ * be thrown and caught by this function.
+ *
+ * @param callExtras the bundle to verify
+ * @return {@code true} if the bundle is corrupted, {@code false} otherwise.
+ */
+ protected boolean areCallExtrasCorrupted(Bundle callExtras) {
+ /**
+ * There's currently a bug in Telephony service (b/25613098) that could corrupt the extras
+ * bundle, resulting in a IllegalArgumentException while validating data under {@link
+ * Bundle#containsKey(String)}.
+ */
+ try {
+ callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS);
+ return false;
+ } catch (IllegalArgumentException e) {
+ LogUtil.e(
+ "DialerCall.areCallExtrasCorrupted", "callExtras is corrupted, ignoring exception", e);
+ return true;
+ }
+ }
+
+ protected void updateFromCallExtras(Bundle callExtras) {
+ if (callExtras == null || areCallExtrasCorrupted(callExtras)) {
+ /**
+ * If the bundle is corrupted, abandon information update as a work around. These are not
+ * critical for the dialer to function.
+ */
+ return;
+ }
+ // Check for a change in the child address and notify any listeners.
+ if (callExtras.containsKey(Connection.EXTRA_CHILD_ADDRESS)) {
+ String childNumber = callExtras.getString(Connection.EXTRA_CHILD_ADDRESS);
+ if (!Objects.equals(childNumber, mChildNumber)) {
+ mChildNumber = childNumber;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallChildNumberChange();
+ }
+ }
+ }
+
+ // Last forwarded number comes in as an array of strings. We want to choose the
+ // last item in the array. The forwarding numbers arrive independently of when the
+ // call is originally set up, so we need to notify the the UI of the change.
+ if (callExtras.containsKey(Connection.EXTRA_LAST_FORWARDED_NUMBER)) {
+ ArrayList<String> lastForwardedNumbers =
+ callExtras.getStringArrayList(Connection.EXTRA_LAST_FORWARDED_NUMBER);
+
+ if (lastForwardedNumbers != null) {
+ String lastForwardedNumber = null;
+ if (!lastForwardedNumbers.isEmpty()) {
+ lastForwardedNumber = lastForwardedNumbers.get(lastForwardedNumbers.size() - 1);
+ }
+
+ if (!Objects.equals(lastForwardedNumber, mLastForwardedNumber)) {
+ mLastForwardedNumber = lastForwardedNumber;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallLastForwardedNumberChange();
+ }
+ }
+ }
+ }
+
+ // DialerCall subject is present in the extras at the start of call, so we do not need to
+ // notify any other listeners of this.
+ if (callExtras.containsKey(Connection.EXTRA_CALL_SUBJECT)) {
+ String callSubject = callExtras.getString(Connection.EXTRA_CALL_SUBJECT);
+ if (!Objects.equals(mCallSubject, callSubject)) {
+ mCallSubject = callSubject;
+ }
+ }
+ }
+
+ /**
+ * Determines if a received upgrade to video request should be cancelled. This can happen if
+ * another InCall UI responds to the upgrade to video request.
+ *
+ * @param newVideoState The new video state.
+ */
+ private void maybeCancelVideoUpgrade(int newVideoState) {
+ boolean isVideoStateChanged = mVideoState != newVideoState;
+
+ if (mSessionModificationState
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST
+ && isVideoStateChanged) {
+
+ LogUtil.i("DialerCall.maybeCancelVideoUpgrade", "cancelling upgrade notification");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+ mVideoState = newVideoState;
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public boolean hasShownWiFiToLteHandoverToast() {
+ return hasShownWiFiToLteHandoverToast;
+ }
+
+ public void setHasShownWiFiToLteHandoverToast() {
+ hasShownWiFiToLteHandoverToast = true;
+ }
+
+ public boolean showWifiHandoverAlertAsToast() {
+ return doNotShowDialogForHandoffToWifiFailure;
+ }
+
+ public void setDoNotShowDialogForHandoffToWifiFailure(boolean bool) {
+ doNotShowDialogForHandoffToWifiFailure = bool;
+ }
+
+ public long getTimeAddedMs() {
+ return mTimeAddedMs;
+ }
+
+ @Nullable
+ public String getNumber() {
+ return TelecomCallUtil.getNumber(mTelecomCall);
+ }
+
+ public void blockCall() {
+ mTelecomCall.reject(false, null);
+ setState(State.BLOCKED);
+ }
+
+ @Nullable
+ public Uri getHandle() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getHandle();
+ }
+
+ public boolean isEmergencyCall() {
+ return mIsEmergencyCall;
+ }
+
+ public boolean isPotentialEmergencyCallback() {
+ // The property PROPERTY_EMERGENCY_CALLBACK_MODE is only set for CDMA calls when the system
+ // is actually in emergency callback mode (ie data is disabled).
+ if (hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE)) {
+ return true;
+ }
+ // We want to treat any incoming call that arrives a short time after an outgoing emergency call
+ // as a potential emergency callback.
+ if (getExtras() != null
+ && getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0)
+ > 0) {
+ long lastEmergencyCallMillis =
+ getExtras().getLong(TelecomManagerCompat.EXTRA_LAST_EMERGENCY_CALLBACK_TIME_MILLIS, 0);
+ if (isInEmergencyCallbackWindow(lastEmergencyCallMillis)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean isInEmergencyCallbackWindow(long timestampMillis) {
+ long emergencyCallbackWindowMillis =
+ ConfigProviderBindings.get(mContext)
+ .getLong(CONFIG_EMERGENCY_CALLBACK_WINDOW_MILLIS, TimeUnit.MINUTES.toMillis(5));
+ return System.currentTimeMillis() - timestampMillis < emergencyCallbackWindowMillis;
+ }
+
+ public int getState() {
+ if (mTelecomCall != null && mTelecomCall.getParent() != null) {
+ return State.CONFERENCED;
+ } else {
+ return mState;
+ }
+ }
+
+ public void setState(int state) {
+ mState = state;
+ if (mState == State.INCOMING) {
+ mLogState.isIncoming = true;
+ } else if (mState == State.DISCONNECTED) {
+ mLogState.duration =
+ getConnectTimeMillis() == 0 ? 0 : System.currentTimeMillis() - getConnectTimeMillis();
+ }
+ }
+
+ public int getNumberPresentation() {
+ return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getHandlePresentation();
+ }
+
+ public int getCnapNamePresentation() {
+ return mTelecomCall == null ? -1 : mTelecomCall.getDetails().getCallerDisplayNamePresentation();
+ }
+
+ @Nullable
+ public String getCnapName() {
+ return mTelecomCall == null ? null : getTelecomCall().getDetails().getCallerDisplayName();
+ }
+
+ public Bundle getIntentExtras() {
+ return mTelecomCall.getDetails().getIntentExtras();
+ }
+
+ @Nullable
+ public Bundle getExtras() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getExtras();
+ }
+
+ /** @return The child number for the call, or {@code null} if none specified. */
+ public String getChildNumber() {
+ return mChildNumber;
+ }
+
+ /** @return The last forwarded number for the call, or {@code null} if none specified. */
+ public String getLastForwardedNumber() {
+ return mLastForwardedNumber;
+ }
+
+ /** @return The call subject, or {@code null} if none specified. */
+ public String getCallSubject() {
+ return mCallSubject;
+ }
+
+ /**
+ * @return {@code true} if the call's phone account supports call subjects, {@code false}
+ * otherwise.
+ */
+ public boolean isCallSubjectSupported() {
+ return mIsCallSubjectSupported;
+ }
+
+ /** Returns call disconnect cause, defined by {@link DisconnectCause}. */
+ public DisconnectCause getDisconnectCause() {
+ if (mState == State.DISCONNECTED || mState == State.IDLE) {
+ return mDisconnectCause;
+ }
+
+ return new DisconnectCause(DisconnectCause.UNKNOWN);
+ }
+
+ public void setDisconnectCause(DisconnectCause disconnectCause) {
+ mDisconnectCause = disconnectCause;
+ mLogState.disconnectCause = mDisconnectCause;
+ }
+
+ /** Returns the possible text message responses. */
+ public List<String> getCannedSmsResponses() {
+ return mTelecomCall.getCannedTextResponses();
+ }
+
+ /** Checks if the call supports the given set of capabilities supplied as a bit mask. */
+ public boolean can(int capabilities) {
+ int supportedCapabilities = mTelecomCall.getDetails().getCallCapabilities();
+
+ if ((capabilities & Call.Details.CAPABILITY_MERGE_CONFERENCE) != 0) {
+ // We allow you to merge if the capabilities allow it or if it is a call with
+ // conferenceable calls.
+ if (mTelecomCall.getConferenceableCalls().isEmpty()
+ && ((Call.Details.CAPABILITY_MERGE_CONFERENCE & supportedCapabilities) == 0)) {
+ // Cannot merge calls if there are no calls to merge with.
+ return false;
+ }
+ capabilities &= ~Call.Details.CAPABILITY_MERGE_CONFERENCE;
+ }
+ return (capabilities == (capabilities & supportedCapabilities));
+ }
+
+ public boolean hasProperty(int property) {
+ return mTelecomCall.getDetails().hasProperty(property);
+ }
+
+ public String getUniqueCallId() {
+ return uniqueCallId;
+ }
+
+ /** Gets the time when the call first became active. */
+ public long getConnectTimeMillis() {
+ return mTelecomCall.getDetails().getConnectTimeMillis();
+ }
+
+ public boolean isConferenceCall() {
+ return hasProperty(Call.Details.PROPERTY_CONFERENCE);
+ }
+
+ @Nullable
+ public GatewayInfo getGatewayInfo() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getGatewayInfo();
+ }
+
+ @Nullable
+ public PhoneAccountHandle getAccountHandle() {
+ return mTelecomCall == null ? null : mTelecomCall.getDetails().getAccountHandle();
+ }
+
+ /**
+ * @return The {@link VideoCall} instance associated with the {@link Call}. Will return {@code
+ * null} until {@link #updateFromTelecomCall(boolean)} has registered a valid callback on the
+ * {@link VideoCall}.
+ */
+ public VideoCall getVideoCall() {
+ return mTelecomCall == null || !mIsVideoCallCallbackRegistered
+ ? null
+ : mTelecomCall.getVideoCall();
+ }
+
+ public List<String> getChildCallIds() {
+ return mChildCallIds;
+ }
+
+ public String getParentId() {
+ Call parentCall = mTelecomCall.getParent();
+ if (parentCall != null) {
+ return mDialerCallDelegate.getDialerCallFromTelecomCall(parentCall).getId();
+ }
+ return null;
+ }
+
+ public int getVideoState() {
+ return mTelecomCall.getDetails().getVideoState();
+ }
+
+ public boolean isVideoCall() {
+ return CallUtil.isVideoEnabled(mContext) && VideoUtils.isVideoCall(getVideoState());
+ }
+
+ /**
+ * Determines if the call handle is an emergency number or not and caches the result to avoid
+ * repeated calls to isEmergencyNumber.
+ */
+ private void updateEmergencyCallState() {
+ mIsEmergencyCall = TelecomCallUtil.isEmergencyCall(mTelecomCall);
+ }
+
+ /**
+ * Gets the video state which was requested via a session modification request.
+ *
+ * @return The video state.
+ */
+ public int getRequestedVideoState() {
+ return mRequestedVideoState;
+ }
+
+ /**
+ * Handles incoming session modification requests. Stores the pending video request and sets the
+ * session modification state to {@link
+ * DialerCall#SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST} so that we can keep
+ * track of the fact the request was received. Only upgrade requests require user confirmation and
+ * will be handled by this method. The remote user can turn off their own camera without
+ * confirmation.
+ *
+ * @param videoState The requested video state.
+ */
+ public void setRequestedVideoState(int videoState) {
+ LogUtil.v("DialerCall.setRequestedVideoState", "videoState: " + videoState);
+ if (videoState == getVideoState()) {
+ LogUtil.e("DialerCall.setRequestedVideoState", "clearing session modification state");
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ return;
+ }
+
+ mRequestedVideoState = videoState;
+ setSessionModificationState(
+ DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST);
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpgradeToVideo();
+ }
+
+ LogUtil.i(
+ "DialerCall.setRequestedVideoState",
+ "mSessionModificationState: %d, videoState: %d",
+ mSessionModificationState,
+ videoState);
+ update();
+ }
+
+ /**
+ * Gets the current video session modification state.
+ *
+ * @return The session modification state.
+ */
+ @SessionModificationState
+ public int getSessionModificationState() {
+ return mSessionModificationState;
+ }
+
+ /**
+ * Set the session modification state. Used to keep track of pending video session modification
+ * operations and to inform listeners of these changes.
+ *
+ * @param state the new session modification state.
+ */
+ public void setSessionModificationState(@SessionModificationState int state) {
+ boolean hasChanged = mSessionModificationState != state;
+ if (hasChanged) {
+ LogUtil.i(
+ "DialerCall.setSessionModificationState", "%d -> %d", mSessionModificationState, state);
+ mSessionModificationState = state;
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallSessionModificationStateChange(state);
+ }
+ }
+ }
+
+ public LogState getLogState() {
+ return mLogState;
+ }
+
+ /**
+ * Determines if the call is an external call.
+ *
+ * <p>An external call is one which does not exist locally for the {@link
+ * android.telecom.ConnectionService} it is associated with.
+ *
+ * <p>External calls are only supported in N and higher.
+ *
+ * @return {@code true} if the call is an external call, {@code false} otherwise.
+ */
+ public boolean isExternalCall() {
+ return VERSION.SDK_INT >= VERSION_CODES.N
+ && hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL);
+ }
+
+ /**
+ * Determines if the external call is pullable.
+ *
+ * <p>An external call is one which does not exist locally for the {@link
+ * android.telecom.ConnectionService} it is associated with. An external call may be "pullable",
+ * which means that the user can request it be transferred to the current device.
+ *
+ * <p>External calls are only supported in N and higher.
+ *
+ * @return {@code true} if the call is an external call, {@code false} otherwise.
+ */
+ public boolean isPullableExternalCall() {
+ return VERSION.SDK_INT >= VERSION_CODES.N
+ && (mTelecomCall.getDetails().getCallCapabilities()
+ & CallCompat.Details.CAPABILITY_CAN_PULL_CALL)
+ == CallCompat.Details.CAPABILITY_CAN_PULL_CALL;
+ }
+
+ /**
+ * Determines if answering this call will cause an ongoing video call to be dropped.
+ *
+ * @return {@code true} if answering this call will drop an ongoing video call, {@code false}
+ * otherwise.
+ */
+ public boolean answeringDisconnectsForegroundVideoCall() {
+ Bundle extras = getExtras();
+ if (extras == null
+ || !extras.containsKey(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL)) {
+ return false;
+ }
+ return extras.getBoolean(CallCompat.Details.EXTRA_ANSWERING_DROPS_FOREGROUND_CALL);
+ }
+
+ private void parseCallSpecificAppData() {
+ if (isExternalCall()) {
+ return;
+ }
+
+ mLogState.callSpecificAppData = CallIntentParser.getCallSpecificAppData(getIntentExtras());
+ if (mLogState.callSpecificAppData == null) {
+ mLogState.callSpecificAppData = new CallSpecificAppData();
+ mLogState.callSpecificAppData.callInitiationType =
+ CallInitiationType.Type.EXTERNAL_INITIATION;
+ }
+ if (getState() == State.INCOMING) {
+ mLogState.callSpecificAppData.callInitiationType =
+ CallInitiationType.Type.INCOMING_INITIATION;
+ }
+ }
+
+ @Override
+ public String toString() {
+ if (mTelecomCall == null) {
+ // This should happen only in testing since otherwise we would never have a null
+ // Telecom call.
+ return String.valueOf(mId);
+ }
+
+ return String.format(
+ Locale.US,
+ "[%s, %s, %s, %s, children:%s, parent:%s, "
+ + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]",
+ mId,
+ State.toString(getState()),
+ Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()),
+ Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()),
+ mChildCallIds,
+ getParentId(),
+ this.mTelecomCall.getConferenceableCalls(),
+ VideoProfile.videoStateToString(mTelecomCall.getDetails().getVideoState()),
+ mSessionModificationState,
+ getVideoSettings());
+ }
+
+ public String toSimpleString() {
+ return super.toString();
+ }
+
+ @CallHistoryStatus
+ public int getCallHistoryStatus() {
+ return mCallHistoryStatus;
+ }
+
+ public void setCallHistoryStatus(@CallHistoryStatus int callHistoryStatus) {
+ mCallHistoryStatus = callHistoryStatus;
+ }
+
+ public boolean didShowCameraPermission() {
+ return didShowCameraPermission;
+ }
+
+ public void setDidShowCameraPermission(boolean didShow) {
+ didShowCameraPermission = didShow;
+ }
+
+ public boolean isInGlobalSpamList() {
+ return isInGlobalSpamList;
+ }
+
+ public void setIsInGlobalSpamList(boolean inSpamList) {
+ isInGlobalSpamList = inSpamList;
+ }
+
+ public boolean isInUserSpamList() {
+ return isInUserSpamList;
+ }
+
+ public void setIsInUserSpamList(boolean inSpamList) {
+ isInUserSpamList = inSpamList;
+ }
+
+ public boolean isInUserWhiteList() {
+ return isInUserWhiteList;
+ }
+
+ public void setIsInUserWhiteList(boolean inWhiteList) {
+ isInUserWhiteList = inWhiteList;
+ }
+
+ public boolean isSpam() {
+ return mIsSpam;
+ }
+
+ public void setSpam(boolean isSpam) {
+ mIsSpam = isSpam;
+ }
+
+ public boolean isBlocked() {
+ return mIsBlocked;
+ }
+
+ public void setBlockedStatus(boolean isBlocked) {
+ mIsBlocked = isBlocked;
+ }
+
+ public boolean isRemotelyHeld() {
+ return isRemotelyHeld;
+ }
+
+ public boolean isIncoming() {
+ return mLogState.isIncoming;
+ }
+
+ public LatencyReport getLatencyReport() {
+ return mLatencyReport;
+ }
+
+ public void unregisterCallback() {
+ mTelecomCall.unregisterCallback(mTelecomCallCallback);
+ }
+
+ public void acceptUpgradeRequest(int videoState) {
+ LogUtil.i("DialerCall.acceptUpgradeRequest", "videoState: " + videoState);
+ VideoProfile videoProfile = new VideoProfile(videoState);
+ getVideoCall().sendSessionModifyResponse(videoProfile);
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ public void declineUpgradeRequest() {
+ LogUtil.i("DialerCall.declineUpgradeRequest", "");
+ VideoProfile videoProfile = new VideoProfile(getVideoState());
+ getVideoCall().sendSessionModifyResponse(videoProfile);
+ setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ }
+
+ public void phoneAccountSelected(PhoneAccountHandle accountHandle, boolean setDefault) {
+ LogUtil.i(
+ "DialerCall.phoneAccountSelected",
+ "accountHandle: %s, setDefault: %b",
+ accountHandle,
+ setDefault);
+ mTelecomCall.phoneAccountSelected(accountHandle, setDefault);
+ }
+
+ public void disconnect() {
+ LogUtil.i("DialerCall.disconnect", "");
+ setState(DialerCall.State.DISCONNECTING);
+ for (DialerCallListener listener : mListeners) {
+ listener.onDialerCallUpdate();
+ }
+ mTelecomCall.disconnect();
+ }
+
+ public void hold() {
+ LogUtil.i("DialerCall.hold", "");
+ mTelecomCall.hold();
+ }
+
+ public void unhold() {
+ LogUtil.i("DialerCall.unhold", "");
+ mTelecomCall.unhold();
+ }
+
+ public void splitFromConference() {
+ LogUtil.i("DialerCall.splitFromConference", "");
+ mTelecomCall.splitFromConference();
+ }
+
+ public void answer(int videoState) {
+ LogUtil.i("DialerCall.answer", "videoState: " + videoState);
+ mTelecomCall.answer(videoState);
+ }
+
+ public void reject(boolean rejectWithMessage, String message) {
+ LogUtil.i("DialerCall.reject", "");
+ mTelecomCall.reject(rejectWithMessage, message);
+ }
+
+ /** Return the string label to represent the call provider */
+ public String getCallProviderLabel() {
+ if (callProviderLabel == null) {
+ PhoneAccount account = getPhoneAccount();
+ if (account != null && !TextUtils.isEmpty(account.getLabel())) {
+ List<PhoneAccountHandle> accounts =
+ mContext.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+ if (accounts != null && accounts.size() > 1) {
+ callProviderLabel = account.getLabel().toString();
+ }
+ }
+ if (callProviderLabel == null) {
+ callProviderLabel = "";
+ }
+ }
+ return callProviderLabel;
+ }
+
+ private PhoneAccount getPhoneAccount() {
+ PhoneAccountHandle accountHandle = getAccountHandle();
+ if (accountHandle == null) {
+ return null;
+ }
+ return mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ }
+
+ public String getCallbackNumber() {
+ if (callbackNumber == null) {
+ // Show the emergency callback number if either:
+ // 1. This is an emergency call.
+ // 2. The phone is in Emergency Callback Mode, which means we should show the callback
+ // number.
+ boolean showCallbackNumber = hasProperty(Details.PROPERTY_EMERGENCY_CALLBACK_MODE);
+
+ if (isEmergencyCall() || showCallbackNumber) {
+ callbackNumber = getSubscriptionNumber();
+ } else {
+ StatusHints statusHints = getTelecomCall().getDetails().getStatusHints();
+ if (statusHints != null) {
+ Bundle extras = statusHints.getExtras();
+ if (extras != null) {
+ callbackNumber = extras.getString(TelecomManager.EXTRA_CALL_BACK_NUMBER);
+ }
+ }
+ }
+
+ String simNumber =
+ mContext.getSystemService(TelecomManager.class).getLine1Number(getAccountHandle());
+ if (!showCallbackNumber && PhoneNumberUtils.compare(callbackNumber, simNumber)) {
+ LogUtil.v(
+ "DialerCall.getCallbackNumber",
+ "numbers are the same (and callback number is not being forced to show);"
+ + " not showing the callback number");
+ callbackNumber = "";
+ }
+ if (callbackNumber == null) {
+ callbackNumber = "";
+ }
+ }
+ return callbackNumber;
+ }
+
+ private String getSubscriptionNumber() {
+ // If it's an emergency call, and they're not populating the callback number,
+ // then try to fall back to the phone sub info (to hopefully get the SIM's
+ // number directly from the telephony layer).
+ PhoneAccountHandle accountHandle = getAccountHandle();
+ if (accountHandle != null) {
+ PhoneAccount account =
+ mContext.getSystemService(TelecomManager.class).getPhoneAccount(accountHandle);
+ if (account != null) {
+ return getNumberFromHandle(account.getSubscriptionAddress());
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Specifies whether a number is in the call history or not. {@link #CALL_HISTORY_STATUS_UNKNOWN}
+ * means there is no result.
+ */
+ @IntDef({
+ CALL_HISTORY_STATUS_UNKNOWN,
+ CALL_HISTORY_STATUS_PRESENT,
+ CALL_HISTORY_STATUS_NOT_PRESENT
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CallHistoryStatus {}
+
+ /* Defines different states of this call */
+ public static class State {
+
+ public static final int INVALID = 0;
+ public static final int NEW = 1; /* The call is new. */
+ public static final int IDLE = 2; /* The call is idle. Nothing active */
+ public static final int ACTIVE = 3; /* There is an active call */
+ public static final int INCOMING = 4; /* A normal incoming phone call */
+ public static final int CALL_WAITING = 5; /* Incoming call while another is active */
+ public static final int DIALING = 6; /* An outgoing call during dial phase */
+ public static final int REDIALING = 7; /* Subsequent dialing attempt after a failure */
+ public static final int ONHOLD = 8; /* An active phone call placed on hold */
+ public static final int DISCONNECTING = 9; /* A call is being ended. */
+ public static final int DISCONNECTED = 10; /* State after a call disconnects */
+ public static final int CONFERENCED = 11; /* DialerCall part of a conference call */
+ public static final int SELECT_PHONE_ACCOUNT = 12; /* Waiting for account selection */
+ public static final int CONNECTING = 13; /* Waiting for Telecom broadcast to finish */
+ public static final int BLOCKED = 14; /* The number was found on the block list */
+ public static final int PULLING = 15; /* An external call being pulled to the device */
+
+ public static boolean isConnectingOrConnected(int state) {
+ switch (state) {
+ case ACTIVE:
+ case INCOMING:
+ case CALL_WAITING:
+ case CONNECTING:
+ case DIALING:
+ case PULLING:
+ case REDIALING:
+ case ONHOLD:
+ case CONFERENCED:
+ return true;
+ default:
+ }
+ return false;
+ }
+
+ public static boolean isDialing(int state) {
+ return state == DIALING || state == PULLING || state == REDIALING;
+ }
+
+ public static String toString(int state) {
+ switch (state) {
+ case INVALID:
+ return "INVALID";
+ case NEW:
+ return "NEW";
+ case IDLE:
+ return "IDLE";
+ case ACTIVE:
+ return "ACTIVE";
+ case INCOMING:
+ return "INCOMING";
+ case CALL_WAITING:
+ return "CALL_WAITING";
+ case DIALING:
+ return "DIALING";
+ case PULLING:
+ return "PULLING";
+ case REDIALING:
+ return "REDIALING";
+ case ONHOLD:
+ return "ONHOLD";
+ case DISCONNECTING:
+ return "DISCONNECTING";
+ case DISCONNECTED:
+ return "DISCONNECTED";
+ case CONFERENCED:
+ return "CONFERENCED";
+ case SELECT_PHONE_ACCOUNT:
+ return "SELECT_PHONE_ACCOUNT";
+ case CONNECTING:
+ return "CONNECTING";
+ case BLOCKED:
+ return "BLOCKED";
+ default:
+ return "UNKNOWN";
+ }
+ }
+ }
+
+ /**
+ * Defines different states of session modify requests, which are used to upgrade to video, or
+ * downgrade to audio.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SESSION_MODIFICATION_STATE_NO_REQUEST,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE,
+ SESSION_MODIFICATION_STATE_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT,
+ SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED,
+ SESSION_MODIFICATION_STATE_REQUEST_REJECTED,
+ SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE
+ })
+ public @interface SessionModificationState {}
+
+ public static final int SESSION_MODIFICATION_STATE_NO_REQUEST = 0;
+ public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE = 1;
+ public static final int SESSION_MODIFICATION_STATE_REQUEST_FAILED = 2;
+ public static final int SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST = 3;
+ public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT = 4;
+ public static final int SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED = 5;
+ public static final int SESSION_MODIFICATION_STATE_REQUEST_REJECTED = 6;
+ public static final int SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE = 7;
+
+ public static class VideoSettings {
+
+ public static final int CAMERA_DIRECTION_UNKNOWN = -1;
+ public static final int CAMERA_DIRECTION_FRONT_FACING = CameraCharacteristics.LENS_FACING_FRONT;
+ public static final int CAMERA_DIRECTION_BACK_FACING = CameraCharacteristics.LENS_FACING_BACK;
+
+ private int mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
+
+ /**
+ * Gets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
+ * state of the call should be used to infer the camera direction.
+ *
+ * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
+ * @see {@link CameraCharacteristics#LENS_FACING_BACK}
+ */
+ public int getCameraDir() {
+ return mCameraDirection;
+ }
+
+ /**
+ * Sets the camera direction. if camera direction is set to CAMERA_DIRECTION_UNKNOWN, the video
+ * state of the call should be used to infer the camera direction.
+ *
+ * @see {@link CameraCharacteristics#LENS_FACING_FRONT}
+ * @see {@link CameraCharacteristics#LENS_FACING_BACK}
+ */
+ public void setCameraDir(int cameraDirection) {
+ if (cameraDirection == CAMERA_DIRECTION_FRONT_FACING
+ || cameraDirection == CAMERA_DIRECTION_BACK_FACING) {
+ mCameraDirection = cameraDirection;
+ } else {
+ mCameraDirection = CAMERA_DIRECTION_UNKNOWN;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "(CameraDir:" + getCameraDir() + ")";
+ }
+ }
+
+ /**
+ * Tracks any state variables that is useful for logging. There is some amount of overlap with
+ * existing call member variables, but this duplication helps to ensure that none of these logging
+ * variables will interface with/and affect call logic.
+ */
+ public static class LogState {
+
+ public DisconnectCause disconnectCause;
+ public boolean isIncoming = false;
+ public int contactLookupResult = ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE;
+ public CallSpecificAppData callSpecificAppData;
+ // If this was a conference call, the total number of calls involved in the conference.
+ public int conferencedCalls = 0;
+ public long duration = 0;
+ public boolean isLogged = false;
+
+ private static String lookupToString(int lookupType) {
+ switch (lookupType) {
+ case ContactLookupResult.Type.LOCAL_CONTACT:
+ return "Local";
+ case ContactLookupResult.Type.LOCAL_CACHE:
+ return "Cache";
+ case ContactLookupResult.Type.REMOTE:
+ return "Remote";
+ case ContactLookupResult.Type.EMERGENCY:
+ return "Emergency";
+ case ContactLookupResult.Type.VOICEMAIL:
+ return "Voicemail";
+ default:
+ return "Not found";
+ }
+ }
+
+ private static String initiationToString(CallSpecificAppData callSpecificAppData) {
+ if (callSpecificAppData == null) {
+ return "null";
+ }
+ switch (callSpecificAppData.callInitiationType) {
+ case CallInitiationType.Type.INCOMING_INITIATION:
+ return "Incoming";
+ case CallInitiationType.Type.DIALPAD:
+ return "Dialpad";
+ case CallInitiationType.Type.SPEED_DIAL:
+ return "Speed Dial";
+ case CallInitiationType.Type.REMOTE_DIRECTORY:
+ return "Remote Directory";
+ case CallInitiationType.Type.SMART_DIAL:
+ return "Smart Dial";
+ case CallInitiationType.Type.REGULAR_SEARCH:
+ return "Regular Search";
+ case CallInitiationType.Type.CALL_LOG:
+ return "DialerCall Log";
+ case CallInitiationType.Type.CALL_LOG_FILTER:
+ return "DialerCall Log Filter";
+ case CallInitiationType.Type.VOICEMAIL_LOG:
+ return "Voicemail Log";
+ case CallInitiationType.Type.CALL_DETAILS:
+ return "DialerCall Details";
+ case CallInitiationType.Type.QUICK_CONTACTS:
+ return "Quick Contacts";
+ case CallInitiationType.Type.EXTERNAL_INITIATION:
+ return "External";
+ case CallInitiationType.Type.LAUNCHER_SHORTCUT:
+ return "Launcher Shortcut";
+ default:
+ return "Unknown: " + callSpecificAppData.callInitiationType;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "["
+ + "%s, " // DisconnectCause toString already describes the object type
+ + "isIncoming: %s, "
+ + "contactLookup: %s, "
+ + "callInitiation: %s, "
+ + "duration: %s"
+ + "]",
+ disconnectCause,
+ isIncoming,
+ lookupToString(contactLookupResult),
+ initiationToString(callSpecificAppData),
+ duration);
+ }
+ }
+
+ /** Called when canned text responses have been loaded. */
+ public interface CannedTextResponsesLoadedListener {
+ void onCannedTextResponsesLoaded(DialerCall call);
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCallDelegate.java b/java/com/android/incallui/call/DialerCallDelegate.java
new file mode 100644
index 000000000..463b4916a
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCallDelegate.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.incallui.call;
+
+import android.telecom.Call;
+
+/** Callback from the call module to the container. */
+public interface DialerCallDelegate {
+
+ DialerCall getDialerCallFromTelecomCall(Call telecomCall);
+}
diff --git a/java/com/android/incallui/call/DialerCallListener.java b/java/com/android/incallui/call/DialerCallListener.java
new file mode 100644
index 000000000..b426cd72e
--- /dev/null
+++ b/java/com/android/incallui/call/DialerCallListener.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.incallui.call;
+
+import com.android.incallui.call.DialerCall.SessionModificationState;
+
+/** Used to monitor state changes in a dialer call. */
+public interface DialerCallListener {
+
+ void onDialerCallDisconnect();
+
+ void onDialerCallUpdate();
+
+ void onDialerCallChildNumberChange();
+
+ void onDialerCallLastForwardedNumberChange();
+
+ void onDialerCallUpgradeToVideo();
+
+ void onDialerCallSessionModificationStateChange(@SessionModificationState int state);
+
+ void onWiFiToLteHandover();
+
+ void onHandoverToWifiFailure();
+}
diff --git a/java/com/android/incallui/call/ExternalCallList.java b/java/com/android/incallui/call/ExternalCallList.java
new file mode 100644
index 000000000..52a7a304b
--- /dev/null
+++ b/java/com/android/incallui/call/ExternalCallList.java
@@ -0,0 +1,136 @@
+/*
+ * 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.incallui.call;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.telecom.Call;
+import android.util.ArraySet;
+import com.android.contacts.common.compat.CallCompat;
+import com.android.dialer.common.LogUtil;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Tracks the external calls known to the InCall UI.
+ *
+ * <p>External calls are those with {@code android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}.
+ */
+public class ExternalCallList {
+
+ private final Set<Call> mExternalCalls = new ArraySet<>();
+ private final Set<ExternalCallListener> mExternalCallListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<ExternalCallListener, Boolean>(8, 0.9f, 1));
+ /** Handles {@link android.telecom.Call.Callback} callbacks. */
+ private final Call.Callback mTelecomCallCallback =
+ new Call.Callback() {
+ @Override
+ public void onDetailsChanged(Call call, Call.Details details) {
+ notifyExternalCallUpdated(call);
+ }
+ };
+
+ /** Begins tracking an external call and notifies listeners of the new call. */
+ public void onCallAdded(Call telecomCall) {
+ if (!telecomCall.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ throw new IllegalArgumentException();
+ }
+ mExternalCalls.add(telecomCall);
+ telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper()));
+ notifyExternalCallAdded(telecomCall);
+ }
+
+ /** Stops tracking an external call and notifies listeners of the removal of the call. */
+ public void onCallRemoved(Call telecomCall) {
+ if (!mExternalCalls.contains(telecomCall)) {
+ // This can happen on M for external calls from blocked numbers
+ LogUtil.i("ExternalCallList.onCallRemoved", "attempted to remove unregistered call");
+ return;
+ }
+ mExternalCalls.remove(telecomCall);
+ telecomCall.unregisterCallback(mTelecomCallCallback);
+ notifyExternalCallRemoved(telecomCall);
+ }
+
+ /** Adds a new listener to external call events. */
+ public void addExternalCallListener(@NonNull ExternalCallListener listener) {
+ mExternalCallListeners.add(listener);
+ }
+
+ /** Removes a listener to external call events. */
+ public void removeExternalCallListener(@NonNull ExternalCallListener listener) {
+ if (!mExternalCallListeners.contains(listener)) {
+ LogUtil.i(
+ "ExternalCallList.removeExternalCallListener",
+ "attempt to remove unregistered listener.");
+ }
+ mExternalCallListeners.remove(listener);
+ }
+
+ public boolean isCallTracked(@NonNull android.telecom.Call telecomCall) {
+ return mExternalCalls.contains(telecomCall);
+ }
+
+ /** Notifies listeners of the addition of a new external call. */
+ private void notifyExternalCallAdded(Call call) {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallAdded(call);
+ }
+ }
+
+ /** Notifies listeners of the removal of an external call. */
+ private void notifyExternalCallRemoved(Call call) {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallRemoved(call);
+ }
+ }
+
+ /** Notifies listeners of changes to an external call. */
+ private void notifyExternalCallUpdated(Call call) {
+ if (!call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) {
+ // A previous external call has been pulled and is now a regular call, so we will remove
+ // it from the external call listener and ensure that the CallList is informed of the
+ // change.
+ onCallRemoved(call);
+
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallPulled(call);
+ }
+ } else {
+ for (ExternalCallListener listener : mExternalCallListeners) {
+ listener.onExternalCallUpdated(call);
+ }
+ }
+ }
+
+ /**
+ * Defines events which the {@link ExternalCallList} exposes to interested components (e.g. {@link
+ * com.android.incallui.ExternalCallNotifier ExternalCallNotifier}).
+ */
+ public interface ExternalCallListener {
+
+ void onExternalCallAdded(Call call);
+
+ void onExternalCallRemoved(Call call);
+
+ void onExternalCallUpdated(Call call);
+
+ void onExternalCallPulled(Call call);
+ }
+}
diff --git a/java/com/android/incallui/call/InCallServiceListener.java b/java/com/android/incallui/call/InCallServiceListener.java
new file mode 100644
index 000000000..e48ce9d79
--- /dev/null
+++ b/java/com/android/incallui/call/InCallServiceListener.java
@@ -0,0 +1,40 @@
+/*
+ * 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.incallui.call;
+
+import android.telecom.InCallService;
+
+/**
+ * Interface implemented by In-Call components that maintain a reference to the Telecom API {@code
+ * InCallService} object. Clarifies the expectations associated with the relevant method calls.
+ */
+public interface InCallServiceListener {
+
+ /**
+ * Called once at {@code InCallService} startup time with a valid instance. At that time, there
+ * will be no existing {@code DialerCall}s.
+ *
+ * @param inCallService The {@code InCallService} object.
+ */
+ void setInCallService(InCallService inCallService);
+
+ /**
+ * Called once at {@code InCallService} shutdown time. At that time, any {@code DialerCall}s will
+ * have transitioned through the disconnected state and will no longer exist.
+ */
+ void clearInCallService();
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindings.java b/java/com/android/incallui/call/InCallUiLegacyBindings.java
new file mode 100644
index 000000000..1b0ed4542
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindings.java
@@ -0,0 +1,26 @@
+/*
+ * 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.incallui.call;
+
+/**
+ * These are old bindings between InCallUi and the container application. All new bindings should be
+ * added to the bindings module and not here.
+ */
+public interface InCallUiLegacyBindings {
+
+ void logCall(DialerCall call);
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java b/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java
new file mode 100644
index 000000000..8604976f7
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindingsFactory.java
@@ -0,0 +1,26 @@
+/*
+ * 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.incallui.call;
+
+/**
+ * This interface should be implementated by the Application subclass. It allows the in call UI
+ * module to get references to the InCallUiLegacyBindings.
+ */
+public interface InCallUiLegacyBindingsFactory {
+
+ InCallUiLegacyBindings newInCallUiLegacyBindings();
+}
diff --git a/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java b/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java
new file mode 100644
index 000000000..8869c64b2
--- /dev/null
+++ b/java/com/android/incallui/call/InCallUiLegacyBindingsStub.java
@@ -0,0 +1,24 @@
+/*
+ * 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.incallui.call;
+
+/** Default implementation for in call UI legacy bindings. */
+public class InCallUiLegacyBindingsStub implements InCallUiLegacyBindings {
+
+ @Override
+ public void logCall(DialerCall call) {}
+}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallback.java b/java/com/android/incallui/call/InCallVideoCallCallback.java
new file mode 100644
index 000000000..f897ac9dd
--- /dev/null
+++ b/java/com/android/incallui/call/InCallVideoCallCallback.java
@@ -0,0 +1,197 @@
+/*
+ * 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.incallui.call;
+
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.telecom.Connection;
+import android.telecom.Connection.VideoProvider;
+import android.telecom.InCallService.VideoCall;
+import android.telecom.VideoProfile;
+import android.telecom.VideoProfile.CameraCapabilities;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+
+/** Implements the InCallUI VideoCall Callback. */
+public class InCallVideoCallCallback extends VideoCall.Callback implements Runnable {
+
+ private static final int CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS = 4000;
+
+ private final DialerCall call;
+ @Nullable private Handler handler;
+ @SessionModificationState private int newSessionModificationState;
+
+ public InCallVideoCallCallback(DialerCall call) {
+ this.call = call;
+ }
+
+ @Override
+ public void onSessionModifyRequestReceived(VideoProfile videoProfile) {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyRequestReceived", "videoProfile: " + videoProfile);
+ int previousVideoState = VideoUtils.getUnPausedVideoState(call.getVideoState());
+ int newVideoState = VideoUtils.getUnPausedVideoState(videoProfile.getVideoState());
+
+ boolean wasVideoCall = VideoUtils.isVideoCall(previousVideoState);
+ boolean isVideoCall = VideoUtils.isVideoCall(newVideoState);
+
+ if (wasVideoCall && !isVideoCall) {
+ LogUtil.v(
+ "InCallVideoCallCallback.onSessionModifyRequestReceived",
+ "call downgraded to " + newVideoState);
+ } else if (previousVideoState != newVideoState) {
+ InCallVideoCallCallbackNotifier.getInstance().upgradeToVideoRequest(call, newVideoState);
+ }
+ }
+
+ /**
+ * @param status Status of the session modify request. Valid values are {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_SUCCESS}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_FAIL}, {@link
+ * Connection.VideoProvider#SESSION_MODIFY_REQUEST_INVALID}
+ * @param responseProfile The actual profile changes made by the peer device.
+ */
+ @Override
+ public void onSessionModifyResponseReceived(
+ int status, VideoProfile requestedProfile, VideoProfile responseProfile) {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "status: %d, "
+ + "requestedProfile: %s, responseProfile: %s, current session modification state: %d",
+ status,
+ requestedProfile,
+ responseProfile,
+ call.getSessionModificationState());
+
+ if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE) {
+ if (handler == null) {
+ handler = new Handler();
+ } else {
+ handler.removeCallbacks(this);
+ }
+
+ newSessionModificationState = getDialerSessionModifyStateTelecomStatus(status);
+ if (status != VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
+ // This will update the video UI to display the error message.
+ call.setSessionModificationState(newSessionModificationState);
+ }
+
+ // Wait for 4 seconds and then clean the session modification state. This allows the video UI
+ // to stay up so that the user can read the error message.
+ //
+ // If the other person accepted the upgrade request then this will keep the video UI up until
+ // the call's video state change. Without this we would switch to the voice call and then
+ // switch back to video UI.
+ handler.postDelayed(this, CLEAR_FAILED_REQUEST_TIMEOUT_MILLIS);
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST) {
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else if (call.getSessionModificationState()
+ == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_RESPONSE) {
+ call.setSessionModificationState(getDialerSessionModifyStateTelecomStatus(status));
+ } else {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "call is not waiting for " + "response, doing nothing");
+ }
+ }
+
+ @SessionModificationState
+ private int getDialerSessionModifyStateTelecomStatus(int telecomStatus) {
+ switch (telecomStatus) {
+ case VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS:
+ return DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST;
+ case VideoProvider.SESSION_MODIFY_REQUEST_FAIL:
+ case VideoProvider.SESSION_MODIFY_REQUEST_INVALID:
+ // Check if it's already video call, which means the request is not video upgrade request.
+ if (VideoUtils.isVideoCall(call.getVideoState())) {
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ } else {
+ return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED;
+ }
+ case VideoProvider.SESSION_MODIFY_REQUEST_TIMED_OUT:
+ return DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ case VideoProvider.SESSION_MODIFY_REQUEST_REJECTED_BY_REMOTE:
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED;
+ default:
+ LogUtil.e(
+ "InCallVideoCallCallback.getDialerSessionModifyStateTelecomStatus",
+ "unknown status: %d",
+ telecomStatus);
+ return DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED;
+ }
+ }
+
+ @Override
+ public void onCallSessionEvent(int event) {
+ InCallVideoCallCallbackNotifier.getInstance().callSessionEvent(event);
+ }
+
+ @Override
+ public void onPeerDimensionsChanged(int width, int height) {
+ InCallVideoCallCallbackNotifier.getInstance().peerDimensionsChanged(call, width, height);
+ }
+
+ @Override
+ public void onVideoQualityChanged(int videoQuality) {
+ InCallVideoCallCallbackNotifier.getInstance().videoQualityChanged(call, videoQuality);
+ }
+
+ /**
+ * Handles a change to the call data usage. No implementation as the in-call UI does not display
+ * data usage.
+ *
+ * @param dataUsage The updated data usage.
+ */
+ @Override
+ public void onCallDataUsageChanged(long dataUsage) {
+ LogUtil.v("InCallVideoCallCallback.onCallDataUsageChanged", "dataUsage = " + dataUsage);
+ InCallVideoCallCallbackNotifier.getInstance().callDataUsageChanged(dataUsage);
+ }
+
+ /**
+ * Handles changes to the camera capabilities. No implementation as the in-call UI does not make
+ * use of camera capabilities.
+ *
+ * @param cameraCapabilities The changed camera capabilities.
+ */
+ @Override
+ public void onCameraCapabilitiesChanged(CameraCapabilities cameraCapabilities) {
+ if (cameraCapabilities != null) {
+ InCallVideoCallCallbackNotifier.getInstance()
+ .cameraDimensionsChanged(
+ call, cameraCapabilities.getWidth(), cameraCapabilities.getHeight());
+ }
+ }
+
+ /**
+ * Called 4 seconds after the remote user responds to the video upgrade request. We use this to
+ * clear the session modify state.
+ */
+ @Override
+ public void run() {
+ if (call.getSessionModificationState() == newSessionModificationState) {
+ LogUtil.i("InCallVideoCallCallback.onSessionModifyResponseReceived", "clearing state");
+ call.setSessionModificationState(DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST);
+ } else {
+ LogUtil.i(
+ "InCallVideoCallCallback.onSessionModifyResponseReceived",
+ "session modification state has changed, not clearing state");
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
new file mode 100644
index 000000000..4a949263c
--- /dev/null
+++ b/java/com/android/incallui/call/InCallVideoCallCallbackNotifier.java
@@ -0,0 +1,279 @@
+/*
+ * 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.incallui.call;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Class used by {@link InCallService.VideoCallCallback} to notify interested parties of incoming
+ * events.
+ */
+public class InCallVideoCallCallbackNotifier {
+
+ /** Singleton instance of this class. */
+ private static InCallVideoCallCallbackNotifier sInstance = new InCallVideoCallCallbackNotifier();
+
+ /**
+ * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before
+ * resizing, 1 means we only expect a single thread to access the map so make only a single shard
+ */
+ private final Set<SessionModificationListener> mSessionModificationListeners =
+ Collections.newSetFromMap(
+ new ConcurrentHashMap<SessionModificationListener, Boolean>(8, 0.9f, 1));
+
+ private final Set<VideoEventListener> mVideoEventListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<VideoEventListener, Boolean>(8, 0.9f, 1));
+ private final Set<SurfaceChangeListener> mSurfaceChangeListeners =
+ Collections.newSetFromMap(new ConcurrentHashMap<SurfaceChangeListener, Boolean>(8, 0.9f, 1));
+
+ /** Private constructor. Instance should only be acquired through getInstance(). */
+ private InCallVideoCallCallbackNotifier() {}
+
+ /** Static singleton accessor method. */
+ public static InCallVideoCallCallbackNotifier getInstance() {
+ return sInstance;
+ }
+
+ /**
+ * Adds a new {@link SessionModificationListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addSessionModificationListener(@NonNull SessionModificationListener listener) {
+ Objects.requireNonNull(listener);
+ mSessionModificationListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link SessionModificationListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeSessionModificationListener(@Nullable SessionModificationListener listener) {
+ if (listener != null) {
+ mSessionModificationListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a new {@link VideoEventListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addVideoEventListener(@NonNull VideoEventListener listener) {
+ Objects.requireNonNull(listener);
+ mVideoEventListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link VideoEventListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeVideoEventListener(@Nullable VideoEventListener listener) {
+ if (listener != null) {
+ mVideoEventListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Adds a new {@link SurfaceChangeListener}.
+ *
+ * @param listener The listener.
+ */
+ public void addSurfaceChangeListener(@NonNull SurfaceChangeListener listener) {
+ Objects.requireNonNull(listener);
+ mSurfaceChangeListeners.add(listener);
+ }
+
+ /**
+ * Remove a {@link SurfaceChangeListener}.
+ *
+ * @param listener The listener.
+ */
+ public void removeSurfaceChangeListener(@Nullable SurfaceChangeListener listener) {
+ if (listener != null) {
+ mSurfaceChangeListeners.remove(listener);
+ }
+ }
+
+ /**
+ * Inform listeners of an upgrade to video request for a call.
+ *
+ * @param call The call.
+ * @param videoState The video state we want to upgrade to.
+ */
+ public void upgradeToVideoRequest(DialerCall call, int videoState) {
+ LogUtil.v(
+ "InCallVideoCallCallbackNotifier.upgradeToVideoRequest",
+ "call = " + call + " new video state = " + videoState);
+ for (SessionModificationListener listener : mSessionModificationListeners) {
+ listener.onUpgradeToVideoRequest(call, videoState);
+ }
+ }
+
+ /**
+ * Inform listeners of a call session event.
+ *
+ * @param event The call session event.
+ */
+ public void callSessionEvent(int event) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onCallSessionEvent(event);
+ }
+ }
+
+ /**
+ * Inform listeners of a downgrade to audio.
+ *
+ * @param call The call.
+ * @param paused The paused state.
+ */
+ public void peerPausedStateChanged(DialerCall call, boolean paused) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onPeerPauseStateChanged(call, paused);
+ }
+ }
+
+ /**
+ * Inform listeners of any change in the video quality of the call
+ *
+ * @param call The call.
+ * @param videoQuality The updated video quality of the call.
+ */
+ public void videoQualityChanged(DialerCall call, int videoQuality) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onVideoQualityChanged(call, videoQuality);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to peer dimensions.
+ *
+ * @param call The call.
+ * @param width New peer width.
+ * @param height New peer height.
+ */
+ public void peerDimensionsChanged(DialerCall call, int width, int height) {
+ for (SurfaceChangeListener listener : mSurfaceChangeListeners) {
+ listener.onUpdatePeerDimensions(call, width, height);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to camera dimensions.
+ *
+ * @param call The call.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ public void cameraDimensionsChanged(DialerCall call, int width, int height) {
+ for (SurfaceChangeListener listener : mSurfaceChangeListeners) {
+ listener.onCameraDimensionsChange(call, width, height);
+ }
+ }
+
+ /**
+ * Inform listeners of a change to call data usage.
+ *
+ * @param dataUsage data usage value
+ */
+ public void callDataUsageChanged(long dataUsage) {
+ for (VideoEventListener listener : mVideoEventListeners) {
+ listener.onCallDataUsageChange(dataUsage);
+ }
+ }
+
+ /** Listener interface for any class that wants to be notified of upgrade to video request. */
+ public interface SessionModificationListener {
+
+ /**
+ * Called when a peer request is received to upgrade an audio-only call to a video call.
+ *
+ * @param call The call the request was received for.
+ * @param videoState The requested video state.
+ */
+ void onUpgradeToVideoRequest(DialerCall call, int videoState);
+ }
+
+ /**
+ * Listener interface for any class that wants to be notified of video events, including pause and
+ * un-pause of peer video, video quality changes.
+ */
+ public interface VideoEventListener {
+
+ /**
+ * Called when the peer pauses or un-pauses video transmission.
+ *
+ * @param call The call which paused or un-paused video transmission.
+ * @param paused {@code True} when the video transmission is paused, {@code false} otherwise.
+ */
+ void onPeerPauseStateChanged(DialerCall call, boolean paused);
+
+ /**
+ * Called when the video quality changes.
+ *
+ * @param call The call whose video quality changes.
+ * @param videoCallQuality - values are QUALITY_HIGH, MEDIUM, LOW and UNKNOWN.
+ */
+ void onVideoQualityChanged(DialerCall call, int videoCallQuality);
+
+ /*
+ * Called when call data usage value is requested or when call data usage value is updated
+ * because of a call state change
+ *
+ * @param dataUsage call data usage value
+ */
+ void onCallDataUsageChange(long dataUsage);
+
+ /**
+ * Called when call session event is raised.
+ *
+ * @param event The call session event.
+ */
+ void onCallSessionEvent(int event);
+ }
+
+ /**
+ * Listener interface for any class that wants to be notified of changes to the video surfaces.
+ */
+ public interface SurfaceChangeListener {
+
+ /**
+ * Called when the peer video feed changes dimensions. This can occur when the peer rotates
+ * their device, changing the aspect ratio of the video signal.
+ *
+ * @param call The call which experienced a peer video
+ */
+ void onUpdatePeerDimensions(DialerCall call, int width, int height);
+
+ /**
+ * Called when the local camera changes dimensions. This occurs when a change in camera occurs.
+ *
+ * @param call The call which experienced the camera dimension change.
+ * @param width The new camera video width.
+ * @param height The new camera video height.
+ */
+ void onCameraDimensionsChange(DialerCall call, int width, int height);
+ }
+}
diff --git a/java/com/android/incallui/call/TelecomAdapter.java b/java/com/android/incallui/call/TelecomAdapter.java
new file mode 100644
index 000000000..ebf4ecf4f
--- /dev/null
+++ b/java/com/android/incallui/call/TelecomAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * 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.incallui.call;
+
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.telecom.InCallService;
+import com.android.dialer.common.LogUtil;
+import java.util.List;
+
+/** Wrapper around Telecom APIs. */
+public final class TelecomAdapter implements InCallServiceListener {
+
+ private static final String ADD_CALL_MODE_KEY = "add_call_mode";
+
+ private static TelecomAdapter sInstance;
+ private InCallService mInCallService;
+
+ private TelecomAdapter() {}
+
+ @MainThread
+ public static TelecomAdapter getInstance() {
+ if (!Looper.getMainLooper().isCurrentThread()) {
+ throw new IllegalStateException();
+ }
+ if (sInstance == null) {
+ sInstance = new TelecomAdapter();
+ }
+ return sInstance;
+ }
+
+ @Override
+ public void setInCallService(InCallService inCallService) {
+ mInCallService = inCallService;
+ }
+
+ @Override
+ public void clearInCallService() {
+ mInCallService = null;
+ }
+
+ private android.telecom.Call getTelecomCallById(String callId) {
+ DialerCall call = CallList.getInstance().getCallById(callId);
+ return call == null ? null : call.getTelecomCall();
+ }
+
+ public void mute(boolean shouldMute) {
+ if (mInCallService != null) {
+ mInCallService.setMuted(shouldMute);
+ } else {
+ LogUtil.e("TelecomAdapter.mute", "mInCallService is null");
+ }
+ }
+
+ public void setAudioRoute(int route) {
+ if (mInCallService != null) {
+ mInCallService.setAudioRoute(route);
+ } else {
+ LogUtil.e("TelecomAdapter.setAudioRoute", "mInCallService is null");
+ }
+ }
+
+ public void merge(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ List<android.telecom.Call> conferenceable = call.getConferenceableCalls();
+ if (!conferenceable.isEmpty()) {
+ call.conference(conferenceable.get(0));
+ } else {
+ if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE)) {
+ call.mergeConference();
+ }
+ }
+ } else {
+ LogUtil.e("TelecomAdapter.merge", "call not in call list " + callId);
+ }
+ }
+
+ public void swap(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE)) {
+ call.swapConference();
+ }
+ } else {
+ LogUtil.e("TelecomAdapter.swap", "call not in call list " + callId);
+ }
+ }
+
+ public void addCall() {
+ if (mInCallService != null) {
+ Intent intent = new Intent(Intent.ACTION_DIAL);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // when we request the dialer come up, we also want to inform
+ // it that we're going through the "add call" option from the
+ // InCallScreen / PhoneUtils.
+ intent.putExtra(ADD_CALL_MODE_KEY, true);
+ try {
+ LogUtil.d("TelecomAdapter.addCall", "Sending the add DialerCall intent");
+ mInCallService.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // This is rather rare but possible.
+ // Note: this method is used even when the phone is encrypted. At that moment
+ // the system may not find any Activity which can accept this Intent.
+ LogUtil.e("TelecomAdapter.addCall", "Activity for adding calls isn't found.", e);
+ }
+ }
+ }
+
+ public void playDtmfTone(String callId, char digit) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.playDtmfTone(digit);
+ } else {
+ LogUtil.e("TelecomAdapter.playDtmfTone", "call not in call list " + callId);
+ }
+ }
+
+ public void stopDtmfTone(String callId) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.stopDtmfTone();
+ } else {
+ LogUtil.e("TelecomAdapter.stopDtmfTone", "call not in call list " + callId);
+ }
+ }
+
+ public void postDialContinue(String callId, boolean proceed) {
+ android.telecom.Call call = getTelecomCallById(callId);
+ if (call != null) {
+ call.postDialContinue(proceed);
+ } else {
+ LogUtil.e("TelecomAdapter.postDialContinue", "call not in call list " + callId);
+ }
+ }
+
+ public boolean canAddCall() {
+ if (mInCallService != null) {
+ return mInCallService.canAddCall();
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/incallui/call/VideoUtils.java b/java/com/android/incallui/call/VideoUtils.java
new file mode 100644
index 000000000..80fbfb1cc
--- /dev/null
+++ b/java/com/android/incallui/call/VideoUtils.java
@@ -0,0 +1,151 @@
+/*
+ * 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.incallui.call;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.telecom.VideoProfile;
+import com.android.dialer.compat.CompatUtils;
+import com.android.dialer.util.DialerUtils;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Objects;
+
+public class VideoUtils {
+
+ private static final String PREFERENCE_CAMERA_ALLOWED_BY_USER = "camera_allowed_by_user";
+
+ public static boolean isVideoCall(@Nullable DialerCall call) {
+ return call != null && isVideoCall(call.getVideoState());
+ }
+
+ public static boolean isVideoCall(int videoState) {
+ return CompatUtils.isVideoCompatible()
+ && (VideoProfile.isTransmissionEnabled(videoState)
+ || VideoProfile.isReceptionEnabled(videoState));
+ }
+
+ public static boolean hasSentVideoUpgradeRequest(@Nullable DialerCall call) {
+ return call != null && hasSentVideoUpgradeRequest(call.getSessionModificationState());
+ }
+
+ public static boolean hasSentVideoUpgradeRequest(@SessionModificationState int state) {
+ return state == DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE
+ || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED
+ || state == DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED
+ || state == DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT;
+ }
+
+ public static boolean hasReceivedVideoUpgradeRequest(@Nullable DialerCall call) {
+ return call != null && hasReceivedVideoUpgradeRequest(call.getSessionModificationState());
+ }
+
+ public static boolean hasReceivedVideoUpgradeRequest(@SessionModificationState int state) {
+ return state == DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST;
+ }
+
+ public static boolean isBidirectionalVideoCall(DialerCall call) {
+ return CompatUtils.isVideoCompatible() && VideoProfile.isBidirectional(call.getVideoState());
+ }
+
+ public static boolean isTransmissionEnabled(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return false;
+ }
+
+ return VideoProfile.isTransmissionEnabled(call.getVideoState());
+ }
+
+ public static boolean isIncomingVideoCall(DialerCall call) {
+ if (!VideoUtils.isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return (state == DialerCall.State.INCOMING) || (state == DialerCall.State.CALL_WAITING);
+ }
+
+ public static boolean isActiveVideoCall(DialerCall call) {
+ return VideoUtils.isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ public static boolean isOutgoingVideoCall(DialerCall call) {
+ if (!VideoUtils.isVideoCall(call)) {
+ return false;
+ }
+ final int state = call.getState();
+ return DialerCall.State.isDialing(state)
+ || state == DialerCall.State.CONNECTING
+ || state == DialerCall.State.SELECT_PHONE_ACCOUNT;
+ }
+
+ public static boolean isAudioCall(DialerCall call) {
+ if (!CompatUtils.isVideoCompatible()) {
+ return true;
+ }
+
+ return call != null && VideoProfile.isAudioOnly(call.getVideoState());
+ }
+
+ // TODO (ims-vt) Check if special handling is needed for CONF calls.
+ public static boolean canVideoPause(DialerCall call) {
+ return isVideoCall(call) && call.getState() == DialerCall.State.ACTIVE;
+ }
+
+ public static VideoProfile makeVideoPauseProfile(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ if (VideoProfile.isAudioOnly(call.getVideoState())) {
+ throw new IllegalStateException();
+ }
+ return new VideoProfile(getPausedVideoState(call.getVideoState()));
+ }
+
+ public static VideoProfile makeVideoUnPauseProfile(@NonNull DialerCall call) {
+ Objects.requireNonNull(call);
+ return new VideoProfile(getUnPausedVideoState(call.getVideoState()));
+ }
+
+ public static int getUnPausedVideoState(int videoState) {
+ return videoState & (~VideoProfile.STATE_PAUSED);
+ }
+
+ public static int getPausedVideoState(int videoState) {
+ return videoState | VideoProfile.STATE_PAUSED;
+ }
+
+ public static boolean hasCameraPermissionAndAllowedByUser(@NonNull Context context) {
+ return isCameraAllowedByUser(context) && hasCameraPermission(context);
+ }
+
+ public static boolean hasCameraPermission(@NonNull Context context) {
+ return ContextCompat.checkSelfPermission(context, android.Manifest.permission.CAMERA)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ public static boolean isCameraAllowedByUser(@NonNull Context context) {
+ return DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
+ .getBoolean(PREFERENCE_CAMERA_ALLOWED_BY_USER, false);
+ }
+
+ public static void setCameraAllowedByUser(@NonNull Context context) {
+ DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
+ .edit()
+ .putBoolean(PREFERENCE_CAMERA_ALLOWED_BY_USER, true)
+ .apply();
+ }
+}
diff --git a/java/com/android/incallui/commontheme/AndroidManifest.xml b/java/com/android/incallui/commontheme/AndroidManifest.xml
new file mode 100644
index 000000000..1d5914f07
--- /dev/null
+++ b/java/com/android/incallui/commontheme/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.commontheme">
+</manifest>
diff --git a/java/com/android/incallui/commontheme/res/animator/button_state.xml b/java/com/android/incallui/commontheme/res/animator/button_state.xml
new file mode 100644
index 000000000..70958d610
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/animator/button_state.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:state_pressed="true" android:state_enabled="true">
+ <set>
+ <objectAnimator android:propertyName="translationZ"
+ android:duration="100"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+ <objectAnimator android:propertyName="elevation"
+ android:duration="0"
+ android:valueTo="@dimen/incall_call_button_elevation"
+ android:valueType="floatType"/>
+ </set>
+ </item>
+ <!-- base state -->
+ <item android:state_enabled="true">
+ <set>
+ <objectAnimator android:propertyName="translationZ"
+ android:duration="100"
+ android:valueTo="0"
+ android:startDelay="100"
+ android:valueType="floatType"/>
+ <objectAnimator android:propertyName="elevation"
+ android:duration="0"
+ android:valueTo="@dimen/incall_call_button_elevation"
+ android:valueType="floatType" />
+ </set>
+ </item>
+ ...
+</selector> \ No newline at end of file
diff --git a/java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml b/java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml
new file mode 100644
index 000000000..8d78f0017
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/animator/disabled_alpha.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item android:state_enabled="false">
+ <set>
+ <objectAnimator
+ android:propertyName="alpha"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:valueTo=".3f"
+ android:valueType="floatType"/>
+ </set>
+ </item>
+ <item>
+ <set>
+ <objectAnimator
+ android:propertyName="alpha"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:valueTo="1f"
+ android:valueType="floatType"/>
+ </set>
+ </item>
+</selector>
diff --git a/java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml b/java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml
new file mode 100644
index 000000000..cd474c5e5
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/color/incall_button_ripple.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#80888888" android:state_checked="true"/>
+ <item android:color="#80ffffff"/>
+</selector>
diff --git a/java/com/android/incallui/commontheme/res/color/incall_button_white.xml b/java/com/android/incallui/commontheme/res/color/incall_button_white.xml
new file mode 100644
index 000000000..5df441ff0
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/color/incall_button_white.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="@android:color/white" android:state_enabled="true"/>
+ <item android:color="#99ffffff" android:state_enabled="false"/>
+</selector>
diff --git a/java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..26f3fe001
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-hdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..5b0a9d663
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-mdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..d595b190d
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-xhdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..fb7cf161b
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-xxhdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png b/java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png
new file mode 100644
index 000000000..4bb58d9f5
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable-xxxhdpi/ic_phone_audio_white_36dp.png
Binary files differ
diff --git a/java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml b/java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml
new file mode 100644
index 000000000..090506aa6
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable/answer_answer_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80FFFFFF">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="#09ad00"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml b/java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml
new file mode 100644
index 000000000..abfd56ecf
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable/answer_decline_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80FFFFFF">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="#DF0000"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml b/java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml
new file mode 100644
index 000000000..3c9f4bc0b
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/drawable/incall_end_call_background.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80FFFFFF">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="#FFDF0000"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml b/java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml
new file mode 100644
index 000000000..e1390597a
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values-w260dp-h520dp/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="incall_end_call_button_size">64dp</dimen>
+ <drawable name="incall_end_call_icon">@drawable/quantum_ic_call_end_white_36</drawable>
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml b/java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml
new file mode 100644
index 000000000..e1390597a
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values-w520dp-h260dp-land/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="incall_end_call_button_size">64dp</dimen>
+ <drawable name="incall_end_call_icon">@drawable/quantum_ic_call_end_white_36</drawable>
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values/colors.xml b/java/com/android/incallui/commontheme/res/values/colors.xml
new file mode 100644
index 000000000..d38e34716
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/colors.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- 50% black background drawn over the video to make it easier to see text and buttons. -->
+ <color name="videocall_overlay_background_color">#7E000000</color>
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/commontheme/res/values/dimens.xml b/java/com/android/incallui/commontheme/res/values/dimens.xml
new file mode 100644
index 000000000..649ba2cde
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/dimens.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="incall_end_call_button_size">48dp</dimen>
+ <dimen name="incall_call_button_elevation">8dp</dimen>
+ <drawable name="incall_end_call_icon">@drawable/quantum_ic_call_end_white_24</drawable>
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values/strings.xml b/java/com/android/incallui/commontheme/res/values/strings.xml
new file mode 100644
index 000000000..6f346a34d
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/strings.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="incall_content_description_end_call">End call</string>
+
+ <string name="incall_content_description_muted">Muted</string>
+
+ <string name="incall_content_description_unmuted">Unmuted</string>
+
+ <string name="incall_content_description_swap_calls">Swap calls</string>
+
+ <string name="incall_content_description_merge_calls">Merge calls</string>
+
+ <string name="incall_content_description_earpiece">Handset earpiece</string>
+
+ <string name="incall_content_description_speaker">Speaker</string>
+
+ <string name="incall_content_description_bluetooth">Bluetooth</string>
+
+ <string name="incall_content_description_headset">Wired headset</string>
+
+ <!-- Text for the onscreen "Hold" button when it is not selected. Pressing it will put
+ the call on hold. -->
+ <string name="incall_content_description_hold">Hold call</string>
+ <!-- Text for the onscreen "Hold" button when it is selected. Pressing it will resume
+ the call from a previously held state. -->
+ <string name="incall_content_description_unhold">Resume call</string>
+
+ <string name="incall_content_description_video_on">Video on</string>
+
+ <string name="incall_content_description_video_off">Video off</string>
+
+ <string name="incall_content_description_swap_video">Swap video</string>
+
+</resources>
diff --git a/java/com/android/incallui/commontheme/res/values/styles.xml b/java/com/android/incallui/commontheme/res/values/styles.xml
new file mode 100644
index 000000000..311f9cf4b
--- /dev/null
+++ b/java/com/android/incallui/commontheme/res/values/styles.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+
+ <style name="Dialer.Incall.TextAppearance.Large">
+ <item name="android:textColor">?android:textColorPrimary</item>
+ <item name="android:textSize">36sp</item>
+ <item name="android:fontFamily">sans-serif-light</item>
+ </style>
+
+ <style name="Dialer.Incall.TextAppearance.Label">
+ <item name="android:textColor">?android:textColorPrimary</item>
+ <item name="android:textSize">12sp</item>
+ </style>
+
+ <style name="Dialer.Incall.TextAppearance" parent="android:TextAppearance.Material">
+ <item name="android:textColor">?android:textColorSecondary</item>
+ <item name="android:textSize">18sp</item>
+ </style>
+
+ <style name="Incall.Button.End" parent="android:Widget.Material.Button">
+ <item name="android:background">@drawable/incall_end_call_background</item>
+ <item name="android:elevation">8dp</item>
+ <item name="android:layout_height">@dimen/incall_end_call_button_size</item>
+ <item name="android:layout_width">@dimen/incall_end_call_button_size</item>
+ <item name="android:padding">8dp</item>
+ <item name="android:src">@drawable/incall_end_call_icon</item>
+ <item name="android:stateListAnimator">@animator/disabled_alpha</item>
+ </style>
+
+ <style name="Answer.Button" parent="android:Widget.Material.Button">
+ <item name="android:stateListAnimator">@animator/button_state</item>
+ </style>
+
+ <style name="Answer.Button.Answer">
+ <item name="android:background">@drawable/answer_answer_background</item>
+ </style>
+
+ <style name="Answer.Button.Decline">
+ <item name="android:background">@drawable/answer_decline_background</item>
+ </style>
+
+</resources>
diff --git a/java/com/android/incallui/contactgrid/AndroidManifest.xml b/java/com/android/incallui/contactgrid/AndroidManifest.xml
new file mode 100644
index 000000000..520010548
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.contactgrid">
+</manifest>
diff --git a/java/com/android/incallui/contactgrid/BottomRow.java b/java/com/android/incallui/contactgrid/BottomRow.java
new file mode 100644
index 000000000..aaf7e8214
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/BottomRow.java
@@ -0,0 +1,142 @@
+/*
+ * 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.incallui.contactgrid;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+
+/**
+ * Gets the content of the bottom row. For example:
+ *
+ * <ul>
+ * <li>Mobile +1 (650) 253-0000
+ * <li>[HD icon] 00:15
+ * <li>Call ended
+ * <li>Hanging up
+ * </ul>
+ */
+public class BottomRow {
+
+ /** Content of the bottom row. */
+ public static class Info {
+
+ @Nullable public final CharSequence label;
+ public final boolean isTimerVisible;
+ public final boolean isWorkIconVisible;
+ public final boolean isHdIconVisible;
+ public final boolean isForwardIconVisible;
+ public final boolean isSpamIconVisible;
+ public final boolean shouldPopulateAccessibilityEvent;
+
+ public Info(
+ @Nullable CharSequence label,
+ boolean isTimerVisible,
+ boolean isWorkIconVisible,
+ boolean isHdIconVisible,
+ boolean isForwardIconVisible,
+ boolean isSpamIconVisible,
+ boolean shouldPopulateAccessibilityEvent) {
+ this.label = label;
+ this.isTimerVisible = isTimerVisible;
+ this.isWorkIconVisible = isWorkIconVisible;
+ this.isHdIconVisible = isHdIconVisible;
+ this.isForwardIconVisible = isForwardIconVisible;
+ this.isSpamIconVisible = isSpamIconVisible;
+ this.shouldPopulateAccessibilityEvent = shouldPopulateAccessibilityEvent;
+ }
+ }
+
+ private BottomRow() {}
+
+ public static Info getInfo(Context context, PrimaryCallState state, PrimaryInfo primaryInfo) {
+ CharSequence label;
+ boolean isTimerVisible = state.state == State.ACTIVE;
+ boolean isForwardIconVisible = state.isForwardedNumber;
+ boolean isWorkIconVisible = state.isWorkCall;
+ boolean isHdIconVisible = state.isHdAudioCall && !isForwardIconVisible;
+ boolean isSpamIconVisible = false;
+ boolean shouldPopulateAccessibilityEvent = true;
+
+ if (isIncoming(state) && primaryInfo.isSpam) {
+ label = context.getString(R.string.contact_grid_incoming_suspected_spam);
+ isSpamIconVisible = true;
+ isHdIconVisible = false;
+ } else if (state.state == State.DISCONNECTING) {
+ // While in the DISCONNECTING state we display a "Hanging up" message in order to make the UI
+ // feel more responsive. (In GSM it's normal to see a delay of a couple of seconds while
+ // negotiating the disconnect with the network, so the "Hanging up" state at least lets the
+ // user know that we're doing something. This state is currently not used with CDMA.)
+ label = context.getString(R.string.incall_hanging_up);
+ } else if (state.state == State.DISCONNECTED) {
+ label = state.disconnectCause.getLabel();
+ if (TextUtils.isEmpty(label)) {
+ label = context.getString(R.string.incall_call_ended);
+ }
+ } else if (!TextUtils.isEmpty(state.callbackNumber)) {
+ // This is used for carriers like Project Fi to show the callback number for emergency calls.
+ label =
+ context.getString(
+ R.string.contact_grid_callback_number,
+ PhoneNumberUtils.formatNumber(state.callbackNumber));
+ isTimerVisible = false;
+ } else {
+ label = getLabelForPhoneNumber(primaryInfo);
+ shouldPopulateAccessibilityEvent = primaryInfo.nameIsNumber;
+ }
+
+ return new Info(
+ label,
+ isTimerVisible,
+ isWorkIconVisible,
+ isHdIconVisible,
+ isForwardIconVisible,
+ isSpamIconVisible,
+ shouldPopulateAccessibilityEvent);
+ }
+
+ private static CharSequence getLabelForPhoneNumber(PrimaryInfo primaryInfo) {
+ if (primaryInfo.nameIsNumber) {
+ return primaryInfo.location;
+ }
+ if (!TextUtils.isEmpty(primaryInfo.number)) {
+ CharSequence spannedNumber = spanDisplayNumber(primaryInfo.number);
+ if (primaryInfo.label == null) {
+ return spannedNumber;
+ } else {
+ return TextUtils.concat(primaryInfo.label, " ", spannedNumber);
+ }
+ }
+ return null;
+ }
+
+ private static CharSequence spanDisplayNumber(String displayNumber) {
+ return PhoneNumberUtilsCompat.createTtsSpannable(
+ BidiFormatter.getInstance().unicodeWrap(displayNumber, TextDirectionHeuristics.LTR));
+ }
+
+ private static boolean isIncoming(PrimaryCallState state) {
+ return state.state == State.INCOMING || state.state == State.CALL_WAITING;
+ }
+}
diff --git a/java/com/android/incallui/contactgrid/ContactGridManager.java b/java/com/android/incallui/contactgrid/ContactGridManager.java
new file mode 100644
index 000000000..81c225163
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/ContactGridManager.java
@@ -0,0 +1,315 @@
+/*
+ * 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.incallui.contactgrid;
+
+import android.content.Context;
+import android.os.SystemClock;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.Chronometer;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.ViewAnimator;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.contacts.common.lettertiles.LetterTileDrawable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.util.DrawableConverter;
+import com.android.incallui.incall.protocol.ContactPhotoType;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import java.util.List;
+
+/** Utility to manage the Contact grid */
+public class ContactGridManager {
+
+ private final Context context;
+ private final View contactGridLayout;
+
+ // Row 0: Captain Holt ON HOLD
+ // Row 0: Calling...
+ // Row 0: [Wi-Fi icon] Calling via Starbucks Wi-Fi
+ // Row 0: [Wi-Fi icon] Starbucks Wi-Fi
+ // Row 0: Hey Jake, pick up!
+ private ImageView connectionIconImageView;
+ private TextView statusTextView;
+
+ // Row 1: Jake Peralta [Contact photo]
+ // Row 1: Walgreens
+ // Row 1: +1 (650) 253-0000
+ private TextView contactNameTextView;
+ @Nullable private ImageView avatarImageView;
+
+ // Row 2: Mobile +1 (650) 253-0000
+ // Row 2: [HD icon] 00:15
+ // Row 2: Call ended
+ // Row 2: Hanging up
+ // Row 2: [Alert sign] Suspected spam caller
+ // Row 2: Your emergency callback number: +1 (650) 253-0000
+ private ImageView workIconImageView;
+ private ImageView hdIconImageView;
+ private ImageView forwardIconImageView;
+ private ImageView spamIconImageView;
+ private ViewAnimator bottomTextSwitcher;
+ private TextView bottomTextView;
+ private Chronometer bottomTimerView;
+ private int avatarSize;
+ private boolean hideAvatar;
+ private boolean showAnonymousAvatar;
+ private boolean middleRowVisible = true;
+
+ private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo();
+ private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState();
+ private final LetterTileDrawable letterTile;
+
+
+ public ContactGridManager(
+ View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
+ context = view.getContext();
+ Assert.isNotNull(context);
+
+ this.avatarImageView = avatarImageView;
+ this.avatarSize = avatarSize;
+ this.showAnonymousAvatar = showAnonymousAvatar;
+ connectionIconImageView = (ImageView) view.findViewById(R.id.contactgrid_connection_icon);
+ statusTextView = (TextView) view.findViewById(R.id.contactgrid_status_text);
+ contactNameTextView = (TextView) view.findViewById(R.id.contactgrid_contact_name);
+ workIconImageView = (ImageView) view.findViewById(R.id.contactgrid_workIcon);
+ hdIconImageView = (ImageView) view.findViewById(R.id.contactgrid_hdIcon);
+ forwardIconImageView = (ImageView) view.findViewById(R.id.contactgrid_forwardIcon);
+ spamIconImageView = (ImageView) view.findViewById(R.id.contactgrid_spamIcon);
+ bottomTextSwitcher = (ViewAnimator) view.findViewById(R.id.contactgrid_bottom_text_switcher);
+ bottomTextView = (TextView) view.findViewById(R.id.contactgrid_bottom_text);
+ bottomTimerView = (Chronometer) view.findViewById(R.id.contactgrid_bottom_timer);
+
+ contactGridLayout = (View) contactNameTextView.getParent();
+ letterTile = new LetterTileDrawable(context.getResources());
+ }
+
+ public void show() {
+ contactGridLayout.setVisibility(View.VISIBLE);
+ }
+
+ public void hide() {
+ contactGridLayout.setVisibility(View.GONE);
+ }
+
+ public void setAvatarHidden(boolean hide) {
+ if (hide != hideAvatar) {
+ hideAvatar = hide;
+ updatePrimaryNameAndPhoto();
+ }
+ }
+
+ public boolean isAvatarHidden() {
+ return hideAvatar;
+ }
+
+ public View getContainerView() {
+ return contactGridLayout;
+ }
+
+ public void setIsMiddleRowVisible(boolean isMiddleRowVisible) {
+ if (middleRowVisible == isMiddleRowVisible) {
+ return;
+ }
+ middleRowVisible = isMiddleRowVisible;
+
+ contactNameTextView.setVisibility(isMiddleRowVisible ? View.VISIBLE : View.GONE);
+ updateAvatarVisibility();
+ }
+
+ public void setPrimary(PrimaryInfo primaryInfo) {
+ this.primaryInfo = primaryInfo;
+ updatePrimaryNameAndPhoto();
+ updateBottomRow();
+ }
+
+ public void setCallState(PrimaryCallState primaryCallState) {
+ this.primaryCallState = primaryCallState;
+ updatePrimaryNameAndPhoto();
+ updateBottomRow();
+ updateTopRow();
+ }
+
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ dispatchPopulateAccessibilityEvent(event, statusTextView);
+ dispatchPopulateAccessibilityEvent(event, contactNameTextView);
+ BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo);
+ if (info.shouldPopulateAccessibilityEvent) {
+ dispatchPopulateAccessibilityEvent(event, bottomTextView);
+ }
+ }
+
+ public void setAvatarImageView(
+ @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) {
+ this.avatarImageView = avatarImageView;
+ this.avatarSize = avatarSize;
+ this.showAnonymousAvatar = showAnonymousAvatar;
+ updatePrimaryNameAndPhoto();
+ }
+
+ private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
+ final List<CharSequence> eventText = event.getText();
+ int size = eventText.size();
+ view.dispatchPopulateAccessibilityEvent(event);
+ // If no text added write null to keep relative position.
+ if (size == eventText.size()) {
+ eventText.add(null);
+ }
+ }
+
+ private boolean updateAvatarVisibility() {
+ if (avatarImageView == null) {
+ return false;
+ }
+
+ if (!middleRowVisible) {
+ avatarImageView.setVisibility(View.GONE);
+ return false;
+ }
+
+ boolean hasPhoto =
+ primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
+ if (!hasPhoto && !showAnonymousAvatar) {
+ avatarImageView.setVisibility(View.GONE);
+ return false;
+ }
+
+ avatarImageView.setVisibility(View.VISIBLE);
+ return true;
+ }
+
+ /**
+ * Updates row 0. For example:
+ *
+ * <ul>
+ * <li>Captain Holt ON HOLD
+ * <li>Calling...
+ * <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi
+ * <li>[Wi-Fi icon] Starbucks Wi-Fi
+ * <li>Call from
+ * </ul>
+ */
+ private void updateTopRow() {
+ TopRow.Info info = TopRow.getInfo(context, primaryCallState);
+ if (TextUtils.isEmpty(info.label)) {
+ // Use INVISIBLE here to prevent the rows below this one from moving up and down.
+ statusTextView.setVisibility(View.INVISIBLE);
+ statusTextView.setText(null);
+ } else {
+ statusTextView.setText(info.label);
+ statusTextView.setVisibility(View.VISIBLE);
+ statusTextView.setSingleLine(info.labelIsSingleLine);
+ }
+
+ if (info.icon == null) {
+ connectionIconImageView.setVisibility(View.GONE);
+ } else {
+ connectionIconImageView.setVisibility(View.VISIBLE);
+ connectionIconImageView.setImageDrawable(info.icon);
+ }
+ }
+
+ /**
+ * Updates row 1. For example:
+ *
+ * <ul>
+ * <li>Jake Peralta [Contact photo]
+ * <li>Walgreens
+ * <li>+1 (650) 253-0000
+ * </ul>
+ */
+ private void updatePrimaryNameAndPhoto() {
+ if (TextUtils.isEmpty(primaryInfo.name)) {
+ contactNameTextView.setText(null);
+ } else {
+ contactNameTextView.setText(
+ primaryInfo.nameIsNumber
+ ? PhoneNumberUtilsCompat.createTtsSpannable(primaryInfo.name)
+ : primaryInfo.name);
+
+ // Set direction of the name field
+ int nameDirection = View.TEXT_DIRECTION_INHERIT;
+ if (primaryInfo.nameIsNumber) {
+ nameDirection = View.TEXT_DIRECTION_LTR;
+ }
+ contactNameTextView.setTextDirection(nameDirection);
+ }
+
+ if (avatarImageView != null) {
+ if (hideAvatar) {
+ avatarImageView.setVisibility(View.GONE);
+ } else if (avatarImageView != null && avatarSize > 0 && updateAvatarVisibility()) {
+ boolean hasPhoto =
+ primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT;
+ // Contact has a photo, don't render a letter tile.
+ if (hasPhoto) {
+ avatarImageView.setBackground(
+ DrawableConverter.getRoundedDrawable(
+ context, primaryInfo.photo, avatarSize, avatarSize));
+ // Contact has a name, that isn't a number.
+ } else {
+ int contactType =
+ primaryCallState.isVoiceMailNumber
+ ? LetterTileDrawable.TYPE_VOICEMAIL
+ : LetterTileDrawable.TYPE_DEFAULT;
+ letterTile.setCanonicalDialerLetterTileDetails(
+ primaryInfo.name,
+ primaryInfo.contactInfoLookupKey,
+ LetterTileDrawable.SHAPE_CIRCLE,
+ contactType);
+ avatarImageView.setBackground(letterTile);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates row 2. For example:
+ *
+ * <ul>
+ * <li>Mobile +1 (650) 253-0000
+ * <li>[HD icon] 00:15
+ * <li>Call ended
+ * <li>Hanging up
+ * </ul>
+ */
+ private void updateBottomRow() {
+ BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo);
+
+ bottomTextView.setText(info.label);
+ bottomTextView.setAllCaps(info.isSpamIconVisible);
+ workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE);
+ hdIconImageView.setVisibility(info.isHdIconVisible ? View.VISIBLE : View.GONE);
+ forwardIconImageView.setVisibility(info.isForwardIconVisible ? View.VISIBLE : View.GONE);
+ spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE);
+
+ if (info.isTimerVisible) {
+ bottomTextSwitcher.setDisplayedChild(1);
+ bottomTimerView.setBase(
+ primaryCallState.connectTimeMillis
+ - System.currentTimeMillis()
+ + SystemClock.elapsedRealtime());
+ bottomTimerView.start();
+ } else {
+ bottomTextSwitcher.setDisplayedChild(0);
+ bottomTimerView.stop();
+ }
+ }
+}
diff --git a/java/com/android/incallui/contactgrid/TopRow.java b/java/com/android/incallui/contactgrid/TopRow.java
new file mode 100644
index 000000000..a340fd0a0
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/TopRow.java
@@ -0,0 +1,168 @@
+/*
+ * 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.incallui.contactgrid;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.State;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+
+/**
+ * Gets the content of the top row. For example:
+ *
+ * <ul>
+ * <li>Captain Holt ON HOLD
+ * <li>Calling...
+ * <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi
+ * <li>[Wi-Fi icon] Starbucks Wi-Fi
+ * <li>Call from
+ * </ul>
+ */
+public class TopRow {
+
+ /** Content of the top row. */
+ public static class Info {
+
+ @Nullable public final CharSequence label;
+ @Nullable public final Drawable icon;
+ public final boolean labelIsSingleLine;
+
+ public Info(@Nullable CharSequence label, @Nullable Drawable icon, boolean labelIsSingleLine) {
+ this.label = label;
+ this.icon = icon;
+ this.labelIsSingleLine = labelIsSingleLine;
+ }
+ }
+
+ private TopRow() {}
+
+ public static Info getInfo(Context context, PrimaryCallState state) {
+ CharSequence label = null;
+ Drawable icon = state.connectionIcon;
+ boolean labelIsSingleLine = true;
+
+ if (state.isWifi && icon == null) {
+ icon = context.getDrawable(R.drawable.quantum_ic_network_wifi_white_24);
+ }
+
+ if (state.state == State.INCOMING || state.state == State.CALL_WAITING) {
+ // Call from
+ // [Wi-Fi icon] Video call from
+ // Hey Jake, pick up!
+ if (!TextUtils.isEmpty(state.callSubject)) {
+ label = state.callSubject;
+ labelIsSingleLine = false;
+ } else {
+ label = getLabelForIncoming(context, state);
+ }
+ } else if (VideoUtils.hasSentVideoUpgradeRequest(state.sessionModificationState)
+ || VideoUtils.hasReceivedVideoUpgradeRequest(state.sessionModificationState)) {
+ label = getLabelForVideoRequest(context, state);
+ } else if (state.state == State.PULLING) {
+ label = context.getString(R.string.incall_transferring);
+ } else if (state.state == State.DIALING || state.state == State.CONNECTING) {
+ // [Wi-Fi icon] Calling via Google Guest
+ // Calling...
+ label = getLabelForDialing(context, state);
+ } else if (state.state == State.ACTIVE && state.isRemotelyHeld) {
+ label = context.getString(R.string.incall_remotely_held);
+ } else {
+ // Video calling...
+ // [Wi-Fi icon] Starbucks Wi-Fi
+ label = getConnectionLabel(state);
+ }
+
+ return new Info(label, icon, labelIsSingleLine);
+ }
+
+ private static CharSequence getLabelForIncoming(Context context, PrimaryCallState state) {
+ if (VideoUtils.isVideoCall(state.videoState)) {
+ return getLabelForIncomingVideo(context, state.isWifi);
+ } else if (state.isWifi && !TextUtils.isEmpty(state.connectionLabel)) {
+ return state.connectionLabel;
+ } else if (isAccount(state)) {
+ return context.getString(R.string.contact_grid_incoming_via_template, state.connectionLabel);
+ } else if (state.isWorkCall) {
+ return context.getString(R.string.contact_grid_incoming_work_call);
+ } else {
+ return context.getString(R.string.contact_grid_incoming_voice_call);
+ }
+ }
+
+ private static CharSequence getLabelForIncomingVideo(Context context, boolean isWifi) {
+ if (isWifi) {
+ return context.getString(R.string.contact_grid_incoming_wifi_video_call);
+ } else {
+ return context.getString(R.string.contact_grid_incoming_video_call);
+ }
+ }
+
+ private static CharSequence getLabelForDialing(Context context, PrimaryCallState state) {
+ if (!TextUtils.isEmpty(state.connectionLabel) && !state.isWifi) {
+ return context.getString(R.string.incall_calling_via_template, state.connectionLabel);
+ } else {
+ if (VideoUtils.isVideoCall(state.videoState)) {
+ if (state.isWifi) {
+ return context.getString(R.string.incall_wifi_video_call_requesting);
+ } else {
+ return context.getString(R.string.incall_video_call_requesting);
+ }
+ }
+ return context.getString(R.string.incall_connecting);
+ }
+ }
+
+ private static CharSequence getConnectionLabel(PrimaryCallState state) {
+ if (!TextUtils.isEmpty(state.connectionLabel)
+ && (isAccount(state) || state.isWifi || state.isConference)) {
+ // We normally don't show a "call state label" at all when active
+ // (but we can use the call state label to display the provider name).
+ return state.connectionLabel;
+ } else {
+ return null;
+ }
+ }
+
+ private static CharSequence getLabelForVideoRequest(Context context, PrimaryCallState state) {
+ switch (state.sessionModificationState) {
+ case DialerCall.SESSION_MODIFICATION_STATE_WAITING_FOR_UPGRADE_TO_VIDEO_RESPONSE:
+ return context.getString(R.string.incall_video_call_requesting);
+ case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_FAILED:
+ case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_FAILED:
+ return context.getString(R.string.incall_video_call_request_failed);
+ case DialerCall.SESSION_MODIFICATION_STATE_REQUEST_REJECTED:
+ return context.getString(R.string.incall_video_call_request_rejected);
+ case DialerCall.SESSION_MODIFICATION_STATE_UPGRADE_TO_VIDEO_REQUEST_TIMED_OUT:
+ return context.getString(R.string.incall_video_call_request_timed_out);
+ case DialerCall.SESSION_MODIFICATION_STATE_RECEIVED_UPGRADE_TO_VIDEO_REQUEST:
+ return getLabelForIncomingVideo(context, state.isWifi);
+ case DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST:
+ default:
+ Assert.fail();
+ return null;
+ }
+ }
+
+ private static boolean isAccount(PrimaryCallState state) {
+ return !TextUtils.isEmpty(state.connectionLabel) && TextUtils.isEmpty(state.gatewayNumber);
+ }
+}
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
new file mode 100644
index 000000000..3900be556
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_bottom_row.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ tools:showIn="@layout/incall_contact_grid">
+ <ImageView
+ android:id="@id/contactgrid_workIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/ic_work_profile"
+ android:tint="#ffffff"
+ tools:visibility="gone"
+ />
+ <ImageView
+ android:id="@id/contactgrid_hdIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/quantum_ic_hd_white_24"
+ tools:visibility="gone"
+ />
+ <ImageView
+ android:id="@id/contactgrid_forwardIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/quantum_ic_forward_white_24"
+ tools:visibility="gone"
+ />
+ <ImageView
+ android:id="@+id/contactgrid_spamIcon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="8dp"
+ android:scaleType="fitCenter"
+ android:src="@drawable/quantum_ic_report_white_18"
+ tools:visibility="gone"
+ />
+ <ViewAnimator
+ android:id="@+id/contactgrid_bottom_text_switcher"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="2dp"
+ android:measureAllChildren="false">
+ <TextView
+ android:id="@+id/contactgrid_bottom_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ tools:gravity="start"
+ tools:text="Mobile +1 (650) 253-0000"/>
+ <Chronometer
+ android:id="@+id/contactgrid_bottom_timer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ tools:gravity="center"/>
+ </ViewAnimator>
+</LinearLayout>
diff --git a/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml
new file mode 100644
index 000000000..59359c9c1
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/layout/incall_contactgrid_top_row.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal"
+ tools:showIn="@layout/incall_contact_grid">
+ <ImageView
+ android:id="@id/contactgrid_connection_icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_marginEnd="10dp"
+ android:scaleType="fitCenter"
+ tools:src="@android:drawable/sym_def_app_icon"
+ tools:visibility="visible"
+ />
+ <TextView
+ android:id="@id/contactgrid_status_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ tools:text="Captain Holt"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/contactgrid/res/values/ids.xml b/java/com/android/incallui/contactgrid/res/values/ids.xml
new file mode 100644
index 000000000..821dc9d98
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/values/ids.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <item name="contactgrid_connection_icon" type="id"/>
+ <item name="contactgrid_status_text" type="id"/>
+ <item name="contactgrid_contact_name" type="id"/>
+ <item name="contactgrid_workIcon" type="id"/>
+ <item name="contactgrid_hdIcon" type="id"/>
+ <item name="contactgrid_forwardIcon" type="id"/>
+ <item name="contactgrid_spamIcon" type="id"/>
+ <item name="contactgrid_bottom_text" type="id"/>
+ <item name="contactgrid_bottom_timer" type="id"/>
+ <item name="contactgrid_avatar" type="id"/>
+ <item name="contactgrid_top_row" type="id"/>
+ <item name="contactgrid_bottom_row" type="id"/>
+</resources>
diff --git a/java/com/android/incallui/contactgrid/res/values/strings.xml b/java/com/android/incallui/contactgrid/res/values/strings.xml
new file mode 100644
index 000000000..385f843b1
--- /dev/null
+++ b/java/com/android/incallui/contactgrid/res/values/strings.xml
@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Title displayed in the overlay for outgoing calls which include the name of the provider.
+ [CHAR LIMIT=40] -->
+ <string name="incall_calling_via_template">Calling via <xliff:g id="provider_name">%s</xliff:g></string>
+
+ <!-- Displayed above the contact name during an outgoing phone call. Indicates that the call is
+ in the connecting stage. -->
+ <string name="incall_connecting">Calling…</string>
+
+ <!-- Displayed above the contact name when an external call is being pulled to the local
+ device. -->
+ <string name="incall_transferring">Transferring…</string>
+
+ <!-- Displayed above the contact name when the user requests an upgrade from a voice call to a
+ video call. -->
+ <string name="incall_video_call_requesting">Video calling…</string>
+
+ <!-- Displayed above the contact name when the user requests an upgrade from a voice call to a
+ Wi-Fi video call. -->
+ <string name="incall_wifi_video_call_requesting">Wi-Fi video calling…</string>
+
+ <!-- Displayed above the contact name when the user's video upgrade failed due to an unknown
+ reason. -->
+ <string name="incall_video_call_request_failed">Unable to connect</string>
+
+ <!-- Displayed above the contact name when the user's video upgrade was declined by the remote
+ party. -->
+ <string name="incall_video_call_request_rejected">Call declined</string>
+
+ <!-- Displayed above the contact name when no response was received for the user's upgrade
+ requests and we timed out. -->
+ <string name="incall_video_call_request_timed_out">Call timed out</string>
+
+ <!-- In-call screen: status label for a call that's in the process of hanging up
+ [CHAR LIMIT=25] -->
+ <string name="incall_hanging_up">Hanging up</string>
+
+ <!-- In-call screen: status label displayed briefly after a call ends [CHAR LIMIT=25] -->
+ <string name="incall_call_ended">Call ended</string>
+
+ <!-- In-call screen: label shown at the top of the screen when a call is on hold by the remote
+ party [CHAR LIMIT=25] -->
+ <string name="incall_remotely_held">On hold</string>
+
+ <!-- Displayed in the answer call screen for incoming video calls. -->
+ <string name="contact_grid_incoming_video_call">Video call from</string>
+
+ <!-- Displayed in the answer call screen for incoming video calls over Wi-F. -->
+ <string name="contact_grid_incoming_wifi_video_call">Wi-Fi video call from</string>
+
+ <!-- Displayed in the answer call screen for incoming voice calls. -->
+ <string name="contact_grid_incoming_voice_call">Call from</string>
+
+ <!-- Displayed in the answer call screen for incoming voice calls. -->
+ <string name="contact_grid_incoming_work_call">Work call from</string>
+
+ <!-- Displayed in the answer call screen for incoming calls via a phone account. -->
+ <string name="contact_grid_incoming_via_template">Incoming via <xliff:g id="provider_name">%s</xliff:g></string>
+
+ <!-- Displayed in the answer call screen for incoming spam calls. -->
+ <string name="contact_grid_incoming_suspected_spam">Suspected spam caller</string>
+
+ <!-- In-call screen: string shown to the user when their outgoing number is different than the
+ number reported by TelephonyManager#getLine1Number(). This is used for carriers like
+ Project Fi so that users can give their number to emergency responders. -->
+ <string name="contact_grid_callback_number">Callback number: <xliff:g id="dark_number">%1$s</xliff:g></string>
+</resources>
diff --git a/java/com/android/incallui/hold/AndroidManifest.xml b/java/com/android/incallui/hold/AndroidManifest.xml
new file mode 100644
index 000000000..2aedce903
--- /dev/null
+++ b/java/com/android/incallui/hold/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.hold">
+</manifest>
diff --git a/java/com/android/incallui/hold/OnHoldFragment.java b/java/com/android/incallui/hold/OnHoldFragment.java
new file mode 100644
index 000000000..c6952131b
--- /dev/null
+++ b/java/com/android/incallui/hold/OnHoldFragment.java
@@ -0,0 +1,102 @@
+/*
+ * 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.incallui.hold;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.telephony.PhoneNumberUtils;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.transition.TransitionManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+
+/** Shows banner UI for background call */
+public class OnHoldFragment extends Fragment {
+
+ private static final String ARG_INFO = "info";
+ private boolean padTopInset = true;
+ private int topInset;
+
+ public static OnHoldFragment newInstance(@NonNull SecondaryInfo info) {
+ OnHoldFragment fragment = new OnHoldFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(ARG_INFO, info);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ final View view = layoutInflater.inflate(R.layout.incall_on_hold_banner, viewGroup, false);
+
+ SecondaryInfo secondaryInfo = getArguments().getParcelable(ARG_INFO);
+ secondaryInfo = Assert.isNotNull(secondaryInfo);
+
+ ((TextView) view.findViewById(R.id.hold_contact_name))
+ .setText(
+ secondaryInfo.nameIsNumber
+ ? PhoneNumberUtils.createTtsSpannable(
+ BidiFormatter.getInstance()
+ .unicodeWrap(secondaryInfo.name, TextDirectionHeuristics.LTR))
+ : secondaryInfo.name);
+ ((ImageView) view.findViewById(R.id.hold_phone_icon))
+ .setImageResource(
+ secondaryInfo.isVideoCall
+ ? R.drawable.quantum_ic_videocam_white_18
+ : R.drawable.quantum_ic_call_white_18);
+ view.addOnAttachStateChangeListener(
+ new OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ topInset = v.getRootWindowInsets().getSystemWindowInsetTop();
+ applyInset();
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {}
+ });
+ return view;
+ }
+
+ public void setPadTopInset(boolean padTopInset) {
+ this.padTopInset = padTopInset;
+ applyInset();
+ }
+
+ private void applyInset() {
+ if (getView() == null) {
+ return;
+ }
+
+ int newPadding = padTopInset ? topInset : 0;
+ if (newPadding != getView().getPaddingTop()) {
+ TransitionManager.beginDelayedTransition(((ViewGroup) getView().getParent()));
+ getView().setPadding(0, newPadding, 0, 0);
+ }
+ }
+}
diff --git a/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
new file mode 100644
index 000000000..c213af5da
--- /dev/null
+++ b/java/com/android/incallui/hold/res/layout/incall_on_hold_banner.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="#CC212121"
+ android:fitsSystemWindows="true">
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp"
+ android:paddingTop="16dp"
+ android:paddingBottom="16dp"
+ android:gravity="center_vertical">
+
+ <ImageView
+ android:id="@+id/hold_phone_icon"
+ android:layout_width="18dp"
+ android:layout_height="18dp"
+ android:src="@drawable/quantum_ic_call_white_18"
+ android:contentDescription="@null"/>
+
+ <TextView
+ android:id="@+id/hold_contact_name"
+ style="@style/Dialer.Incall.TextAppearance"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="8dp"
+ android:layout_marginEnd="24dp"
+ android:ellipsize="end"
+ android:singleLine="true"
+ android:textColor="@android:color/white"
+ tools:text="Jake Peralta Really Longname"/>
+
+ <TextView
+ style="@style/Dialer.Incall.TextAppearance"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAllCaps="true"
+ android:textColor="@android:color/white"
+ android:text="@string/incall_on_hold"/>
+ </LinearLayout>
+</FrameLayout>
diff --git a/java/com/android/incallui/hold/res/values/strings.xml b/java/com/android/incallui/hold/res/values/strings.xml
new file mode 100644
index 000000000..2e66bcf6c
--- /dev/null
+++ b/java/com/android/incallui/hold/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="incall_on_hold">On hold</string>
+
+</resources>
diff --git a/java/com/android/incallui/incall/bindings/InCallBindings.java b/java/com/android/incallui/incall/bindings/InCallBindings.java
new file mode 100644
index 000000000..8bbbc68e1
--- /dev/null
+++ b/java/com/android/incallui/incall/bindings/InCallBindings.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.incallui.incall.bindings;
+
+import com.android.incallui.incall.impl.InCallFragment;
+import com.android.incallui.incall.protocol.InCallScreen;
+
+/** Bindings for the in call module. */
+public class InCallBindings {
+
+ public static InCallScreen createInCallScreen() {
+ return new InCallFragment();
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/AndroidManifest.xml b/java/com/android/incallui/incall/impl/AndroidManifest.xml
new file mode 100644
index 000000000..a0e3110d8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incall.incall.impl">
+</manifest>
diff --git a/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
new file mode 100644
index 000000000..addebc484
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/AutoValue_MappedButtonConfig_MappingInfo.java
@@ -0,0 +1,135 @@
+/*
+ * 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.incallui.incall.impl;
+
+import javax.annotation.Generated;
+
+@Generated("com.google.auto.value.processor.AutoValueProcessor")
+ final class AutoValue_MappedButtonConfig_MappingInfo extends MappedButtonConfig.MappingInfo {
+
+ private final int slot;
+ private final int slotOrder;
+ private final int conflictOrder;
+
+ private AutoValue_MappedButtonConfig_MappingInfo(
+ int slot,
+ int slotOrder,
+ int conflictOrder) {
+ this.slot = slot;
+ this.slotOrder = slotOrder;
+ this.conflictOrder = conflictOrder;
+ }
+
+ @Override
+ public int getSlot() {
+ return slot;
+ }
+
+ @Override
+ public int getSlotOrder() {
+ return slotOrder;
+ }
+
+ @Override
+ public int getConflictOrder() {
+ return conflictOrder;
+ }
+
+ @Override
+ public String toString() {
+ return "MappingInfo{"
+ + "slot=" + slot + ", "
+ + "slotOrder=" + slotOrder + ", "
+ + "conflictOrder=" + conflictOrder
+ + "}";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (o instanceof MappedButtonConfig.MappingInfo) {
+ MappedButtonConfig.MappingInfo that = (MappedButtonConfig.MappingInfo) o;
+ return (this.slot == that.getSlot())
+ && (this.slotOrder == that.getSlotOrder())
+ && (this.conflictOrder == that.getConflictOrder());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ int h = 1;
+ h *= 1000003;
+ h ^= this.slot;
+ h *= 1000003;
+ h ^= this.slotOrder;
+ h *= 1000003;
+ h ^= this.conflictOrder;
+ return h;
+ }
+
+ static final class Builder extends MappedButtonConfig.MappingInfo.Builder {
+ private Integer slot;
+ private Integer slotOrder;
+ private Integer conflictOrder;
+ Builder() {
+ }
+ private Builder(MappedButtonConfig.MappingInfo source) {
+ this.slot = source.getSlot();
+ this.slotOrder = source.getSlotOrder();
+ this.conflictOrder = source.getConflictOrder();
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo.Builder setSlot(int slot) {
+ this.slot = slot;
+ return this;
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo.Builder setSlotOrder(int slotOrder) {
+ this.slotOrder = slotOrder;
+ return this;
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo.Builder setConflictOrder(int conflictOrder) {
+ this.conflictOrder = conflictOrder;
+ return this;
+ }
+ @Override
+ public MappedButtonConfig.MappingInfo build() {
+ String missing = "";
+ if (this.slot == null) {
+ missing += " slot";
+ }
+ if (this.slotOrder == null) {
+ missing += " slotOrder";
+ }
+ if (this.conflictOrder == null) {
+ missing += " conflictOrder";
+ }
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ return new AutoValue_MappedButtonConfig_MappingInfo(
+ this.slot,
+ this.slotOrder,
+ this.conflictOrder);
+ }
+ }
+
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonChooser.java b/java/com/android/incallui/incall/impl/ButtonChooser.java
new file mode 100644
index 000000000..55b82f015
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/ButtonChooser.java
@@ -0,0 +1,114 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.support.annotation.NonNull;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Determines where logical buttons should be placed in the {@link InCallFragment} based on the
+ * provided mapping.
+ *
+ * <p>The button placement returned by a call to {@link #getButtonPlacement(int, Set)} is created as
+ * follows: one button is placed at each UI slot, using the provided mapping to resolve conflicts.
+ * Any allowed buttons that were not chosen for their desired slot are filled in at the end of the
+ * list until it becomes the proper size.
+ */
+@Immutable
+final class ButtonChooser {
+
+ private final MappedButtonConfig config;
+
+ public ButtonChooser(@NonNull MappedButtonConfig config) {
+ this.config = Assert.isNotNull(config);
+ }
+
+ /**
+ * Returns the buttons that should be shown in the {@link InCallFragment}, ordered appropriately.
+ *
+ * @param numUiButtons the number of ui buttons available.
+ * @param allowedButtons the {@link InCallButtonIds} that can be shown.
+ * @param disabledButtons the {@link InCallButtonIds} that can be shown but in disabled stats.
+ * @return an immutable list whose size is at most {@code numUiButtons}, containing the buttons to
+ * show.
+ */
+ @NonNull
+ public List<Integer> getButtonPlacement(
+ int numUiButtons,
+ @NonNull Set<Integer> allowedButtons,
+ @NonNull Set<Integer> disabledButtons) {
+ Assert.isNotNull(allowedButtons);
+ Assert.checkArgument(numUiButtons >= 0);
+
+ if (numUiButtons == 0 || allowedButtons.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ List<Integer> placedButtons = new ArrayList<>();
+ List<Integer> conflicts = new ArrayList<>();
+ placeButtonsInSlots(numUiButtons, allowedButtons, placedButtons, conflicts);
+ placeConflictsInOpenSlots(
+ numUiButtons, allowedButtons, disabledButtons, placedButtons, conflicts);
+ return Collections.unmodifiableList(placedButtons);
+ }
+
+ private void placeButtonsInSlots(
+ int numUiButtons,
+ @NonNull Set<Integer> allowedButtons,
+ @NonNull List<Integer> placedButtons,
+ @NonNull List<Integer> conflicts) {
+ List<Integer> configuredSlots = config.getOrderedMappedSlots();
+ for (int i = 0; i < configuredSlots.size() && placedButtons.size() < numUiButtons; ++i) {
+ int slotNumber = configuredSlots.get(i);
+ List<Integer> potentialButtons = config.getButtonsForSlot(slotNumber);
+ Collections.sort(potentialButtons, config.getSlotComparator());
+ for (int j = 0; j < potentialButtons.size(); ++j) {
+ if (allowedButtons.contains(potentialButtons.get(j))) {
+ placedButtons.add(potentialButtons.get(j));
+ conflicts.addAll(potentialButtons.subList(j + 1, potentialButtons.size()));
+ break;
+ }
+ }
+ }
+ }
+
+ private void placeConflictsInOpenSlots(
+ int numUiButtons,
+ @NonNull Set<Integer> allowedButtons,
+ @NonNull Set<Integer> disabledButtons,
+ @NonNull List<Integer> placedButtons,
+ @NonNull List<Integer> conflicts) {
+ Collections.sort(conflicts, config.getConflictComparator());
+ for (Integer conflict : conflicts) {
+ if (placedButtons.size() >= numUiButtons) {
+ return;
+ }
+ // If the conflict button is allowed but disabled, don't place it since it probably will
+ // move when it's enabled.
+ if (!allowedButtons.contains(conflict) || disabledButtons.contains(conflict)) {
+ continue;
+ }
+ placedButtons.add(conflict);
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
new file mode 100644
index 000000000..1b168a6f7
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
@@ -0,0 +1,100 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.support.v4.util.ArrayMap;
+import android.telephony.TelephonyManager;
+import com.android.incallui.incall.impl.MappedButtonConfig.MappingInfo;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import java.util.Map;
+
+/**
+ * Creates {@link ButtonChooser} objects, based on the current network and phone type.
+ */
+class ButtonChooserFactory {
+
+ /**
+ * Creates the appropriate {@link ButtonChooser} based on the given information.
+ *
+ * @param voiceNetworkType the result of a call to {@link TelephonyManager#getVoiceNetworkType()}.
+ * @param isWiFi {@code true} if the call is made over WiFi, {@code false} otherwise.
+ * @param phoneType the result of a call to {@link TelephonyManager#getPhoneType()}.
+ * @return the ButtonChooser.
+ */
+ public static ButtonChooser newButtonChooser(
+ int voiceNetworkType, boolean isWiFi, int phoneType) {
+ if (voiceNetworkType == TelephonyManager.NETWORK_TYPE_LTE || isWiFi) {
+ return newImsAndWiFiButtonChooser();
+ }
+
+ if (phoneType == TelephonyManager.PHONE_TYPE_CDMA) {
+ return newCdmaButtonChooser();
+ }
+
+ if (phoneType == TelephonyManager.PHONE_TYPE_GSM) {
+ return newGsmButtonChooser();
+ }
+
+ return newImsAndWiFiButtonChooser();
+ }
+
+ private static ButtonChooser newImsAndWiFiButtonChooser() {
+ Map<Integer, MappingInfo> mapping = createCommonMapping();
+ mapping.put(
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ MappingInfo.builder(4).setSlotOrder(0).build());
+ mapping.put(
+ InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO, MappingInfo.builder(4).setSlotOrder(10).build());
+ mapping.put(
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY, MappingInfo.builder(5).setSlotOrder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_HOLD, MappingInfo.builder(5).setSlotOrder(10).build());
+
+ return new ButtonChooser(new MappedButtonConfig(mapping));
+ }
+
+ private static ButtonChooser newCdmaButtonChooser() {
+ Map<Integer, MappingInfo> mapping = createCommonMapping();
+ mapping.put(
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ MappingInfo.builder(4).setSlotOrder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_SWAP, MappingInfo.builder(5).setSlotOrder(0).build());
+
+ return new ButtonChooser(new MappedButtonConfig(mapping));
+ }
+
+ private static ButtonChooser newGsmButtonChooser() {
+ Map<Integer, MappingInfo> mapping = createCommonMapping();
+ mapping.put(
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY, MappingInfo.builder(4).setSlotOrder(0).build());
+ mapping.put(
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ MappingInfo.builder(4).setSlotOrder(10).build());
+ mapping.put(InCallButtonIds.BUTTON_HOLD, MappingInfo.builder(5).setSlotOrder(0).build());
+
+ return new ButtonChooser(new MappedButtonConfig(mapping));
+ }
+
+ private static Map<Integer, MappingInfo> createCommonMapping() {
+ Map<Integer, MappingInfo> mapping = new ArrayMap<>();
+ mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build());
+ mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build());
+ mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(0).build());
+ mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build());
+ return mapping;
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java
new file mode 100644
index 000000000..95a38be44
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/ButtonController.java
@@ -0,0 +1,584 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.support.annotation.CallSuper;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.telecom.CallAudioState;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.impl.CheckableLabeledButton.OnCheckedChangeListener;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+
+/** Manages a single button. */
+interface ButtonController {
+
+ boolean isEnabled();
+
+ void setEnabled(boolean isEnabled);
+
+ boolean isAllowed();
+
+ void setAllowed(boolean isAllowed);
+
+ void setChecked(boolean isChecked);
+
+ @InCallButtonIds
+ int getInCallButtonId();
+
+ void setButton(CheckableLabeledButton button);
+
+ final class Controllers {
+
+ private static void resetButton(CheckableLabeledButton button) {
+ if (button != null) {
+ button.setOnCheckedChangeListener(null);
+ button.setOnClickListener(null);
+ }
+ }
+ }
+
+ abstract class CheckableButtonController implements ButtonController, OnCheckedChangeListener {
+
+ @NonNull protected final InCallButtonUiDelegate delegate;
+ @InCallButtonIds protected final int buttonId;
+ @StringRes protected final int checkedDescription;
+ @StringRes protected final int uncheckedDescription;
+ protected boolean isEnabled;
+ protected boolean isAllowed;
+ protected boolean isChecked;
+ protected CheckableLabeledButton button;
+
+ protected CheckableButtonController(
+ @NonNull InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int checkedContentDescription,
+ @StringRes int uncheckedContentDescription) {
+ Assert.isNotNull(delegate);
+ this.delegate = delegate;
+ this.buttonId = buttonId;
+ this.checkedDescription = checkedContentDescription;
+ this.uncheckedDescription = uncheckedContentDescription;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ if (button != null) {
+ button.setChecked(isChecked);
+ }
+ }
+
+ @Override
+ @InCallButtonIds
+ public int getInCallButtonId() {
+ return buttonId;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ Controllers.resetButton(this.button);
+
+ this.button = button;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ button.setChecked(isChecked);
+ button.setOnClickListener(null);
+ button.setOnCheckedChangeListener(this);
+ button.setContentDescription(
+ button.getContext().getText(isChecked ? checkedDescription : uncheckedDescription));
+ button.setShouldShowMoreIndicator(false);
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked) {
+ button.setContentDescription(
+ button.getContext().getText(isChecked ? checkedDescription : uncheckedDescription));
+ doCheckedChanged(isChecked);
+ }
+
+ protected abstract void doCheckedChanged(boolean isChecked);
+ }
+
+ abstract class SimpleCheckableButtonController extends CheckableButtonController {
+
+ @StringRes private final int label;
+ @DrawableRes private final int icon;
+
+ protected SimpleCheckableButtonController(
+ @NonNull InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int checkedContentDescription,
+ @StringRes int uncheckedContentDescription,
+ @StringRes int label,
+ @DrawableRes int icon) {
+ super(
+ delegate,
+ buttonId,
+ checkedContentDescription == 0 ? label : checkedContentDescription,
+ uncheckedContentDescription == 0 ? label : uncheckedContentDescription);
+ this.label = label;
+ this.icon = icon;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ super.setButton(button);
+ if (button != null) {
+ button.setLabelText(label);
+ button.setIconDrawable(icon);
+ }
+ }
+ }
+
+ abstract class NonCheckableButtonController implements ButtonController, OnClickListener {
+
+ protected final InCallButtonUiDelegate delegate;
+ @InCallButtonIds protected final int buttonId;
+ @StringRes protected final int contentDescription;
+ protected boolean isEnabled;
+ protected boolean isAllowed;
+ protected CheckableLabeledButton button;
+
+ protected NonCheckableButtonController(
+ InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int contentDescription) {
+ this.delegate = delegate;
+ this.buttonId = buttonId;
+ this.contentDescription = contentDescription;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ Assert.fail();
+ }
+
+ @Override
+ @InCallButtonIds
+ public int getInCallButtonId() {
+ return buttonId;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ Controllers.resetButton(this.button);
+
+ this.button = button;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ button.setChecked(false);
+ button.setOnCheckedChangeListener(null);
+ button.setOnClickListener(this);
+ button.setContentDescription(button.getContext().getText(contentDescription));
+ button.setShouldShowMoreIndicator(false);
+ }
+ }
+ }
+
+ abstract class SimpleNonCheckableButtonController extends NonCheckableButtonController {
+
+ @StringRes private final int label;
+ @DrawableRes private final int icon;
+
+ protected SimpleNonCheckableButtonController(
+ InCallButtonUiDelegate delegate,
+ @InCallButtonIds int buttonId,
+ @StringRes int contentDescription,
+ @StringRes int label,
+ @DrawableRes int icon) {
+ super(delegate, buttonId, contentDescription == 0 ? label : contentDescription);
+ this.label = label;
+ this.icon = icon;
+ }
+
+ @Override
+ @CallSuper
+ public void setButton(CheckableLabeledButton button) {
+ super.setButton(button);
+ if (button != null) {
+ button.setLabelText(label);
+ button.setIconDrawable(icon);
+ }
+ }
+ }
+
+ class MuteButtonController extends SimpleCheckableButtonController {
+
+ public MuteButtonController(InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_MUTE,
+ R.string.incall_content_description_muted,
+ R.string.incall_content_description_unmuted,
+ R.string.incall_label_mute,
+ R.drawable.quantum_ic_mic_off_white_36);
+ }
+
+ @Override
+ public void doCheckedChanged(boolean isChecked) {
+ delegate.muteClicked(isChecked);
+ }
+ }
+
+ class SpeakerButtonController
+ implements ButtonController, OnCheckedChangeListener, OnClickListener {
+
+ @NonNull private final InCallButtonUiDelegate delegate;
+ private boolean isEnabled;
+ private boolean isAllowed;
+ private boolean isChecked;
+ private CheckableLabeledButton button;
+
+ @StringRes private int label = R.string.incall_label_speaker;
+ @DrawableRes private int icon = R.drawable.quantum_ic_volume_up_white_36;
+ private boolean checkable;
+ private CharSequence contentDescription;
+ private CharSequence checkedContentDescription;
+ private CharSequence uncheckedContentDescription;
+
+ public SpeakerButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled && isAllowed);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setEnabled(isEnabled && isAllowed);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ if (button != null) {
+ button.setChecked(isChecked);
+ }
+ }
+
+ @Override
+ public int getInCallButtonId() {
+ return InCallButtonIds.BUTTON_AUDIO;
+ }
+
+ @Override
+ public void setButton(CheckableLabeledButton button) {
+ this.button = button;
+ if (button != null) {
+ button.setEnabled(isEnabled && isAllowed);
+ button.setVisibility(View.VISIBLE);
+ button.setChecked(isChecked);
+ button.setOnClickListener(checkable ? null : this);
+ button.setOnCheckedChangeListener(checkable ? this : null);
+ button.setLabelText(label);
+ button.setIconDrawable(icon);
+ button.setContentDescription(
+ isChecked ? checkedContentDescription : uncheckedContentDescription);
+ button.setShouldShowMoreIndicator(!checkable);
+ }
+ }
+
+ public void setAudioState(CallAudioState audioState) {
+ @StringRes int contentDescriptionResId;
+ if ((audioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ checkable = false;
+ isChecked = false;
+ label = R.string.incall_label_audio;
+
+ if ((audioState.getRoute() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ icon = R.drawable.quantum_ic_bluetooth_audio_white_36;
+ contentDescriptionResId = R.string.incall_content_description_bluetooth;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_SPEAKER)
+ == CallAudioState.ROUTE_SPEAKER) {
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_WIRED_HEADSET)
+ == CallAudioState.ROUTE_WIRED_HEADSET) {
+ icon = R.drawable.quantum_ic_headset_white_36;
+ contentDescriptionResId = R.string.incall_content_description_headset;
+ } else {
+ icon = R.drawable.ic_phone_audio_white_36dp;
+ contentDescriptionResId = R.string.incall_content_description_earpiece;
+ }
+ } else {
+ checkable = true;
+ isChecked = audioState.getRoute() == CallAudioState.ROUTE_SPEAKER;
+ label = R.string.incall_label_speaker;
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ }
+
+ contentDescription = delegate.getContext().getText(contentDescriptionResId);
+ checkedContentDescription =
+ TextUtils.concat(
+ contentDescription,
+ delegate.getContext().getText(R.string.incall_talkback_speaker_on));
+ uncheckedContentDescription =
+ TextUtils.concat(
+ contentDescription,
+ delegate.getContext().getText(R.string.incall_talkback_speaker_off));
+ setButton(button);
+ }
+
+ @Override
+ public void onClick(View v) {
+ delegate.showAudioRouteSelector();
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked) {
+ checkableLabeledButton.setContentDescription(
+ isChecked ? checkedContentDescription : uncheckedContentDescription);
+ delegate.toggleSpeakerphone();
+ }
+ }
+
+ class DialpadButtonController extends SimpleCheckableButtonController {
+
+ public DialpadButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_DIALPAD,
+ 0,
+ 0,
+ R.string.incall_label_dialpad,
+ R.drawable.quantum_ic_dialpad_white_36);
+ }
+
+ @Override
+ public void doCheckedChanged(boolean isChecked) {
+ delegate.showDialpadClicked(isChecked);
+ }
+ }
+
+ class HoldButtonController extends SimpleCheckableButtonController {
+
+ public HoldButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_HOLD,
+ R.string.incall_content_description_unhold,
+ R.string.incall_content_description_hold,
+ R.string.incall_label_hold,
+ R.drawable.quantum_ic_pause_white_36);
+ }
+
+ @Override
+ public void doCheckedChanged(boolean isChecked) {
+ delegate.holdClicked(isChecked);
+ }
+ }
+
+ class AddCallButtonController extends SimpleNonCheckableButtonController {
+
+ public AddCallButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_ADD_CALL,
+ 0,
+ R.string.incall_label_add_call,
+ R.drawable.ic_addcall_white);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.addCallClicked();
+ }
+ }
+
+ class SwapButtonController extends SimpleNonCheckableButtonController {
+
+ public SwapButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_SWAP,
+ R.string.incall_content_description_swap_calls,
+ R.string.incall_label_swap,
+ R.drawable.quantum_ic_swap_calls_white_36);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.swapClicked();
+ }
+ }
+
+ class MergeButtonController extends SimpleNonCheckableButtonController {
+
+ public MergeButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_MERGE,
+ R.string.incall_content_description_merge_calls,
+ R.string.incall_label_merge,
+ R.drawable.quantum_ic_call_merge_white_36);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.mergeClicked();
+ }
+ }
+
+ class UpgradeToVideoButtonController extends SimpleNonCheckableButtonController {
+
+ public UpgradeToVideoButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ super(
+ delegate,
+ InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO,
+ 0,
+ R.string.incall_label_videocall,
+ R.drawable.quantum_ic_videocam_white_36);
+ Assert.isNotNull(delegate);
+ }
+
+ @Override
+ public void onClick(View view) {
+ delegate.changeToVideoClicked();
+ }
+ }
+
+ class ManageConferenceButtonController extends SimpleNonCheckableButtonController {
+
+ private final InCallScreenDelegate inCallScreenDelegate;
+
+ public ManageConferenceButtonController(@NonNull InCallScreenDelegate inCallScreenDelegate) {
+ super(
+ null,
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ R.string.a11y_description_incall_label_manage_content,
+ R.string.incall_label_manage,
+ R.drawable.quantum_ic_group_white_36);
+ Assert.isNotNull(inCallScreenDelegate);
+ this.inCallScreenDelegate = inCallScreenDelegate;
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onManageConferenceClicked();
+ }
+ }
+
+ class SwitchToSecondaryButtonController extends SimpleNonCheckableButtonController {
+
+ private final InCallScreenDelegate inCallScreenDelegate;
+
+ public SwitchToSecondaryButtonController(InCallScreenDelegate inCallScreenDelegate) {
+ super(
+ null,
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
+ R.string.incall_content_description_swap_calls,
+ R.string.incall_label_swap,
+ R.drawable.quantum_ic_swap_calls_white_36);
+ Assert.isNotNull(inCallScreenDelegate);
+ this.inCallScreenDelegate = inCallScreenDelegate;
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onSecondaryInfoClicked();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
new file mode 100644
index 000000000..a681adcb4
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
@@ -0,0 +1,286 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.animation.AnimatorInflater;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.StringRes;
+import android.text.TextUtils.TruncateAt;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.SoundEffectConstants;
+import android.widget.Checkable;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/** A button to show on the incall screen */
+public class CheckableLabeledButton extends LinearLayout implements Checkable {
+
+ private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
+ private static final float DISABLED_STATE_OPACITY = .3f;
+ private boolean broadcasting;
+ private boolean isChecked;
+ private OnCheckedChangeListener onCheckedChangeListener;
+ private ImageView iconView;
+ private TextView labelView;
+ private Drawable background;
+ private Drawable backgroundMore;
+
+ public CheckableLabeledButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ public CheckableLabeledButton(Context context) {
+ this(context, null);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ setOrientation(VERTICAL);
+ setGravity(Gravity.CENTER_HORIZONTAL);
+ Drawable icon;
+ CharSequence labelText;
+ boolean enabled;
+
+ backgroundMore = getResources().getDrawable(R.drawable.incall_button_background_more, null);
+ background = getResources().getDrawable(R.drawable.incall_button_background, null);
+
+ TypedArray typedArray =
+ context.obtainStyledAttributes(attrs, R.styleable.CheckableLabeledButton);
+ icon = typedArray.getDrawable(R.styleable.CheckableLabeledButton_incall_icon);
+ labelText = typedArray.getString(R.styleable.CheckableLabeledButton_incall_labelText);
+ enabled = typedArray.getBoolean(R.styleable.CheckableLabeledButton_android_enabled, true);
+ typedArray.recycle();
+
+ int paddingSize = getResources().getDimensionPixelOffset(R.dimen.incall_button_padding);
+ setPadding(paddingSize, paddingSize, paddingSize, paddingSize);
+
+ int iconSize = getResources().getDimensionPixelSize(R.dimen.incall_labeled_button_size);
+
+ iconView = new ImageView(context, null, android.R.style.Widget_Material_Button_Colored);
+ LayoutParams iconParams = generateDefaultLayoutParams();
+ iconParams.width = iconSize;
+ iconParams.height = iconSize;
+ iconView.setLayoutParams(iconParams);
+ iconView.setScaleType(ScaleType.CENTER_INSIDE);
+ iconView.setImageDrawable(icon);
+ iconView.setImageTintMode(Mode.SRC_IN);
+ iconView.setImageTintList(getResources().getColorStateList(R.color.incall_button_icon, null));
+ iconView.setBackground(getResources().getDrawable(R.drawable.incall_button_background, null));
+ iconView.setDuplicateParentStateEnabled(true);
+ iconView.setElevation(getResources().getDimension(R.dimen.incall_button_elevation));
+ iconView.setStateListAnimator(
+ AnimatorInflater.loadStateListAnimator(context, R.animator.incall_button_elevation));
+ addView(iconView);
+
+ labelView = new TextView(context);
+ LayoutParams labelParams = generateDefaultLayoutParams();
+ labelParams.width = LayoutParams.WRAP_CONTENT;
+ labelParams.height = LayoutParams.WRAP_CONTENT;
+ labelParams.topMargin =
+ context.getResources().getDimensionPixelOffset(R.dimen.incall_button_label_margin);
+ labelView.setLayoutParams(labelParams);
+ labelView.setTextAppearance(R.style.Dialer_Incall_TextAppearance_Label);
+ labelView.setText(labelText);
+ labelView.setSingleLine();
+ labelView.setMaxEms(9);
+ labelView.setEllipsize(TruncateAt.END);
+ labelView.setGravity(Gravity.CENTER);
+ labelView.setDuplicateParentStateEnabled(true);
+ addView(labelView);
+
+ setFocusable(true);
+ setClickable(true);
+ setEnabled(enabled);
+ setOutlineProvider(null);
+ }
+
+ @Override
+ public void refreshDrawableState() {
+ super.refreshDrawableState();
+ iconView.setAlpha(isEnabled() ? 1f : DISABLED_STATE_OPACITY);
+ labelView.setAlpha(isEnabled() ? 1f : DISABLED_STATE_OPACITY);
+ }
+
+ public void setIconDrawable(@DrawableRes int drawableRes) {
+ iconView.setImageResource(drawableRes);
+ }
+
+ public void setLabelText(@StringRes int stringRes) {
+ labelView.setText(stringRes);
+ }
+
+ /** Shows or hides a little down arrow to indicate that the button will pop up a menu. */
+ public void setShouldShowMoreIndicator(boolean shouldShow) {
+ iconView.setBackground(shouldShow ? backgroundMore : background);
+ }
+
+ @Override
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ performSetChecked(checked);
+ }
+
+ @Override
+ public void toggle() {
+ userRequestedSetChecked(!isChecked());
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ this.onCheckedChangeListener = listener;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (!isCheckable()) {
+ return super.performClick();
+ }
+
+ toggle();
+ final boolean handled = super.performClick();
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return handled;
+ }
+
+ private boolean isCheckable() {
+ return onCheckedChangeListener != null;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ performSetChecked(savedState.isChecked);
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ return new SavedState(isChecked(), super.onSaveInstanceState());
+ }
+
+ /**
+ * Called when the state of the button should be updated, this should not be the result of user
+ * interaction.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void performSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ isChecked = checked;
+ refreshDrawableState();
+ }
+
+ /**
+ * Called when the user interacts with a button. This should not result in the button updating
+ * state, rather the request should be propagated to the associated listener.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void userRequestedSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ if (broadcasting) {
+ return;
+ }
+ broadcasting = true;
+ if (onCheckedChangeListener != null) {
+ onCheckedChangeListener.onCheckedChanged(this, checked);
+ }
+ broadcasting = false;
+ }
+
+ /** Callback interface to notify when the button's checked state has changed */
+ public interface OnCheckedChangeListener {
+
+ void onCheckedChanged(CheckableLabeledButton checkableLabeledButton, boolean isChecked);
+ }
+
+ private static class SavedState extends BaseSavedState {
+
+ public static final Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ public final boolean isChecked;
+
+ private SavedState(boolean isChecked, Parcelable superState) {
+ super(superState);
+ this.isChecked = isChecked;
+ }
+
+ protected SavedState(Parcel in) {
+ super(in);
+ isChecked = in.readByte() != 0;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeByte((byte) (isChecked ? 1 : 0));
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallButtonGridFragment.java b/java/com/android/incallui/incall/impl/InCallButtonGridFragment.java
new file mode 100644
index 000000000..db0b5b9b8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/InCallButtonGridFragment.java
@@ -0,0 +1,137 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.util.ArraySet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import java.util.List;
+import java.util.Set;
+
+/** Fragment for the in call buttons (mute, speaker, ect.). */
+public class InCallButtonGridFragment extends Fragment {
+
+ private static final int BUTTON_COUNT = 6;
+ private static final int BUTTONS_PER_ROW = 3;
+
+ private CheckableLabeledButton[] buttons = new CheckableLabeledButton[BUTTON_COUNT];
+ private OnButtonGridCreatedListener buttonGridListener;
+
+ public static Fragment newInstance() {
+ return new InCallButtonGridFragment();
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ buttonGridListener = FragmentUtils.getParent(this, OnButtonGridCreatedListener.class);
+ Assert.isNotNull(buttonGridListener);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle bundle) {
+ View view = inflater.inflate(R.layout.incall_button_grid, parent, false);
+
+ buttons[0] = ((CheckableLabeledButton) view.findViewById(R.id.incall_first_button));
+ buttons[1] = ((CheckableLabeledButton) view.findViewById(R.id.incall_second_button));
+ buttons[2] = ((CheckableLabeledButton) view.findViewById(R.id.incall_third_button));
+ buttons[3] = ((CheckableLabeledButton) view.findViewById(R.id.incall_fourth_button));
+ buttons[4] = ((CheckableLabeledButton) view.findViewById(R.id.incall_fifth_button));
+ buttons[5] = ((CheckableLabeledButton) view.findViewById(R.id.incall_sixth_button));
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ buttonGridListener.onButtonGridCreated(this);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ buttonGridListener.onButtonGridDestroyed();
+ }
+
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ for (CheckableLabeledButton button : buttons) {
+ button.setImportantForAccessibility(
+ isShowing
+ ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ : View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
+ }
+
+ public int updateButtonStates(
+ List<ButtonController> buttonControllers,
+ @Nullable ButtonChooser buttonChooser,
+ int voiceNetworkType,
+ int phoneType) {
+ Set<Integer> allowedButtons = new ArraySet<>();
+ Set<Integer> disabledButtons = new ArraySet<>();
+ for (ButtonController controller : buttonControllers) {
+ if (controller.isAllowed()) {
+ allowedButtons.add(controller.getInCallButtonId());
+ if (!controller.isEnabled()) {
+ disabledButtons.add(controller.getInCallButtonId());
+ }
+ }
+ }
+
+ for (ButtonController controller : buttonControllers) {
+ controller.setButton(null);
+ }
+
+ if (buttonChooser == null) {
+ buttonChooser =
+ ButtonChooserFactory.newButtonChooser(voiceNetworkType, false /* isWiFi */, phoneType);
+ }
+
+ int numVisibleButtons = getResources().getInteger(R.integer.incall_num_rows) * BUTTONS_PER_ROW;
+ List<Integer> buttonsToPlace =
+ buttonChooser.getButtonPlacement(numVisibleButtons, allowedButtons, disabledButtons);
+
+ for (int i = 0; i < BUTTON_COUNT; ++i) {
+ if (i >= buttonsToPlace.size()) {
+ buttons[i].setVisibility(View.INVISIBLE);
+ continue;
+ }
+ @InCallButtonIds int button = buttonsToPlace.get(i);
+ buttonGridListener.getButtonController(button).setButton(buttons[i]);
+ }
+
+ return numVisibleButtons;
+ }
+
+ /** Interface to let the listener know the status of the button grid. */
+ public interface OnButtonGridCreatedListener {
+ void onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment);
+ void onButtonGridDestroyed();
+
+ ButtonController getButtonController(@InCallButtonIds int id);
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
new file mode 100644
index 000000000..ef8a1edd8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -0,0 +1,501 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.TabLayout;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.view.ViewPager;
+import android.telecom.CallAudioState;
+import android.telephony.TelephonyManager;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.Toast;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
+import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Fragment that shows UI for an ongoing voice call. */
+public class InCallFragment extends Fragment
+ implements InCallScreen,
+ InCallButtonUi,
+ OnClickListener,
+ AudioRouteSelectorPresenter,
+ OnButtonGridCreatedListener {
+
+ private List<ButtonController> buttonControllers = new ArrayList<>();
+ private View endCallButton;
+ private TabLayout tabLayout;
+ private ViewPager pager;
+ private InCallPagerAdapter adapter;
+ private ContactGridManager contactGridManager;
+ private InCallScreenDelegate inCallScreenDelegate;
+ private InCallButtonUiDelegate inCallButtonUiDelegate;
+ private InCallButtonGridFragment inCallButtonGridFragment;
+ @Nullable private ButtonChooser buttonChooser;
+ private SecondaryInfo savedSecondaryInfo;
+ private int voiceNetworkType;
+ private int phoneType;
+ private boolean stateRestored;
+
+ private static boolean isSupportedButton(@InCallButtonIds int id) {
+ return id == InCallButtonIds.BUTTON_AUDIO
+ || id == InCallButtonIds.BUTTON_MUTE
+ || id == InCallButtonIds.BUTTON_DIALPAD
+ || id == InCallButtonIds.BUTTON_HOLD
+ || id == InCallButtonIds.BUTTON_SWAP
+ || id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO
+ || id == InCallButtonIds.BUTTON_ADD_CALL
+ || id == InCallButtonIds.BUTTON_MERGE
+ || id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (savedSecondaryInfo != null) {
+ setSecondary(savedSecondaryInfo);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ inCallButtonUiDelegate =
+ FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
+ .newInCallButtonUiDelegate();
+ if (savedInstanceState != null) {
+ inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
+ stateRestored = true;
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ @NonNull LayoutInflater layoutInflater,
+ @Nullable ViewGroup viewGroup,
+ @Nullable Bundle bundle) {
+ LogUtil.i("InCallFragment.onCreateView", null);
+ final View view = layoutInflater.inflate(R.layout.frag_incall_voice, viewGroup, false);
+ contactGridManager =
+ new ContactGridManager(
+ view,
+ (ImageView) view.findViewById(R.id.contactgrid_avatar),
+ getResources().getDimensionPixelSize(R.dimen.incall_avatar_size),
+ true /* showAnonymousAvatar */);
+
+ tabLayout = (TabLayout) view.findViewById(R.id.incall_tab_dots);
+ pager = (ViewPager) view.findViewById(R.id.incall_pager);
+
+ endCallButton = view.findViewById(R.id.incall_end_call);
+ endCallButton.setOnClickListener(this);
+
+ if (ContextCompat.checkSelfPermission(getContext(), permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ voiceNetworkType = TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ } else {
+
+ voiceNetworkType =
+ VERSION.SDK_INT >= VERSION_CODES.N
+ ? getContext().getSystemService(TelephonyManager.class).getVoiceNetworkType()
+ : TelephonyManager.NETWORK_TYPE_UNKNOWN;
+ }
+ phoneType = getContext().getSystemService(TelephonyManager.class).getPhoneType();
+ return view;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ inCallButtonUiDelegate.refreshMuteState();
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle bundle) {
+ LogUtil.i("InCallFragment.onViewCreated", null);
+ super.onViewCreated(view, bundle);
+ inCallScreenDelegate =
+ FragmentUtils.getParent(this, InCallScreenDelegateFactory.class).newInCallScreenDelegate();
+ Assert.isNotNull(inCallScreenDelegate);
+
+ buttonControllers.add(new ButtonController.MuteButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.SpeakerButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.DialpadButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.HoldButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.AddCallButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.SwapButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(new ButtonController.MergeButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(
+ new ButtonController.UpgradeToVideoButtonController(inCallButtonUiDelegate));
+ buttonControllers.add(
+ new ButtonController.ManageConferenceButtonController(inCallScreenDelegate));
+ buttonControllers.add(
+ new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate));
+
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ inCallScreenDelegate.onInCallScreenUnready();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ inCallButtonUiDelegate.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == endCallButton) {
+ LogUtil.i("InCallFragment.onClick", "end call button clicked");
+ inCallScreenDelegate.onEndCallClicked();
+ } else {
+ LogUtil.e("InCallFragment.onClick", "unknown view: " + view);
+ Assert.fail();
+ }
+ }
+
+ @Override
+ public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
+ LogUtil.i("InCallFragment.setPrimary", primaryInfo.toString());
+ if (adapter == null) {
+ initAdapter(primaryInfo.multimediaData);
+ }
+ contactGridManager.setPrimary(primaryInfo);
+
+ if (primaryInfo.shouldShowLocation) {
+ // Hide the avatar to make room for location
+ contactGridManager.setAvatarHidden(true);
+
+ // Need to widen the contact grid to fit location information
+ View contactGridView = getView().findViewById(R.id.incall_contact_grid);
+ ViewGroup.LayoutParams params = contactGridView.getLayoutParams();
+ if (params instanceof ViewGroup.MarginLayoutParams) {
+ ((ViewGroup.MarginLayoutParams) params).setMarginStart(0);
+ ((ViewGroup.MarginLayoutParams) params).setMarginEnd(0);
+ }
+ contactGridView.setLayoutParams(params);
+
+ // Need to let the dialpad move up a little further when location info is being shown
+ View dialpadView = getView().findViewById(R.id.incall_dialpad_container);
+ params = dialpadView.getLayoutParams();
+ if (params instanceof RelativeLayout.LayoutParams) {
+ ((RelativeLayout.LayoutParams) params).removeRule(RelativeLayout.BELOW);
+ }
+ dialpadView.setLayoutParams(params);
+ }
+ }
+
+ private void initAdapter(MultimediaData multimediaData) {
+ adapter = new InCallPagerAdapter(getChildFragmentManager(), multimediaData);
+ pager.setAdapter(adapter);
+
+ if (adapter.getCount() > 1) {
+ tabLayout.setVisibility(pager.getVisibility());
+ tabLayout.setupWithViewPager(pager, true);
+ if (!stateRestored) {
+ new Handler()
+ .postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ // In order to prevent user confusion and educate the user on our UI, we animate
+ // the view pager to the button grid after 2 seconds show them when the UI is
+ // that they are more familiar with.
+ pager.setCurrentItem(adapter.getButtonGridPosition());
+ }
+ },
+ 2000);
+ }
+ } else {
+ tabLayout.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
+ LogUtil.i("InCallFragment.setSecondary", secondaryInfo.toString());
+ getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY)
+ .setEnabled(secondaryInfo.shouldShow);
+ getButtonController(InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY)
+ .setAllowed(secondaryInfo.shouldShow);
+ updateButtonStates();
+
+ if (!isAdded()) {
+ savedSecondaryInfo = secondaryInfo;
+ return;
+ }
+ savedSecondaryInfo = null;
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.incall_on_hold_banner);
+ if (secondaryInfo.shouldShow) {
+ transaction.replace(R.id.incall_on_hold_banner, OnHoldFragment.newInstance(secondaryInfo));
+ } else {
+ if (oldBanner != null) {
+ transaction.remove(oldBanner);
+ }
+ }
+ transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
+ transaction.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("InCallFragment.setCallState", primaryCallState.toString());
+ contactGridManager.setCallState(primaryCallState);
+ buttonChooser =
+ ButtonChooserFactory.newButtonChooser(voiceNetworkType, primaryCallState.isWifi, phoneType);
+ updateButtonStates();
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
+ if (endCallButton != null) {
+ endCallButton.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {
+ getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setAllowed(visible);
+ getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).setEnabled(visible);
+ updateButtonStates();
+ }
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ return getButtonController(InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE).isAllowed();
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public void showNoteSentToast() {
+ LogUtil.i("InCallFragment.showNoteSentToast", null);
+ Toast.makeText(getContext(), R.string.incall_note_sent, Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void updateInCallScreenColors() {}
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ LogUtil.i("InCallFragment.onInCallScreenDialpadVisibilityChange", "isShowing: " + isShowing);
+ // Take note that the dialpad button isShowing
+ getButtonController(InCallButtonIds.BUTTON_DIALPAD).setChecked(isShowing);
+
+ // This check is needed because there is a race condition where we attempt to update
+ // ButtonGridFragment before it is ready, so we check whether it is ready first and once it is
+ // ready, #onButtonGridCreated will mark the dialpad button as isShowing.
+ if (inCallButtonGridFragment != null) {
+ // Update the Android Button's state to isShowing.
+ inCallButtonGridFragment.onInCallScreenDialpadVisibilityChange(isShowing);
+ }
+ }
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ return R.id.incall_dialpad_container;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void showButton(@InCallButtonIds int buttonId, boolean show) {
+ LogUtil.v(
+ "InCallFragment.showButton",
+ "buttionId: %s, show: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ show);
+ if (isSupportedButton(buttonId)) {
+ getButtonController(buttonId).setAllowed(show);
+ }
+ }
+
+ @Override
+ public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
+ LogUtil.v(
+ "InCallFragment.enableButton",
+ "buttonId: %s, enable: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ enable);
+ if (isSupportedButton(buttonId)) {
+ getButtonController(buttonId).setEnabled(enable);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ LogUtil.v("InCallFragment.setEnabled", "enabled: " + enabled);
+ for (ButtonController buttonController : buttonControllers) {
+ buttonController.setEnabled(enabled);
+ }
+ }
+
+ @Override
+ public void setHold(boolean value) {
+ getButtonController(InCallButtonIds.BUTTON_HOLD).setChecked(value);
+ }
+
+ @Override
+ public void setCameraSwitched(boolean isBackFacingCamera) {}
+
+ @Override
+ public void setVideoPaused(boolean isPaused) {}
+
+ @Override
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("InCallFragment.setAudioState", "audioState: " + audioState);
+ ((SpeakerButtonController) getButtonController(InCallButtonIds.BUTTON_AUDIO))
+ .setAudioState(audioState);
+ getButtonController(InCallButtonIds.BUTTON_MUTE).setChecked(audioState.isMuted());
+ }
+
+ @Override
+ public void updateButtonStates() {
+ // When the incall screen is ready, this method is called from #setSecondary, even though the
+ // incall button ui is not ready yet. This method is called again once the incall button ui is
+ // ready though, so this operation is safe and will be executed asap.
+ if (inCallButtonGridFragment == null) {
+ return;
+ }
+ int numVisibleButtons =
+ inCallButtonGridFragment.updateButtonStates(
+ buttonControllers, buttonChooser, voiceNetworkType, phoneType);
+
+ int visibility = numVisibleButtons == 0 ? View.GONE : View.VISIBLE;
+ pager.setVisibility(visibility);
+ if (adapter != null && adapter.getCount() > 1) {
+ tabLayout.setVisibility(visibility);
+ }
+ }
+
+ @Override
+ public void updateInCallButtonUiColors() {}
+
+ @Override
+ public Fragment getInCallButtonUiFragment() {
+ return this;
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
+ .show(getChildFragmentManager(), null);
+ }
+
+ @Override
+ public void onAudioRouteSelected(int audioRoute) {
+ inCallButtonUiDelegate.setAudioRoute(audioRoute);
+ }
+
+ @NonNull
+ @Override
+ public ButtonController getButtonController(@InCallButtonIds int id) {
+ for (ButtonController buttonController : buttonControllers) {
+ if (buttonController.getInCallButtonId() == id) {
+ return buttonController;
+ }
+ }
+ Assert.fail();
+ return null;
+ }
+
+ @Override
+ public void onButtonGridCreated(InCallButtonGridFragment inCallButtonGridFragment) {
+ LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiReady");
+ this.inCallButtonGridFragment = inCallButtonGridFragment;
+ inCallButtonUiDelegate.onInCallButtonUiReady(this);
+ updateButtonStates();
+ }
+
+ @Override
+ public void onButtonGridDestroyed() {
+ LogUtil.i("InCallFragment.onButtonGridCreated", "InCallUiUnready");
+ inCallButtonUiDelegate.onInCallButtonUiUnready();
+ this.inCallButtonGridFragment = null;
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ return fragment != null && fragment.isVisible();
+ }
+
+ @Override
+ public void showLocationUi(@Nullable Fragment locationUi) {
+ boolean isShowing = isShowingLocationUi();
+ if (!isShowing && locationUi != null) {
+ // Show the location fragment.
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.incall_location_holder, locationUi)
+ .commitAllowingStateLoss();
+ } else if (isShowing && locationUi == null) {
+ // Hide the location fragment
+ Fragment fragment = getChildFragmentManager().findFragmentById(R.id.incall_location_holder);
+ getChildFragmentManager().beginTransaction().remove(fragment).commitAllowingStateLoss();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/InCallPagerAdapter.java b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
new file mode 100644
index 000000000..50eb4c8c3
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/InCallPagerAdapter.java
@@ -0,0 +1,59 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.text.TextUtils;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.sessiondata.MultimediaFragment;
+
+/** View pager adapter for in call ui. */
+public class InCallPagerAdapter extends FragmentPagerAdapter {
+
+ @Nullable private final MultimediaData attachments;
+
+ public InCallPagerAdapter(FragmentManager fragmentManager, MultimediaData attachments) {
+ super(fragmentManager);
+ this.attachments = attachments;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if (position == getButtonGridPosition()) {
+ return InCallButtonGridFragment.newInstance();
+ } else {
+ // TODO: handle fragment invalidation for when the data changes.
+ return MultimediaFragment.newInstance(attachments, true, false);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ if (attachments != null
+ && (!TextUtils.isEmpty(attachments.getSubject()) || attachments.hasImageData())) {
+ return 2;
+ }
+ return 1;
+ }
+
+ public int getButtonGridPosition() {
+ return getCount() - 1;
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/MappedButtonConfig.java b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
new file mode 100644
index 000000000..ecdb5dfea
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/MappedButtonConfig.java
@@ -0,0 +1,193 @@
+/*
+ * 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.incallui.incall.impl;
+
+import android.support.annotation.NonNull;
+import android.support.v4.util.ArrayMap;
+import android.util.ArraySet;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * Determines logical button slot and ordering based on a provided mapping.
+ *
+ * <p>The provided mapping is declared with the following pieces of information: key, the {@link
+ * InCallButtonIds} for which the mapping applies; {@link MappingInfo#getSlot()}, the arbitrarily
+ * indexed slot into which the InCallButtonId desires to be placed; {@link
+ * MappingInfo#getSlotOrder()}, the slotOrder, used to choose the correct InCallButtonId when
+ * multiple desire to be placed in the same slot; and {@link MappingInfo#getConflictOrder()}, the
+ * conflictOrder, used to determine the overall order for InCallButtonIds that weren't chosen for
+ * their desired slot.
+ */
+@Immutable
+final class MappedButtonConfig {
+
+ @NonNull private final Map<Integer, MappingInfo> mapping;
+ @NonNull private final List<Integer> orderedMappedSlots;
+
+ /**
+ * Creates this MappedButtonConfig with the given mapping of {@link InCallButtonIds} to their
+ * corresponding slots and order.
+ *
+ * @param mapping the mapping.
+ */
+ public MappedButtonConfig(@NonNull Map<Integer, MappingInfo> mapping) {
+ this.mapping = new ArrayMap<>();
+ this.mapping.putAll(Assert.isNotNull(mapping));
+ this.orderedMappedSlots = findOrderedMappedSlots();
+ }
+
+ private List<Integer> findOrderedMappedSlots() {
+ Set<Integer> slots = new ArraySet<>();
+ for (Entry<Integer, MappingInfo> entry : mapping.entrySet()) {
+ slots.add(entry.getValue().getSlot());
+ }
+ List<Integer> orderedSlots = new ArrayList<>(slots);
+ Collections.sort(orderedSlots);
+ return orderedSlots;
+ }
+
+ /** Returns an immutable list of the slots for which this class has button mapping. */
+ @NonNull
+ public List<Integer> getOrderedMappedSlots() {
+ if (mapping.isEmpty()) {
+ return Collections.emptyList();
+ }
+ return Collections.unmodifiableList(orderedMappedSlots);
+ }
+
+ /**
+ * Returns a list of {@link InCallButtonIds} that are configured to be placed in the given ui
+ * slot. The slot can be based from any index, as long as it matches the provided mapping.
+ */
+ @NonNull
+ public List<Integer> getButtonsForSlot(int slot) {
+ List<Integer> buttons = new ArrayList<>();
+ for (Entry<Integer, MappingInfo> entry : mapping.entrySet()) {
+ if (entry.getValue().getSlot() == slot) {
+ buttons.add(entry.getKey());
+ }
+ }
+ return buttons;
+ }
+
+ /**
+ * Returns a {@link Comparator} capable of ordering {@link InCallButtonIds} that are configured to
+ * be placed in the same slot. InCallButtonIds are sorted based on the natural ordering of {@link
+ * MappingInfo#getSlotOrder()}.
+ *
+ * <p>Note: the returned Comparator's compare method will throw an {@link
+ * IllegalArgumentException} if called with InCallButtonIds that have no configuration or are not
+ * to be placed in the same slot.
+ */
+ @NonNull
+ public Comparator<Integer> getSlotComparator() {
+ return new Comparator<Integer>() {
+ @Override
+ public int compare(Integer lhs, Integer rhs) {
+ MappingInfo lhsInfo = lookupMappingInfo(lhs);
+ MappingInfo rhsInfo = lookupMappingInfo(rhs);
+ if (lhsInfo.getSlot() != rhsInfo.getSlot()) {
+ throw new IllegalArgumentException("lhs and rhs don't go in the same slot");
+ }
+ return lhsInfo.getSlotOrder() - rhsInfo.getSlotOrder();
+ }
+ };
+ }
+
+ /**
+ * Returns a {@link Comparator} capable of ordering {@link InCallButtonIds} by their conflict
+ * score. This comparator should be used when multiple InCallButtonIds could have been shown in
+ * the same slot. InCallButtonIds are sorted based on the natural ordering of {@link
+ * MappingInfo#getConflictOrder()}.
+ *
+ * <p>Note: the returned Comparator's compare method will throw an {@link
+ * IllegalArgumentException} if called with InCallButtonIds that have no configuration.
+ */
+ @NonNull
+ public Comparator<Integer> getConflictComparator() {
+ return new Comparator<Integer>() {
+ @Override
+ public int compare(Integer lhs, Integer rhs) {
+ MappingInfo lhsInfo = lookupMappingInfo(lhs);
+ MappingInfo rhsInfo = lookupMappingInfo(rhs);
+ return lhsInfo.getConflictOrder() - rhsInfo.getConflictOrder();
+ }
+ };
+ }
+
+ @NonNull
+ private MappingInfo lookupMappingInfo(@InCallButtonIds int button) {
+ MappingInfo info = mapping.get(button);
+ if (info == null) {
+ throw new IllegalArgumentException(
+ "Unknown InCallButtonId: " + InCallButtonIdsExtension.toString(button));
+ }
+ return info;
+ }
+
+ /** Holds information about button mapping. */
+
+ abstract static class MappingInfo {
+
+ /** The Ui slot into which a given button desires to be placed. */
+ public abstract int getSlot();
+
+ /**
+ * Returns an integer used to determine which button is chosen for a slot when multiple buttons
+ * desire to be placed in the same slot. Follows from the natural ordering of integers, i.e. a
+ * lower slotOrder results in the button being chosen.
+ */
+ public abstract int getSlotOrder();
+
+ /**
+ * Returns an integer used to determine the order in which buttons that weren't chosen for their
+ * desired slot are placed into the Ui. Follows from the natural ordering of integers, i.e. a
+ * lower conflictOrder results in the button being chosen.
+ */
+ public abstract int getConflictOrder();
+
+ static Builder builder(int slot) {
+ return new AutoValue_MappedButtonConfig_MappingInfo.Builder()
+ .setSlot(slot)
+ .setSlotOrder(Integer.MAX_VALUE)
+ .setConflictOrder(Integer.MAX_VALUE);
+ }
+
+ /** Class used to build instances of {@link MappingInfo}. */
+
+ abstract static class Builder {
+ public abstract Builder setSlot(int slot);
+
+ public abstract Builder setSlotOrder(int slotOrder);
+
+ public abstract Builder setConflictOrder(int conflictOrder);
+
+ public abstract MappingInfo build();
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml b/java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml
new file mode 100644
index 000000000..69215adda
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/animator/incall_button_elevation.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:state_enabled="true"
+ android:state_pressed="true">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="8dp"
+ android:valueType="floatType"/>
+
+ </item>
+ <item
+ android:state_checked="true"
+ android:state_enabled="true"
+ android:state_pressed="false">
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="4dp"
+ android:valueType="floatType"/>
+
+ </item>
+ <item>
+ <objectAnimator
+ android:duration="@android:integer/config_shortAnimTime"
+ android:propertyName="translationZ"
+ android:valueTo="0dp"
+ android:valueType="floatType"/>
+ </item>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
new file mode 100644
index 000000000..6d8556759
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/color/incall_button_icon.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#FF01579B" android:state_checked="true"/>
+ <item android:color="#FFFFFFFF"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png b/java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png
new file mode 100644
index 000000000..a60805258
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable-mdpi/ic_addcall_white.png
Binary files differ
diff --git a/java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png b/java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png
new file mode 100644
index 000000000..d2a843c38
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable-xhdpi/ic_addcall_white.png
Binary files differ
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml
new file mode 100644
index 000000000..c8bd29568
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/incall_button_background_checked"
+ android:state_checked="true"/>
+ <item android:drawable="@drawable/incall_button_background_unchecked"/>
+ </selector>
+ </item>
+ <item>
+ <ripple android:color="@color/incall_button_ripple">
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+</layer-list>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml
new file mode 100644
index 000000000..73c6947e2
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_checked.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@color/incall_button_white"/>
+</shape>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml
new file mode 100644
index 000000000..6755f0fae
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_more.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/incall_button_background_checked"
+ android:state_checked="true"/>
+ <item android:drawable="@drawable/incall_button_background_unchecked"/>
+ </selector>
+ </item>
+ <item>
+ <ripple android:color="@color/incall_button_ripple">
+ <item
+ android:id="@android:id/mask"
+ android:gravity="center">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </item>
+ </ripple>
+ </item>
+
+ <!-- This adds a little down arrow to indicate that the button will pop up a menu. Use an explicit
+ <bitmap> to avoid scaling the icon up to the full size of the button. -->
+ <item>
+ <bitmap
+ android:gravity="end"
+ android:src="@drawable/quantum_ic_arrow_drop_down_white_18"/>
+ </item>
+</layer-list>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml
new file mode 100644
index 000000000..f7ffa4d50
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_button_background_unchecked.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="oval">
+ <solid android:color="@android:color/transparent"/>
+</shape>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml
new file mode 100644
index 000000000..4daf0527c
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_add_call.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_addcall_white"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml
new file mode 100644
index 000000000..091142bef
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_dialpad.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_dialpad_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml
new file mode 100644
index 000000000..a48e4c4ed
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_manage.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_group_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml
new file mode 100644
index 000000000..61d75556e
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_merge.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_call_merge_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml
new file mode 100644
index 000000000..6aa8ab8ce
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/incall_ic_pause.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/quantum_ic_pause_white_36"/>
+</selector>
diff --git a/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml
new file mode 100644
index 000000000..6a55b35dc
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_default.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:innerRadius="0dp"
+ android:shape="ring"
+ android:thickness="2dp"
+ android:useLevel="false">
+ <solid android:color="@android:color/darker_gray"/>
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml
new file mode 100644
index 000000000..fc673c6ed
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/tab_indicator_selected.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item>
+ <shape
+ android:innerRadius="0dp"
+ android:shape="ring"
+ android:thickness="4dp"
+ android:useLevel="false">
+ <solid android:color="@color/background_dialer_white"/>
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml b/java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml
new file mode 100644
index 000000000..303a49bd8
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/drawable/tab_selector.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/tab_indicator_selected"
+ android:state_selected="true"/>
+ <item android:drawable="@drawable/tab_indicator_default"/>
+</selector> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml b/java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml
new file mode 100644
index 000000000..335ac8ae2
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/layout/call_composer_data_fragment.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <TextView
+ android:id="@+id/subject"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:padding="8dp"
+ android:textSize="24sp"
+ android:textColor="@color/primary_text_color"
+ android:background="@color/background_dialer_white"/>
+</FrameLayout> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
new file mode 100644
index 000000000..9b950462c
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/layout/frag_incall_voice.xml
@@ -0,0 +1,104 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:fitsSystemWindows="true">
+
+ <LinearLayout
+ android:id="@id/incall_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:layout_marginStart="@dimen/incall_window_margin_horizontal"
+ android:layout_marginEnd="@dimen/incall_window_margin_horizontal"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+
+ <ImageView
+ android:id="@id/contactgrid_avatar"
+ android:layout_width="@dimen/incall_avatar_size"
+ android:layout_height="@dimen/incall_avatar_size"
+ android:layout_marginBottom="8dp"
+ android:elevation="2dp"/>
+
+ <include
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ app:autoResizeText_minTextSize="28sp"
+ tools:text="Jake Peralta"
+ tools:ignore="Deprecated"/>
+
+ <include
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <FrameLayout
+ android:id="@+id/incall_location_holder"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+ </LinearLayout>
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/incall_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_above="@+id/incall_tab_dots"
+ android:layout_below="@+id/incall_contact_grid"
+ android:layout_centerHorizontal="true"/>
+
+ <android.support.design.widget.TabLayout
+ android:id="@+id/incall_tab_dots"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/incall_end_call"
+ android:visibility="gone"
+ app:tabBackground="@drawable/tab_selector"
+ app:tabGravity="center"
+ app:tabIndicatorHeight="0dp"/>
+
+ <FrameLayout
+ android:id="@+id/incall_dialpad_container"
+ style="@style/DialpadContainer"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ tools:background="@android:color/white"
+ tools:visibility="gone"/>
+ <ImageButton
+ android:id="@+id/incall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="36dp"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/incall_content_description_end_call"/>
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@id/incall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml b/java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml
new file mode 100644
index 000000000..59e99440e
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/layout/incall_button_grid.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/incall_window_margin_horizontal"
+ android:layout_marginEnd="@dimen/incall_window_margin_horizontal"
+ tools:showIn="@layout/frag_incall_voice">
+ <GridLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:columnCount="3"
+ android:orientation="horizontal">
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_first_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:enabled="false"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_mute"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_second_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:enabled="false"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_dialpad"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_third_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:enabled="false"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_speaker"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_fourth_button"
+ android:layout_marginTop="@dimen/incall_button_vertical_padding"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_add_call"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_fifth_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:layout_marginTop="@dimen/incall_button_vertical_padding"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_hold"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ <com.android.incallui.incall.impl.CheckableLabeledButton
+ android:id="@+id/incall_sixth_button"
+ android:layout_width="0dp"
+ android:layout_columnWeight="1"
+ android:layout_marginTop="@dimen/incall_button_vertical_padding"
+ android:gravity="center"
+ app:incall_labelText="@string/incall_label_videocall"
+ tools:background="#FFFF0000"
+ tools:layout_height="@dimen/tools_button_height"
+ tools:layout_width="@dimen/incall_labeled_button_size"/>
+ </GridLayout>
+</FrameLayout>
diff --git a/java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml
new file mode 100644
index 000000000..1fe0c4db9
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h320dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <bool name="incall_dialpad_allowed">true</bool>
+ <integer name="incall_num_rows">1</integer>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml
new file mode 100644
index 000000000..aac42c563
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h385dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_avatar_size">64dp</dimen>
+ <dimen name="incall_avatar_marginBottom">8dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml
new file mode 100644
index 000000000..ef1a800ac
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h480dp/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <integer name="incall_num_rows">2</integer>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml
new file mode 100644
index 000000000..1f37cd504
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h580dp/dimens.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_avatar_size">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml b/java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml
new file mode 100644
index 000000000..b58ef4819
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-h580dp/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+
+ <style name="DialpadContainer">
+ <item name="android:layout_below">@id/incall_contact_grid</item>
+ <item name="android:layout_marginTop">8dp</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml
new file mode 100644
index 000000000..e73eb934c
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-w260dp-h520dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_button_horizontal_padding">16dp</dimen>
+ <dimen name="incall_button_vertical_padding">16dp</dimen>
+ <dimen name="incall_labeled_button_size">64dp</dimen>
+ <dimen name="tools_button_height">92dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml b/java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml
new file mode 100644
index 000000000..502ae72dc
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values-w300dp-h540dp/dimens.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_button_horizontal_padding">32dp</dimen>
+ <dimen name="incall_button_vertical_padding">32dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/attrs.xml b/java/com/android/incallui/incall/impl/res/values/attrs.xml
new file mode 100644
index 000000000..ed1b2a853
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="CheckableLabeledButton">
+ <attr format="reference" name="incall_icon"/>
+ <attr format="string|reference" name="incall_labelText"/>
+ <attr name="android:enabled"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/dimens.xml b/java/com/android/incallui/incall/impl/res/values/dimens.xml
new file mode 100644
index 000000000..249788785
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/dimens.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="incall_button_label_margin">8dp</dimen>
+ <dimen name="incall_button_elevation">0dp</dimen>
+ <dimen name="incall_end_call_spacing">116dp</dimen>
+ <dimen name="incall_button_padding">4dp</dimen>
+ <dimen name="incall_button_horizontal_padding">8dp</dimen>
+ <dimen name="incall_button_vertical_padding">8dp</dimen>
+ <dimen name="incall_avatar_size">0dp</dimen>
+ <dimen name="incall_avatar_marginBottom">0dp</dimen>
+ <dimen name="incall_labeled_button_size">48dp</dimen>
+ <dimen name="tools_button_height">76dp</dimen>
+ <dimen name="incall_window_margin_horizontal">24dp</dimen>
+
+ <bool name="incall_dialpad_allowed">false</bool>
+ <integer name="incall_num_rows">0</integer>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/ids.xml b/java/com/android/incallui/incall/impl/res/values/ids.xml
new file mode 100644
index 000000000..e1368f95d
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/ids.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="incall_on_hold_banner" type="id"/>
+ <item name="incall_button_grid" type="id"/>
+ <item name="incall_contact_grid" type="id"/>
+</resources>
diff --git a/java/com/android/incallui/incall/impl/res/values/strings.xml b/java/com/android/incallui/incall/impl/res/values/strings.xml
new file mode 100644
index 000000000..054ca9687
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/strings.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Button shown during a phone call to upgrade to video.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_videocall">Video call</string>
+
+ <!-- Button shown during a phone call to put the call on hold.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_hold">Hold</string>
+
+ <!-- Button shown during a phone call to add a new phone call.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_add_call">Add call</string>
+
+ <!-- Button shown during a phone call to mute the microphone.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_mute">Mute</string>
+
+ <!-- Button shown during a phone call to show the dialpad.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_dialpad">Keypad</string>
+
+ <!-- Button shown during a phone to route audio from earpiece to speaker phone.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_speaker">Speaker</string>
+
+ <!-- Talkback text for speaker button status. [CHAR LIMIT=12] -->
+ <string name="incall_talkback_speaker_on">, is on</string>
+
+ <!-- Talkback text for speaker button status. [CHAR LIMIT=12] -->
+ <string name="incall_talkback_speaker_off">, is Off</string>
+
+ <!-- Button shown during a phone to merge two ongoing calls.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_merge">Merge</string>
+
+ <!-- Button shown during a phone to show the manage conference call screen.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_manage">Manage</string>
+
+ <string name="a11y_description_incall_label_manage_content">Manage callers</string>
+
+ <!-- Button shown during a phone to swap from the foreground call to the background call.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_swap">Swap</string>
+
+ <!-- Button shown during a phone to switch the audio route.
+ [CHAR LIMIT=12] -->
+ <string name="incall_label_audio">Sound</string>
+
+ <!-- Used to inform the user that the note associated with an outgoing call has been sent.
+ [CHAR LIMIT=32] -->
+ <string name="incall_note_sent">Note sent</string>
+
+</resources> \ No newline at end of file
diff --git a/java/com/android/incallui/incall/impl/res/values/styles.xml b/java/com/android/incallui/incall/impl/res/values/styles.xml
new file mode 100644
index 000000000..2392574a3
--- /dev/null
+++ b/java/com/android/incallui/incall/impl/res/values/styles.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+
+ <style name="DialpadContainer">
+ <item name="android:layout_alignParentTop">true</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/incall/protocol/ContactPhotoType.java b/java/com/android/incallui/incall/protocol/ContactPhotoType.java
new file mode 100644
index 000000000..d79b7550b
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/ContactPhotoType.java
@@ -0,0 +1,35 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Types of contact photos we can have. */
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ ContactPhotoType.BUSINESS,
+ ContactPhotoType.CONTACT,
+})
+public @interface ContactPhotoType {
+
+ int DEFAULT_PLACEHOLDER = 0;
+ int BUSINESS = 1;
+ int CONTACT = 2;
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
new file mode 100644
index 000000000..50ebc6413
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
@@ -0,0 +1,59 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Ids for buttons in the in call UI. */
+@Retention(RetentionPolicy.SOURCE)
+@IntDef({
+ InCallButtonIds.BUTTON_AUDIO,
+ InCallButtonIds.BUTTON_MUTE,
+ InCallButtonIds.BUTTON_DIALPAD,
+ InCallButtonIds.BUTTON_HOLD,
+ InCallButtonIds.BUTTON_SWAP,
+ InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO,
+ InCallButtonIds.BUTTON_SWITCH_CAMERA,
+ InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO,
+ InCallButtonIds.BUTTON_ADD_CALL,
+ InCallButtonIds.BUTTON_MERGE,
+ InCallButtonIds.BUTTON_PAUSE_VIDEO,
+ InCallButtonIds.BUTTON_MANAGE_VIDEO_CONFERENCE,
+ InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
+ InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
+ InCallButtonIds.BUTTON_COUNT,
+})
+public @interface InCallButtonIds {
+
+ int BUTTON_AUDIO = 0;
+ int BUTTON_MUTE = 1;
+ int BUTTON_DIALPAD = 2;
+ int BUTTON_HOLD = 3;
+ int BUTTON_SWAP = 4;
+ int BUTTON_UPGRADE_TO_VIDEO = 5;
+ int BUTTON_SWITCH_CAMERA = 6;
+ int BUTTON_DOWNGRADE_TO_AUDIO = 7;
+ int BUTTON_ADD_CALL = 8;
+ int BUTTON_MERGE = 9;
+ int BUTTON_PAUSE_VIDEO = 10;
+ int BUTTON_MANAGE_VIDEO_CONFERENCE = 11;
+ int BUTTON_MANAGE_VOICE_CONFERENCE = 12;
+ int BUTTON_SWITCH_TO_SECONDARY = 13;
+ int BUTTON_COUNT = 14;
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
new file mode 100644
index 000000000..6d802e346
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
@@ -0,0 +1,61 @@
+/*
+ * 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.incallui.incall.protocol;
+
+/** Utility class for {@link InCallButtonIds}. */
+public class InCallButtonIdsExtension {
+
+ /**
+ * Converts the given {@link InCallButtonIds} to a human readable string.
+ *
+ * @param id the id to convert.
+ * @return the human readable string.
+ */
+ public static String toString(@InCallButtonIds int id) {
+ if (id == InCallButtonIds.BUTTON_AUDIO) {
+ return "AUDIO";
+ } else if (id == InCallButtonIds.BUTTON_MUTE) {
+ return "MUTE";
+ } else if (id == InCallButtonIds.BUTTON_DIALPAD) {
+ return "DIALPAD";
+ } else if (id == InCallButtonIds.BUTTON_HOLD) {
+ return "HOLD";
+ } else if (id == InCallButtonIds.BUTTON_SWAP) {
+ return "SWAP";
+ } else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_VIDEO) {
+ return "UPGRADE_TO_VIDEO";
+ } else if (id == InCallButtonIds.BUTTON_DOWNGRADE_TO_AUDIO) {
+ return "DOWNGRADE_TO_AUDIO";
+ } else if (id == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
+ return "SWITCH_CAMERA";
+ } else if (id == InCallButtonIds.BUTTON_ADD_CALL) {
+ return "ADD_CALL";
+ } else if (id == InCallButtonIds.BUTTON_MERGE) {
+ return "MERGE";
+ } else if (id == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ return "PAUSE_VIDEO";
+ } else if (id == InCallButtonIds.BUTTON_MANAGE_VIDEO_CONFERENCE) {
+ return "MANAGE_VIDEO_CONFERENCE";
+ } else if (id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE) {
+ return "MANAGE_VOICE_CONFERENCE";
+ } else if (id == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ return "SWITCH_TO_SECONDARY";
+ } else {
+ return "INVALID_BUTTON: " + id;
+ }
+ }
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUi.java b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
new file mode 100644
index 000000000..96d741af3
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
@@ -0,0 +1,50 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.support.v4.app.Fragment;
+import android.telecom.CallAudioState;
+
+/** Interface for the call button UI. */
+public interface InCallButtonUi {
+
+ void showButton(@InCallButtonIds int buttonId, boolean show);
+
+ void enableButton(@InCallButtonIds int buttonId, boolean enable);
+
+ void setEnabled(boolean on);
+
+ void setHold(boolean on);
+
+ void setCameraSwitched(boolean isBackFacingCamera);
+
+ void setVideoPaused(boolean isPaused);
+
+ void setAudioState(CallAudioState audioState);
+
+ /**
+ * Once showButton() has been called on each of the individual buttons in the UI, call this to
+ * configure the overflow menu appropriately.
+ */
+ void updateButtonStates();
+
+ void updateInCallButtonUiColors();
+
+ Fragment getInCallButtonUiFragment();
+
+ void showAudioRouteSelector();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
new file mode 100644
index 000000000..5e69f0e2d
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.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.incallui.incall.protocol;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.CallAudioState;
+
+/** Callbacks from the module out to the container. */
+public interface InCallButtonUiDelegate {
+
+ void onInCallButtonUiReady(InCallButtonUi inCallButtonUi);
+
+ void onInCallButtonUiUnready();
+
+ void onSaveInstanceState(Bundle outState);
+
+ void onRestoreInstanceState(Bundle savedInstanceState);
+
+ void refreshMuteState();
+
+ void addCallClicked();
+
+ void muteClicked(boolean checked);
+
+ void mergeClicked();
+
+ void holdClicked(boolean checked);
+
+ void swapClicked();
+
+ void showDialpadClicked(boolean checked);
+
+ void changeToVideoClicked();
+
+ void switchCameraClicked(boolean useFrontFacingCamera);
+
+ void toggleCameraClicked();
+
+ void pauseVideoClicked(boolean pause);
+
+ void toggleSpeakerphone();
+
+ CallAudioState getCurrentAudioState();
+
+ void setAudioRoute(int route);
+
+ void onEndCallClicked();
+
+ void showAudioRouteSelector();
+
+ Context getContext();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java
new file mode 100644
index 000000000..ca7d11951
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.incallui.incall.protocol;
+
+/** Callbacks from the module out to the container. */
+public interface InCallButtonUiDelegateFactory {
+
+ InCallButtonUiDelegate newInCallButtonUiDelegate();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallScreen.java b/java/com/android/incallui/incall/protocol/InCallScreen.java
new file mode 100644
index 000000000..612ad26f5
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallScreen.java
@@ -0,0 +1,53 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+import android.view.accessibility.AccessibilityEvent;
+
+/** Interface for the call card module. */
+public interface InCallScreen {
+
+ void setPrimary(@NonNull PrimaryInfo primaryInfo);
+
+ void setSecondary(@NonNull SecondaryInfo secondaryInfo);
+
+ void setCallState(@NonNull PrimaryCallState primaryCallState);
+
+ void setEndCallButtonEnabled(boolean enabled, boolean animate);
+
+ void showManageConferenceCallButton(boolean visible);
+
+ boolean isManageConferenceVisible();
+
+ void dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+
+ void showNoteSentToast();
+
+ void updateInCallScreenColors();
+
+ void onInCallScreenDialpadVisibilityChange(boolean isShowing);
+
+ int getAnswerAndDialpadContainerResourceId();
+
+ void showLocationUi(Fragment locationUi);
+
+ boolean isShowingLocationUi();
+
+ Fragment getInCallScreenFragment();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallScreenDelegate.java b/java/com/android/incallui/incall/protocol/InCallScreenDelegate.java
new file mode 100644
index 000000000..b39f9f4a2
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallScreenDelegate.java
@@ -0,0 +1,43 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.graphics.drawable.Drawable;
+
+/** Callbacks from the module out to the container. */
+public interface InCallScreenDelegate {
+
+ void onInCallScreenDelegateInit(InCallScreen inCallScreen);
+
+ void onInCallScreenReady();
+
+ void onInCallScreenUnready();
+
+ void onEndCallClicked();
+
+ void onSecondaryInfoClicked();
+
+ void onCallStateButtonClicked();
+
+ void onManageConferenceClicked();
+
+ void onShrinkAnimationComplete();
+
+ void onInCallScreenResumed();
+
+ Drawable getDefaultContactPhotoDrawable();
+}
diff --git a/java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java b/java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java
new file mode 100644
index 000000000..6706691c8
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/InCallScreenDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.incallui.incall.protocol;
+
+/** Callbacks from the module out to the container. */
+public interface InCallScreenDelegateFactory {
+
+ InCallScreenDelegate newInCallScreenDelegate();
+}
diff --git a/java/com/android/incallui/incall/protocol/PrimaryCallState.java b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
new file mode 100644
index 000000000..782090832
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/PrimaryCallState.java
@@ -0,0 +1,114 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.graphics.drawable.Drawable;
+import android.telecom.DisconnectCause;
+import android.telecom.VideoProfile;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Locale;
+
+/** State of the primary call. */
+public class PrimaryCallState {
+ public final int state;
+ public final int videoState;
+ @SessionModificationState public final int sessionModificationState;
+ public final DisconnectCause disconnectCause;
+ public final String connectionLabel;
+ public final Drawable connectionIcon;
+ public final String gatewayNumber;
+ public final String callSubject;
+ public final String callbackNumber;
+ public final boolean isWifi;
+ public final boolean isConference;
+ public final boolean isWorkCall;
+ public final boolean isHdAudioCall;
+ public final boolean isForwardedNumber;
+ public final boolean shouldShowContactPhoto;
+ public final long connectTimeMillis;
+ public final boolean isVoiceMailNumber;
+ public final boolean isRemotelyHeld;
+
+ // TODO: Convert to autovalue. b/34502119
+ public static PrimaryCallState createEmptyPrimaryCallState() {
+ return new PrimaryCallState(
+ DialerCall.State.IDLE,
+ VideoProfile.STATE_AUDIO_ONLY,
+ DialerCall.SESSION_MODIFICATION_STATE_NO_REQUEST,
+ new DisconnectCause(DisconnectCause.UNKNOWN),
+ null, /* connectionLabel */
+ null, /* connectionIcon */
+ null, /* gatewayNumber */
+ null, /* callSubject */
+ null, /* callbackNumber */
+ false /* isWifi */,
+ false /* isConference */,
+ false /* isWorkCall */,
+ false /* isHdAudioCall */,
+ false /* isForwardedNumber */,
+ false /* shouldShowContactPhoto */,
+ 0,
+ false /* isVoiceMailNumber */,
+ false /* isRemotelyHeld */);
+ }
+
+ public PrimaryCallState(
+ int state,
+ int videoState,
+ @SessionModificationState int sessionModificationState,
+ DisconnectCause disconnectCause,
+ String connectionLabel,
+ Drawable connectionIcon,
+ String gatewayNumber,
+ String callSubject,
+ String callbackNumber,
+ boolean isWifi,
+ boolean isConference,
+ boolean isWorkCall,
+ boolean isHdAudioCall,
+ boolean isForwardedNumber,
+ boolean shouldShowContactPhoto,
+ long connectTimeMillis,
+ boolean isVoiceMailNumber,
+ boolean isRemotelyHeld) {
+ this.state = state;
+ this.videoState = videoState;
+ this.sessionModificationState = sessionModificationState;
+ this.disconnectCause = disconnectCause;
+ this.connectionLabel = connectionLabel;
+ this.connectionIcon = connectionIcon;
+ this.gatewayNumber = gatewayNumber;
+ this.callSubject = callSubject;
+ this.callbackNumber = callbackNumber;
+ this.isWifi = isWifi;
+ this.isConference = isConference;
+ this.isWorkCall = isWorkCall;
+ this.isHdAudioCall = isHdAudioCall;
+ this.isForwardedNumber = isForwardedNumber;
+ this.shouldShowContactPhoto = shouldShowContactPhoto;
+ this.connectTimeMillis = connectTimeMillis;
+ this.isVoiceMailNumber = isVoiceMailNumber;
+ this.isRemotelyHeld = isRemotelyHeld;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US, "PrimaryCallState, state: %d, connectionLabel: %s", state, connectionLabel);
+ }
+}
diff --git a/java/com/android/incallui/incall/protocol/PrimaryInfo.java b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
new file mode 100644
index 000000000..1833ed22e
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/PrimaryInfo.java
@@ -0,0 +1,112 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.multimedia.MultimediaData;
+import java.util.Locale;
+
+/** Information about the primary call. */
+public class PrimaryInfo {
+ @Nullable public final String number;
+ @Nullable public final String name;
+ public final boolean nameIsNumber;
+ // This is from contacts and shows the type of number. For example, "Mobile".
+ @Nullable public final String label;
+ @Nullable public final String location;
+ @Nullable public final Drawable photo;
+ @ContactPhotoType public final int photoType;
+ public final boolean isSipCall;
+ public final boolean isContactPhotoShown;
+ public final boolean isWorkCall;
+ public final boolean isSpam;
+ public final boolean answeringDisconnectsOngoingCall;
+ public final boolean shouldShowLocation;
+ // Used for consistent LetterTile coloring.
+ @Nullable public final String contactInfoLookupKey;
+ @Nullable public final MultimediaData multimediaData;
+
+ // TODO: Convert to autovalue. b/34502119
+ public static PrimaryInfo createEmptyPrimaryInfo() {
+ return new PrimaryInfo(
+ null,
+ null,
+ false,
+ null,
+ null,
+ null,
+ ContactPhotoType.DEFAULT_PLACEHOLDER,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ null,
+ null);
+ }
+
+ public PrimaryInfo(
+ @Nullable String number,
+ @Nullable String name,
+ boolean nameIsNumber,
+ @Nullable String location,
+ @Nullable String label,
+ @Nullable Drawable photo,
+ @ContactPhotoType int phototType,
+ boolean isSipCall,
+ boolean isContactPhotoShown,
+ boolean isWorkCall,
+ boolean isSpam,
+ boolean answeringDisconnectsOngoingCall,
+ boolean shouldShowLocation,
+ @Nullable String contactInfoLookupKey,
+ @Nullable MultimediaData multimediaData) {
+ this.number = number;
+ this.name = name;
+ this.nameIsNumber = nameIsNumber;
+ this.location = location;
+ this.label = label;
+ this.photo = photo;
+ this.photoType = phototType;
+ this.isSipCall = isSipCall;
+ this.isContactPhotoShown = isContactPhotoShown;
+ this.isWorkCall = isWorkCall;
+ this.isSpam = isSpam;
+ this.answeringDisconnectsOngoingCall = answeringDisconnectsOngoingCall;
+ this.shouldShowLocation = shouldShowLocation;
+ this.contactInfoLookupKey = contactInfoLookupKey;
+ this.multimediaData = multimediaData;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "PrimaryInfo, number: %s, name: %s, location: %s, label: %s, "
+ + "photo: %s, photoType: %d, isPhotoVisible: %b",
+ LogUtil.sanitizePhoneNumber(number),
+ LogUtil.sanitizePii(name),
+ LogUtil.sanitizePii(location),
+ label,
+ photo,
+ photoType,
+ isContactPhotoShown);
+ }
+}
diff --git a/java/com/android/incallui/incall/protocol/SecondaryInfo.java b/java/com/android/incallui/incall/protocol/SecondaryInfo.java
new file mode 100644
index 000000000..cadfca6bf
--- /dev/null
+++ b/java/com/android/incallui/incall/protocol/SecondaryInfo.java
@@ -0,0 +1,109 @@
+/*
+ * 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.incallui.incall.protocol;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.dialer.common.LogUtil;
+import java.util.Locale;
+
+/** Information about the secondary call. */
+public class SecondaryInfo implements Parcelable {
+ public final boolean shouldShow;
+ public final String name;
+ public final boolean nameIsNumber;
+ public final String label;
+ public final String providerLabel;
+ public final boolean isConference;
+ public final boolean isVideoCall;
+ public final boolean isFullscreen;
+
+ public static SecondaryInfo createEmptySecondaryInfo(boolean isFullScreen) {
+ return new SecondaryInfo(false, null, false, null, null, false, false, isFullScreen);
+ }
+
+ public SecondaryInfo(
+ boolean shouldShow,
+ String name,
+ boolean nameIsNumber,
+ String label,
+ String providerLabel,
+ boolean isConference,
+ boolean isVideoCall,
+ boolean isFullscreen) {
+ this.shouldShow = shouldShow;
+ this.name = name;
+ this.nameIsNumber = nameIsNumber;
+ this.label = label;
+ this.providerLabel = providerLabel;
+ this.isConference = isConference;
+ this.isVideoCall = isVideoCall;
+ this.isFullscreen = isFullscreen;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "SecondaryInfo, show: %b, name: %s, label: %s, " + "providerLabel: %s",
+ shouldShow,
+ LogUtil.sanitizePii(name),
+ label,
+ providerLabel);
+ }
+
+ protected SecondaryInfo(Parcel in) {
+ shouldShow = in.readByte() != 0;
+ name = in.readString();
+ nameIsNumber = in.readByte() != 0;
+ label = in.readString();
+ providerLabel = in.readString();
+ isConference = in.readByte() != 0;
+ isVideoCall = in.readByte() != 0;
+ isFullscreen = in.readByte() != 0;
+ }
+
+ public static final Creator<SecondaryInfo> CREATOR =
+ new Creator<SecondaryInfo>() {
+ @Override
+ public SecondaryInfo createFromParcel(Parcel in) {
+ return new SecondaryInfo(in);
+ }
+
+ @Override
+ public SecondaryInfo[] newArray(int size) {
+ return new SecondaryInfo[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (shouldShow ? 1 : 0));
+ dest.writeString(name);
+ dest.writeByte((byte) (nameIsNumber ? 1 : 0));
+ dest.writeString(label);
+ dest.writeString(providerLabel);
+ dest.writeByte((byte) (isConference ? 1 : 0));
+ dest.writeByte((byte) (isVideoCall ? 1 : 0));
+ dest.writeByte((byte) (isFullscreen ? 1 : 0));
+ }
+}
diff --git a/java/com/android/incallui/latencyreport/LatencyReport.java b/java/com/android/incallui/latencyreport/LatencyReport.java
new file mode 100644
index 000000000..2e1fbd590
--- /dev/null
+++ b/java/com/android/incallui/latencyreport/LatencyReport.java
@@ -0,0 +1,140 @@
+/*
+ * 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.incallui.latencyreport;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+
+/** Tracks latency information for a call. */
+public class LatencyReport {
+
+ public static final long INVALID_TIME = -1;
+ // The following are hidden constants from android.telecom.TelecomManager.
+ private static final String EXTRA_CALL_CREATED_TIME_MILLIS =
+ "android.telecom.extra.CALL_CREATED_TIME_MILLIS";
+ private static final String EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS =
+ "android.telecom.extra.CALL_TELECOM_ROUTING_START_TIME_MILLIS";
+ private static final String EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS =
+ "android.telecom.extra.CALL_TELECOM_ROUTING_END_TIME_MILLIS";
+ private final boolean mWasIncoming;
+
+ // Time elapsed since boot when the call was created by the connection service.
+ private final long mCreatedTimeMillis;
+
+ // Time elapsed since boot when telecom began processing the call.
+ private final long mTelecomRoutingStartTimeMillis;
+
+ // Time elapsed since boot when telecom finished processing the call. This includes things like
+ // looking up contact info and call blocking but before showing any UI.
+ private final long mTelecomRoutingEndTimeMillis;
+
+ // Time elapsed since boot when the call was added to the InCallUi.
+ private final long mCallAddedTimeMillis;
+
+ // Time elapsed since boot when the call was added and call blocking evaluation was completed.
+ private long mCallBlockingTimeMillis = INVALID_TIME;
+
+ // Time elapsed since boot when the call notification was shown.
+ private long mCallNotificationTimeMillis = INVALID_TIME;
+
+ // Time elapsed since boot when the InCallUI was shown.
+ private long mInCallUiShownTimeMillis = INVALID_TIME;
+
+ // Whether the call was shown to the user as a heads up notification instead of a full screen
+ // UI.
+ private boolean mDidDisplayHeadsUpNotification;
+
+ public LatencyReport() {
+ mWasIncoming = false;
+ mCreatedTimeMillis = INVALID_TIME;
+ mTelecomRoutingStartTimeMillis = INVALID_TIME;
+ mTelecomRoutingEndTimeMillis = INVALID_TIME;
+ mCallAddedTimeMillis = SystemClock.elapsedRealtime();
+ }
+
+ public LatencyReport(android.telecom.Call telecomCall) {
+ mWasIncoming = telecomCall.getState() == android.telecom.Call.STATE_RINGING;
+ Bundle extras = telecomCall.getDetails().getIntentExtras();
+ if (extras == null) {
+ mCreatedTimeMillis = INVALID_TIME;
+ mTelecomRoutingStartTimeMillis = INVALID_TIME;
+ mTelecomRoutingEndTimeMillis = INVALID_TIME;
+ } else {
+ mCreatedTimeMillis = extras.getLong(EXTRA_CALL_CREATED_TIME_MILLIS, INVALID_TIME);
+ mTelecomRoutingStartTimeMillis =
+ extras.getLong(EXTRA_CALL_TELECOM_ROUTING_START_TIME_MILLIS, INVALID_TIME);
+ mTelecomRoutingEndTimeMillis =
+ extras.getLong(EXTRA_CALL_TELECOM_ROUTING_END_TIME_MILLIS, INVALID_TIME);
+ }
+ mCallAddedTimeMillis = SystemClock.elapsedRealtime();
+ }
+
+ public boolean getWasIncoming() {
+ return mWasIncoming;
+ }
+
+ public long getCreatedTimeMillis() {
+ return mCreatedTimeMillis;
+ }
+
+ public long getTelecomRoutingStartTimeMillis() {
+ return mTelecomRoutingStartTimeMillis;
+ }
+
+ public long getTelecomRoutingEndTimeMillis() {
+ return mTelecomRoutingEndTimeMillis;
+ }
+
+ public long getCallAddedTimeMillis() {
+ return mCallAddedTimeMillis;
+ }
+
+ public long getCallBlockingTimeMillis() {
+ return mCallBlockingTimeMillis;
+ }
+
+ public void onCallBlockingDone() {
+ if (mCallBlockingTimeMillis == INVALID_TIME) {
+ mCallBlockingTimeMillis = SystemClock.elapsedRealtime();
+ }
+ }
+
+ public long getCallNotificationTimeMillis() {
+ return mCallNotificationTimeMillis;
+ }
+
+ public void onNotificationShown() {
+ if (mCallNotificationTimeMillis == INVALID_TIME) {
+ mCallNotificationTimeMillis = SystemClock.elapsedRealtime();
+ }
+ }
+
+ public long getInCallUiShownTimeMillis() {
+ return mInCallUiShownTimeMillis;
+ }
+
+ public void onInCallUiShown(boolean forFullScreenIntent) {
+ if (mInCallUiShownTimeMillis == INVALID_TIME) {
+ mInCallUiShownTimeMillis = SystemClock.elapsedRealtime();
+ mDidDisplayHeadsUpNotification = mWasIncoming && !forFullScreenIntent;
+ }
+ }
+
+ public boolean getDidDisplayHeadsUpNotification() {
+ return mDidDisplayHeadsUpNotification;
+ }
+}
diff --git a/java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java b/java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.java
new file mode 100644
index 000000000..9b5335b69
--- /dev/null
+++ b/java/com/android/incallui/legacyblocking/BlockedNumberContentObserver.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.incallui.legacyblocking;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.provider.CallLog;
+import android.support.annotation.NonNull;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import java.util.Objects;
+
+/**
+ * Observes the {@link CallLog} to delete the CallLog entry for a blocked call after it is added.
+ * Automatically de-registers itself {@link #TIMEOUT_MS} ms after registration or if the entry is
+ * found and deleted.
+ */
+public class BlockedNumberContentObserver extends ContentObserver
+ implements DeleteBlockedCallTask.Listener {
+
+ /**
+ * The time after which a {@link BlockedNumberContentObserver} will be automatically unregistered.
+ */
+ public static final int TIMEOUT_MS = 5000;
+
+ @NonNull private final Context context;
+ @NonNull private final Handler handler;
+ private final String number;
+ private final long timeAddedMillis;
+ private final Runnable timeoutRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ unregister();
+ }
+ };
+
+ private final AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+
+ /**
+ * Creates the BlockedNumberContentObserver to delete the new {@link CallLog} entry from the given
+ * blocked number.
+ *
+ * @param number The blocked number.
+ * @param timeAddedMillis The time at which the call from the blocked number was placed.
+ */
+ public BlockedNumberContentObserver(
+ @NonNull Context context, @NonNull Handler handler, String number, long timeAddedMillis) {
+ super(handler);
+ this.context = Objects.requireNonNull(context, "context").getApplicationContext();
+ this.handler = Objects.requireNonNull(handler);
+ this.number = number;
+ this.timeAddedMillis = timeAddedMillis;
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ LogUtil.i(
+ "BlockedNumberContentObserver.onChange",
+ "attempting to remove call log entry from blocked number");
+ asyncTaskExecutor.submit(
+ DeleteBlockedCallTask.IDENTIFIER,
+ new DeleteBlockedCallTask(context, this, number, timeAddedMillis));
+ }
+
+ @Override
+ public void onDeleteBlockedCallTaskComplete(boolean didFindEntry) {
+ if (didFindEntry) {
+ unregister();
+ }
+ }
+
+ /**
+ * Registers this {@link ContentObserver} to listen for changes to the {@link CallLog}. If the
+ * CallLog entry is not found before {@link #TIMEOUT_MS}, this ContentObserver automatically
+ * un-registers itself.
+ */
+ public void register() {
+ LogUtil.i("BlockedNumberContentObserver.register", null);
+ context.getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true, this);
+ handler.postDelayed(timeoutRunnable, TIMEOUT_MS);
+ }
+
+ private void unregister() {
+ LogUtil.i("BlockedNumberContentObserver.unregister", null);
+ handler.removeCallbacks(timeoutRunnable);
+ context.getContentResolver().unregisterContentObserver(this);
+ }
+}
diff --git a/java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java b/java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java
new file mode 100644
index 000000000..a3f2dfa4d
--- /dev/null
+++ b/java/com/android/incallui/legacyblocking/DeleteBlockedCallTask.java
@@ -0,0 +1,124 @@
+/*
+ * 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.incallui.legacyblocking;
+
+import android.Manifest.permission;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.support.v4.content.ContextCompat;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import java.util.Objects;
+
+/**
+ * Deletes a blocked call from the call log. This is only used on Android Marshmallow. On later
+ * versions of the OS, call blocking is implemented in the system and there's no need to mess with
+ * the call log.
+ */
+@TargetApi(VERSION_CODES.M)
+public class DeleteBlockedCallTask extends AsyncTask<Void, Void, Long> {
+
+ public static final String IDENTIFIER = "DeleteBlockedCallTask";
+
+ // Try to identify if a call log entry corresponds to a number which was blocked. We match by
+ // by comparing its creation time to the time it was added in the InCallUi and seeing if they
+ // fall within a certain threshold.
+ private static final int MATCH_BLOCKED_CALL_THRESHOLD_MS = 3000;
+
+ private final Context context;
+ private final Listener listener;
+ private final String number;
+ private final long timeAddedMillis;
+
+ /**
+ * Creates the task to delete the new {@link CallLog} entry from the given blocked number.
+ *
+ * @param number The blocked number.
+ * @param timeAddedMillis The time at which the call from the blocked number was placed.
+ */
+ public DeleteBlockedCallTask(
+ Context context, Listener listener, String number, long timeAddedMillis) {
+ this.context = Objects.requireNonNull(context);
+ this.listener = Objects.requireNonNull(listener);
+ this.number = number;
+ this.timeAddedMillis = timeAddedMillis;
+ }
+
+ @Override
+ public Long doInBackground(Void... params) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_CALL_LOG)
+ != PackageManager.PERMISSION_GRANTED
+ || ContextCompat.checkSelfPermission(context, permission.WRITE_CALL_LOG)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("DeleteBlockedCallTask.doInBackground", "missing call log permissions");
+ return -1L;
+ }
+
+ // First, lookup the call log entry of the most recent call with this number.
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ TelecomUtil.getCallLogUri(context),
+ CallLogDeleteBlockedCallQuery.PROJECTION,
+ CallLog.Calls.NUMBER + "= ?",
+ new String[] {number},
+ CallLog.Calls.DATE + " DESC LIMIT 1")) {
+
+ // If match is found, delete this call log entry and return the call log entry id.
+ if (cursor != null && cursor.moveToFirst()) {
+ long creationTime = cursor.getLong(CallLogDeleteBlockedCallQuery.DATE_COLUMN_INDEX);
+ if (timeAddedMillis > creationTime
+ && timeAddedMillis - creationTime < MATCH_BLOCKED_CALL_THRESHOLD_MS) {
+ long callLogEntryId = cursor.getLong(CallLogDeleteBlockedCallQuery.ID_COLUMN_INDEX);
+ context
+ .getContentResolver()
+ .delete(
+ TelecomUtil.getCallLogUri(context),
+ CallLog.Calls._ID + " IN (" + callLogEntryId + ")",
+ null);
+ return callLogEntryId;
+ }
+ }
+ }
+ return -1L;
+ }
+
+ @Override
+ public void onPostExecute(Long callLogEntryId) {
+ listener.onDeleteBlockedCallTaskComplete(callLogEntryId >= 0);
+ }
+
+ /** Callback invoked when delete is complete. */
+ public interface Listener {
+
+ void onDeleteBlockedCallTaskComplete(boolean didFindEntry);
+ }
+
+ private static class CallLogDeleteBlockedCallQuery {
+
+ static final String[] PROJECTION = new String[] {CallLog.Calls._ID, CallLog.Calls.DATE};
+
+ static final int ID_COLUMN_INDEX = 0;
+ static final int DATE_COLUMN_INDEX = 1;
+ }
+}
diff --git a/java/com/android/incallui/maps/StaticMapBinding.java b/java/com/android/incallui/maps/StaticMapBinding.java
new file mode 100644
index 000000000..9d24ef27a
--- /dev/null
+++ b/java/com/android/incallui/maps/StaticMapBinding.java
@@ -0,0 +1,51 @@
+/*
+ * 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.incallui.maps;
+
+import android.app.Application;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+/** Utility for getting a {@link StaticMapFactory} */
+public class StaticMapBinding {
+
+ @Nullable
+ public static StaticMapFactory get(@NonNull Application application) {
+ if (useTestingInstance) {
+ return testingInstance;
+ }
+ if (application instanceof StaticMapFactory) {
+ return ((StaticMapFactory) application);
+ }
+ return null;
+ }
+
+ private static StaticMapFactory testingInstance;
+ private static boolean useTestingInstance;
+
+ @VisibleForTesting
+ public static void setForTesting(@Nullable StaticMapFactory staticMapFactory) {
+ testingInstance = staticMapFactory;
+ useTestingInstance = true;
+ }
+
+ @VisibleForTesting
+ public static void clearForTesting() {
+ useTestingInstance = false;
+ }
+}
diff --git a/java/com/android/incallui/maps/StaticMapFactory.java b/java/com/android/incallui/maps/StaticMapFactory.java
new file mode 100644
index 000000000..a35013886
--- /dev/null
+++ b/java/com/android/incallui/maps/StaticMapFactory.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.incallui.maps;
+
+import android.location.Location;
+import android.support.annotation.NonNull;
+import android.support.v4.app.Fragment;
+
+/** A Factory that can create Fragments for showing a static map */
+public interface StaticMapFactory {
+
+ @NonNull
+ Fragment getStaticMap(@NonNull Location location);
+}
diff --git a/java/com/android/incallui/res/anim/activity_open_enter.xml b/java/com/android/incallui/res/anim/activity_open_enter.xml
new file mode 100644
index 000000000..71cc096b9
--- /dev/null
+++ b/java/com/android/incallui/res/anim/activity_open_enter.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shareInterpolator="false"
+ android:zAdjustment="top">
+ <alpha
+ android:duration="300"
+ android:fillAfter="true"
+ android:fillBefore="false"
+ android:fillEnabled="true"
+ android:fromAlpha="0.0"
+ android:interpolator="@anim/decelerate_cubic"
+ android:toAlpha="1.0"/>
+ <scale
+ android:duration="300"
+ android:fillAfter="true"
+ android:fillBefore="false"
+ android:fillEnabled="true"
+ android:fromXScale=".8"
+ android:fromYScale=".8"
+ android:interpolator="@anim/decelerate_cubic"
+ android:pivotX="50%p"
+ android:pivotY="50%p"
+ android:toXScale="1.0"
+ android:toYScale="1.0"/>
+</set> \ No newline at end of file
diff --git a/java/com/android/incallui/res/anim/activity_open_exit.xml b/java/com/android/incallui/res/anim/activity_open_exit.xml
new file mode 100644
index 000000000..9b36bb358
--- /dev/null
+++ b/java/com/android/incallui/res/anim/activity_open_exit.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:background="#ff000000"
+ android:zAdjustment="normal">
+ <alpha
+ android:duration="300"
+ android:fillAfter="true"
+ android:fillBefore="false"
+ android:fillEnabled="true"
+ android:fromAlpha="1.0"
+ android:interpolator="@anim/decelerate_quint"
+ android:toAlpha="0.0"/>
+</set> \ No newline at end of file
diff --git a/java/com/android/incallui/res/anim/decelerate_cubic.xml b/java/com/android/incallui/res/anim/decelerate_cubic.xml
new file mode 100644
index 000000000..c2f41597b
--- /dev/null
+++ b/java/com/android/incallui/res/anim/decelerate_cubic.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:factor="1.5"/>
diff --git a/java/com/android/incallui/res/anim/decelerate_quint.xml b/java/com/android/incallui/res/anim/decelerate_quint.xml
new file mode 100644
index 000000000..e55e99c0b
--- /dev/null
+++ b/java/com/android/incallui/res/anim/decelerate_quint.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<decelerateInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
+ android:factor="2.5"/>
diff --git a/java/com/android/incallui/res/anim/on_going_call.xml b/java/com/android/incallui/res/anim/on_going_call.xml
new file mode 100644
index 000000000..3a2e2ba1a
--- /dev/null
+++ b/java/com/android/incallui/res/anim/on_going_call.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
+ android:oneshot="false">
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_01"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_02"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_03"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_04"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_05"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_06"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_07"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_08"
+ android:duration="200"/>
+ <item
+ android:drawable="@drawable/ic_ongoing_phone_24px_09"
+ android:duration="200"/>
+</animation-list> \ No newline at end of file
diff --git a/java/com/android/incallui/res/color/ota_title_color.xml b/java/com/android/incallui/res/color/ota_title_color.xml
new file mode 100644
index 000000000..bf36f56b9
--- /dev/null
+++ b/java/com/android/incallui/res/color/ota_title_color.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#FFA6C839"/>
+</selector>
+
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..1e9294c12
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..757d339c4
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..4e3dbf55d
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..9ab350e9a
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..7c281c3f5
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..bc2b3d2f8
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..fa936cbdc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..ef5137976
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..3712d164d
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..c6a4216a3
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..e4ff6db13
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..185d03393
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..a2177f58a
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png
new file mode 100644
index 000000000..bd9489c85
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..f3581d104
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_business.png b/java/com/android/incallui/res/drawable-hdpi/img_business.png
new file mode 100644
index 000000000..f70634262
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_conference.png b/java/com/android/incallui/res/drawable-hdpi/img_conference.png
new file mode 100644
index 000000000..3d9f683a5
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_no_image.png b/java/com/android/incallui/res/drawable-hdpi/img_no_image.png
new file mode 100644
index 000000000..fd0ab3211
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-hdpi/img_phone.png b/java/com/android/incallui/res/drawable-hdpi/img_phone.png
new file mode 100644
index 000000000..748312e6e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-hdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..edd666b73
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..17eb4824e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..cb7ee1f35
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..73faf52eb
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..933eb5148
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..67b2b1622
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..46abea337
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..0d787ffa4
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..2da4b40d6
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..a34cf4d56
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..ae31e047e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..ec3237086
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..7dc920b2b
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png
new file mode 100644
index 000000000..594d0b9f7
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..501ee842e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_business.png b/java/com/android/incallui/res/drawable-mdpi/img_business.png
new file mode 100644
index 000000000..90738a7ee
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_conference.png b/java/com/android/incallui/res/drawable-mdpi/img_conference.png
new file mode 100644
index 000000000..0694dbd55
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_no_image.png b/java/com/android/incallui/res/drawable-mdpi/img_no_image.png
new file mode 100644
index 000000000..014a1c414
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-mdpi/img_phone.png b/java/com/android/incallui/res/drawable-mdpi/img_phone.png
new file mode 100644
index 000000000..41a1d339d
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-mdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..36210a8cb
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..b00d82edd
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..218cb1214
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..a3896c5c6
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..814ca8ddc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..1fb69a477
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..2578be1e2
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..9a5b91fe5
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..69b472b00
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..118ea33d0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..80ad50b59
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..e56481ed7
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..a8becf485
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png
new file mode 100644
index 000000000..ec915f610
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..2e27936a4
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_business.png b/java/com/android/incallui/res/drawable-xhdpi/img_business.png
new file mode 100644
index 000000000..7b04d956f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_conference.png b/java/com/android/incallui/res/drawable-xhdpi/img_conference.png
new file mode 100644
index 000000000..b0dbcc2dc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_no_image.png b/java/com/android/incallui/res/drawable-xhdpi/img_no_image.png
new file mode 100644
index 000000000..4022207d0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xhdpi/img_phone.png b/java/com/android/incallui/res/drawable-xhdpi/img_phone.png
new file mode 100644
index 000000000..2e0ceec0f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xhdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..9f5120373
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..aeabe4a81
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..5ea577716
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..22d7aa55e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..078b10d4f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_01.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png
new file mode 100644
index 000000000..028e43b6e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_02.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png
new file mode 100644
index 000000000..b7dd070e1
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_03.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png
new file mode 100644
index 000000000..887c803f8
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_04.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png
new file mode 100644
index 000000000..c6ec16893
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_05.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png
new file mode 100644
index 000000000..d0b1e8649
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_06.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_07.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_08.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png
new file mode 100644
index 000000000..871a1ee75
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_ongoing_phone_24px_09.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..c17dfe05f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png
new file mode 100644
index 000000000..baf0cf27f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_phone_paused_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png
new file mode 100644
index 000000000..e3f6d285e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..bfc72736a
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_business.png b/java/com/android/incallui/res/drawable-xxhdpi/img_business.png
new file mode 100644
index 000000000..c17e4c9d8
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_conference.png b/java/com/android/incallui/res/drawable-xxhdpi/img_conference.png
new file mode 100644
index 000000000..a8dba5ed0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png b/java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png
new file mode 100644
index 000000000..2cf7f23a0
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxhdpi/img_phone.png b/java/com/android/incallui/res/drawable-xxhdpi/img_phone.png
new file mode 100644
index 000000000..4eaaba509
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxhdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png
new file mode 100644
index 000000000..01df2b52b
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_block_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png
new file mode 100644
index 000000000..a6e8a7bc1
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_end_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png
new file mode 100644
index 000000000..600cec8e6
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_call_split_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png
new file mode 100644
index 000000000..7d1c061f7
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_close_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png
new file mode 100644
index 000000000..8bcb6f620
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_location_on_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png
new file mode 100644
index 000000000..e24919737
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_person_add_grey600_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png
new file mode 100644
index 000000000..1a6bf1eb3
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_question_mark.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png b/java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png
new file mode 100644
index 000000000..b94f4dfa1
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/ic_schedule_white_24dp.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_business.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_business.png
new file mode 100644
index 000000000..88f14e999
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_business.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png
new file mode 100644
index 000000000..eb42b5552
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_conference.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png
new file mode 100644
index 000000000..216574222
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_no_image.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png b/java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png
new file mode 100644
index 000000000..7cbfbd75e
--- /dev/null
+++ b/java/com/android/incallui/res/drawable-xxxhdpi/img_phone.png
Binary files differ
diff --git a/java/com/android/incallui/res/drawable/img_conference_automirrored.xml b/java/com/android/incallui/res/drawable/img_conference_automirrored.xml
new file mode 100644
index 000000000..78b2876bc
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/img_conference_automirrored.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/img_conference"/> \ No newline at end of file
diff --git a/java/com/android/incallui/res/drawable/img_no_image_automirrored.xml b/java/com/android/incallui/res/drawable/img_no_image_automirrored.xml
new file mode 100644
index 000000000..9a9ec9706
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/img_no_image_automirrored.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+ android:autoMirrored="true"
+ android:src="@drawable/img_no_image"/> \ No newline at end of file
diff --git a/java/com/android/incallui/res/drawable/incall_background_gradient.xml b/java/com/android/incallui/res/drawable/incall_background_gradient.xml
new file mode 100644
index 000000000..5dd927f0f
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/incall_background_gradient.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <gradient
+ android:angle="270"
+ android:startColor="@color/incall_background_gradient_top"
+ android:centerColor="@color/incall_background_gradient_middle"
+ android:endColor="@color/incall_background_gradient_bottom"/>
+</shape>
diff --git a/java/com/android/incallui/res/drawable/spam_notification_icon.xml b/java/com/android/incallui/res/drawable/spam_notification_icon.xml
new file mode 100644
index 000000000..266897838
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/spam_notification_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ 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
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/incall_call_spam_background_color"/>
+ <size
+ android:height="@dimen/notification_large_icon_height"
+ android:width="@dimen/notification_large_icon_width"/>
+ </shape>
+ </item>
+
+ <item
+ android:drawable="@drawable/ic_report_white_36dp"
+ android:gravity="center"/>
+
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/res/drawable/unknown_notification_icon.xml b/java/com/android/incallui/res/drawable/unknown_notification_icon.xml
new file mode 100644
index 000000000..5ab07eccd
--- /dev/null
+++ b/java/com/android/incallui/res/drawable/unknown_notification_icon.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ 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
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item>
+ <shape android:shape="oval">
+ <solid android:color="@color/unknown_number_color"/>
+ <size
+ android:height="@dimen/notification_large_icon_height"
+ android:width="@dimen/notification_large_icon_width"/>
+ </shape>
+ </item>
+
+ <item
+ android:drawable="@drawable/ic_question_mark"
+ android:gravity="center"/>
+
+</layer-list> \ No newline at end of file
diff --git a/java/com/android/incallui/res/layout/activity_manage_conference.xml b/java/com/android/incallui/res/layout/activity_manage_conference.xml
new file mode 100644
index 000000000..60512938c
--- /dev/null
+++ b/java/com/android/incallui/res/layout/activity_manage_conference.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/manageConferencePanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+</FrameLayout>
diff --git a/java/com/android/incallui/res/layout/caller_in_conference.xml b/java/com/android/incallui/res/layout/caller_in_conference.xml
new file mode 100644
index 000000000..3a6773d20
--- /dev/null
+++ b/java/com/android/incallui/res/layout/caller_in_conference.xml
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="64dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="8dp"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <!-- Caller information -->
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@+id/callerPhoto"
+ android:layout_width="@dimen/contact_browser_list_item_photo_size"
+ android:layout_height="@dimen/contact_browser_list_item_photo_size"/>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="16dp"
+ android:paddingBottom="2dp"
+ android:gravity="center_vertical"
+ android:orientation="vertical">
+
+ <!-- Name or number of this caller -->
+ <TextView
+ android:id="@+id/conferenceCallerName"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:layout_marginEnd="2dp"
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textColor="@color/conference_call_manager_caller_name_text_color"
+ android:textSize="16sp"/>
+
+ <!-- Number of this caller if name is supplied above -->
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="bottom"
+ android:orientation="horizontal">
+
+ <!-- Number -->
+ <TextView
+ android:id="@+id/conferenceCallerNumber"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="8dp"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ android:textColor="@color/conference_call_manager_secondary_text_color"
+ android:textSize="14sp"/>
+
+ <!-- Number type -->
+ <TextView
+ android:id="@+id/conferenceCallerNumberType"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:gravity="start"
+ android:singleLine="true"
+ android:textAllCaps="true"
+ android:textColor="@color/conference_call_manager_secondary_text_color"
+ android:textSize="12sp"/>
+
+ </LinearLayout> <!-- End of caller number -->
+
+ </LinearLayout> <!-- End of caller information -->
+
+ </LinearLayout>
+
+ <!-- "Separate" (i.e. "go private") button for this caller -->
+ <ImageView
+ android:id="@+id/conferenceCallerSeparate"
+ android:layout_width="@dimen/conference_call_manager_button_dimension"
+ android:layout_height="@dimen/conference_call_manager_button_dimension"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/goPrivate"
+ android:scaleType="center"
+ android:src="@drawable/ic_call_split_white_24dp"
+ android:tint="@color/conference_call_manager_icon_color"/>
+
+ <!-- "Disconnect" button which terminates the connection with this caller. -->
+ <ImageButton
+ android:id="@+id/conferenceCallerDisconnect"
+ android:layout_width="@dimen/conference_call_manager_button_dimension"
+ android:layout_height="@dimen/conference_call_manager_button_dimension"
+ android:layout_marginStart="8dp"
+ android:background="?android:selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/conference_caller_disconnect_content_description"
+ android:scaleType="center"
+ android:src="@drawable/ic_call_end_white_24dp"
+ android:tint="@color/conference_call_manager_icon_color"/>
+
+</LinearLayout> <!-- End of single list element -->
diff --git a/java/com/android/incallui/res/layout/conference_manager_fragment.xml b/java/com/android/incallui/res/layout/conference_manager_fragment.xml
new file mode 100644
index 000000000..c0cc4cdcf
--- /dev/null
+++ b/java/com/android/incallui/res/layout/conference_manager_fragment.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- The "Manage conference" UI. This panel is displayed (instead of
+ the inCallPanel) when the user clicks the "Manage conference"
+ button while on a conference call. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/manageConferencePanel"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <!-- List of conference participants. -->
+ <ListView
+ android:id="@+id/participantList"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:divider="@null"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:listSelector="@null"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/res/layout/incall_dialpad_fragment.xml b/java/com/android/incallui/res/layout/incall_dialpad_fragment.xml
new file mode 100644
index 000000000..0621d48aa
--- /dev/null
+++ b/java/com/android/incallui/res/layout/incall_dialpad_fragment.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<view xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/dtmf_twelve_key_dialer_view"
+ class="com.android.incallui.DialpadFragment$DialpadSlidingLinearLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+ <include layout="@layout/dialpad_view"/>
+</view>
diff --git a/java/com/android/incallui/res/layout/incall_screen.xml b/java/com/android/incallui/res/layout/incall_screen.xml
new file mode 100644
index 000000000..9090fb287
--- /dev/null
+++ b/java/com/android/incallui/res/layout/incall_screen.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- In-call Phone UI; see InCallActivity.java. -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <FrameLayout
+ android:id="@+id/main"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <View
+ android:id="@+id/psuedo_black_screen_overlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="#000000"
+ android:visibility="gone"
+ android:keepScreenOn="true"/>
+</FrameLayout>
diff --git a/java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml b/java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml
new file mode 100644
index 000000000..bdc4eaff1
--- /dev/null
+++ b/java/com/android/incallui/res/layout/video_call_lte_to_wifi_failed.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="25dp"
+ android:orientation="vertical">
+
+ <CheckBox
+ android:id="@+id/video_call_lte_to_wifi_failed_checkbox"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/video_call_lte_to_wifi_failed_do_not_show"
+ android:textSize="@dimen/video_call_lte_to_wifi_failed_do_not_show_text_size"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/res/values-sw360dp/dimens.xml b/java/com/android/incallui/res/values-sw360dp/dimens.xml
new file mode 100644
index 000000000..ad782e809
--- /dev/null
+++ b/java/com/android/incallui/res/values-sw360dp/dimens.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+
+ <!-- The InCallUI dialpad will sometimes want digits sizes that are different from dialer. -->
+ <dimen name="incall_dialpad_key_number_margin_bottom">
+ @dimen/dialpad_key_number_default_margin_bottom
+ </dimen>
+ <!-- Zero key should have less space between self and text because "+" is smaller -->
+ <dimen name="incall_dialpad_zero_key_number_margin_bottom">
+ @dimen/dialpad_zero_key_number_default_margin_bottom
+ </dimen>
+ <dimen name="incall_dialpad_digits_adjustable_text_size">@dimen/dialpad_digits_text_size</dimen>
+ <dimen name="incall_dialpad_digits_adjustable_height">@dimen/dialpad_digits_height</dimen>
+ <dimen name="incall_dialpad_key_numbers_size">@dimen/dialpad_key_numbers_default_size</dimen>
+
+</resources>
diff --git a/java/com/android/incallui/res/values-w500dp-land/colors.xml b/java/com/android/incallui/res/values-w500dp-land/colors.xml
new file mode 100644
index 000000000..4b0e33ea7
--- /dev/null
+++ b/java/com/android/incallui/res/values-w500dp-land/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+ <!-- Background color for status bar. For portrait this will be ignored. -->
+ <color name="statusbar_background_color">#000000</color>
+</resources>
diff --git a/java/com/android/incallui/res/values-w500dp-land/dimens.xml b/java/com/android/incallui/res/values-w500dp-land/dimens.xml
new file mode 100644
index 000000000..81090fc80
--- /dev/null
+++ b/java/com/android/incallui/res/values-w500dp-land/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+
+ <!-- Whether or not the landscape mode layout is currently being used -->
+ <bool name="is_layout_landscape">true</bool>
+
+</resources>
diff --git a/java/com/android/incallui/res/values/animation_constants.xml b/java/com/android/incallui/res/values/animation_constants.xml
new file mode 100644
index 000000000..ac50db21c
--- /dev/null
+++ b/java/com/android/incallui/res/values/animation_constants.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2014 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<resources>
+ <integer name="reveal_animation_duration">333</integer>
+</resources>
diff --git a/java/com/android/incallui/res/values/colors.xml b/java/com/android/incallui/res/values/colors.xml
new file mode 100644
index 000000000..0c73cdb10
--- /dev/null
+++ b/java/com/android/incallui/res/values/colors.xml
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+
+ <color name="incall_action_bar_background_color">@color/dialer_theme_color</color>
+ <color name="incall_action_bar_text_color">#ffffff</color>
+
+ <!-- Put on top of each photo, implying 80% darker than usual. -->
+ <color name="on_hold_dim_effect">#cc000000</color>
+
+ <color name="conference_call_manager_caller_name_text_color">#4d4d4d</color>
+ <color name="conference_call_manager_icon_color">#999999</color>
+ <!-- Used with some smaller texts in manage conference screen. -->
+ <color name="conference_call_manager_secondary_text_color">#999999</color>
+
+ <color name="incall_dialpad_background">#ffffff</color>
+ <color name="incall_dialpad_background_pressed">#ccaaaaaa</color>
+ <color name="incall_window_scrim">#b2000000</color>
+
+ <!-- Background color for status bar. For portrait this will be ignored. -->
+ <color name="statusbar_background_color">@color/dialer_theme_color</color>
+
+ <color name="translucent_shadow">#33999999</color>
+
+ <!-- 20% opacity, theme color. -->
+ <color name="incall_dialpad_touch_tint">@color/dialer_theme_color_20pct</color>
+
+ <!-- Background colors for InCallUI. This is a set of colors which pass WCAG
+ AA and all have a contrast ratio over 5:1.
+
+ These colors are also used by InCallUIMaterialColorMapUtils to generate
+ primary activity colors.
+
+ -->
+ <array name="background_colors">
+ <item>#00796B</item>
+ <item>#3367D6</item>
+ <item>#303F9F</item>
+ <item>#7B1FA2</item>
+ <item>#C2185B</item>
+ <item>#C53929</item>
+ <item>#A52714</item>
+ </array>
+
+ <!-- Darker versions of background_colors, two shades darker. These colors are used for the
+ status bar. -->
+ <array name="background_colors_dark">
+ <item>#00695C</item>
+ <item>#2A56C6</item>
+ <item>#283593</item>
+ <item>#6A1B9A</item>
+ <item>#AD1457</item>
+ <item>#B93221</item>
+ <item>#841F10</item>
+ </array>
+
+ <!-- Background color for spam. This color must match one of background_colors above. -->
+ <color name="incall_call_spam_background_color">@color/blocked_contact_background</color>
+
+ <!-- Ripple color used over light backgrounds. -->
+ <color name="ripple_light">#40000000</color>
+
+ <!-- Background color for large notification icon in after call from unknown numbers -->
+ <color name="unknown_number_color">#F4B400</color>
+
+ <color name="incall_background_gradient_top">#E91141BB</color>
+ <color name="incall_background_gradient_middle">#E91141BB</color>
+ <color name="incall_background_gradient_bottom">#CC229FEB</color>
+
+ <color name="incall_background_multiwindow">#E91141BB</color>
+
+ <color name="incall_background_gradient_spam_top">#E5A30B0B</color>
+ <color name="incall_background_gradient_spam_middle">#D6C01111</color>
+ <color name="incall_background_gradient_spam_bottom">#B8E55135</color>
+
+ <color name="incall_background_multiwindow_spam">#E9C22E2E</color>
+</resources>
diff --git a/java/com/android/incallui/res/values/config.xml b/java/com/android/incallui/res/values/config.xml
new file mode 100644
index 000000000..0f3c983b7
--- /dev/null
+++ b/java/com/android/incallui/res/values/config.xml
@@ -0,0 +1,23 @@
+<!--
+ ~ 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
+ -->
+<resources>
+ <!-- Determines video calls will automatically enter fullscreen mode after the start of the
+ call. -->
+ <bool name="video_call_auto_fullscreen">true</bool>
+ <!-- The number of milliseconds after which a video call will automatically enter fullscreen
+ mode (requires video_call_auto_fullscreen to be true). -->
+ <integer name="video_call_auto_fullscreen_timeout">5000</integer>
+</resources>
diff --git a/java/com/android/incallui/res/values/dimens.xml b/java/com/android/incallui/res/values/dimens.xml
new file mode 100644
index 000000000..18816f645
--- /dev/null
+++ b/java/com/android/incallui/res/values/dimens.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="incall_action_bar_elevation">3dp</dimen>
+
+ <!-- Margin between the bottom of the "call card" photo
+ and the top of the in-call button cluster. -->
+ <dimen name="in_call_touch_ui_upper_margin">2dp</dimen>
+
+ <!-- Padding at the top and bottom edges of the "provider information" -->
+ <dimen name="provider_info_top_bottom_padding">8dp</dimen>
+
+ <!-- Right padding for name and number fields in the call banner.
+ This padding is used to ensure that ultra-long names or
+ numbers won't overlap the elapsed time indication. -->
+ <dimen name="call_banner_name_number_right_padding">50sp</dimen>
+
+ <!-- The InCallUI dialpad will sometimes want digits sizes that are different
+ from dialer. Note, these are the default sizes for small devices. Larger
+ screen sizes apply the values in values-sw360dp/dimens.xml. -->
+ <dimen name="incall_dialpad_key_number_margin_bottom">1dp</dimen>
+ <!-- Zero key should have less space between self and text because "+" is smaller -->
+ <dimen name="incall_dialpad_zero_key_number_margin_bottom">0dp</dimen>
+ <dimen name="incall_dialpad_digits_adjustable_text_size">20sp</dimen>
+ <dimen name="incall_dialpad_digits_adjustable_height">50dp</dimen>
+ <dimen name="incall_dialpad_key_numbers_size">36sp</dimen>
+
+ <!-- Dimensions for OTA Call Card -->
+ <dimen name="otaactivate_layout_marginTop">10dp</dimen>
+ <dimen name="otalistenprogress_layout_marginTop">5dp</dimen>
+ <dimen name="otasuccessfail_layout_marginTop">10dp</dimen>
+
+ <!-- Dimension used to possibly down-scale high-res photo into what is suitable
+ for notification's large icon. -->
+ <dimen name="notification_icon_size">64dp</dimen>
+
+ <!-- Height of translucent shadow effect -->
+ <dimen name="translucent_shadow_height">2dp</dimen>
+
+ <!-- The smaller dimension of the video preview. When in portrait orientation this is the
+ width of the preview. When in landscape, this is the height. -->
+ <dimen name="video_preview_small_dimension">90dp</dimen>
+
+ <dimen name="conference_call_manager_button_dimension">48dp</dimen>
+
+ <!-- Whether or not the landscape mode layout is currently being used -->
+ <bool name="is_layout_landscape">false</bool>
+
+ <dimen name="video_call_lte_to_wifi_failed_do_not_show_text_size">16sp</dimen>
+
+</resources>
diff --git a/java/com/android/incallui/res/values/strings.xml b/java/com/android/incallui/res/values/strings.xml
new file mode 100644
index 000000000..252d131de
--- /dev/null
+++ b/java/com/android/incallui/res/values/strings.xml
@@ -0,0 +1,367 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Official label of the phone app, as seen in "Manage Applications"
+ and other settings UIs. -->
+ <string name="phoneAppLabel" product="default">Phone</string>
+
+ <!-- Official label for the in-call UI. DO NOT TRANSLATE. -->
+ <string name="inCallLabel" translate="false">InCallUI</string>
+
+ <!-- In-call screen: status label for a conference call -->
+ <string name="confCall">Conference call</string>
+ <!-- In-call screen: call lost dialog text -->
+ <string name="call_lost">Call dropped</string>
+
+ <!-- MMI dialog strings -->
+ <!-- Dialog label when an MMI code starts running -->
+
+ <!-- post dial -->
+ <!-- In-call screen: body text of the dialog that appears when we encounter
+ the "wait" character in a phone number to be dialed; this dialog asks the
+ user if it's OK to send the numbers following the "wait". -->
+ <string name="wait_prompt_str">Send the following tones?\n</string>
+ <!-- In-call screen: body text of the dialog that appears when we encounter
+ the "PAUSE" character in a phone number to be dialed; this dialog gives
+ informative message to the user to show the sending numbers following the "Pause". -->
+ <string name="pause_prompt_str">Sending tones\n</string>
+ <!-- In-call screen: button label on the "wait" prompt dialog -->
+ <string name="send_button">Send</string>
+ <!-- In-call screen: button label on the "wait" prompt dialog in CDMA Mode-->
+ <string name="pause_prompt_yes">Yes</string>
+ <!-- In-call screen: button label on the "wait" prompt dialog in CDMA Mode-->
+ <string name="pause_prompt_no">No</string>
+ <!-- In-call screen: on the "wild" character dialog, this is the label
+ for a text widget that lets the user enter the digits that should
+ replace the "wild" character. -->
+ <string name="wild_prompt_str">Replace wild character with</string>
+
+ <!-- In-call screen: status label for a conference call -->
+ <string name="caller_manage_header">Conference call <xliff:g id="conf_call_time">%s</xliff:g></string>
+
+ <!-- Used in FakePhoneActivity test code. DO NOT TRANSLATE. -->
+ <string name="fake_phone_activity_phoneNumber_text" translatable="false">(650) 555-1234</string>
+ <!-- Used in FakePhoneActivity test code. DO NOT TRANSLATE. -->
+ <string name="fake_phone_activity_infoText_text" translatable="false">Incoming phone number</string>
+ <!-- Used in FakePhoneActivity test code. DO NOT TRANSLATE. -->
+ <string name="fake_phone_activity_placeCall_text" translatable="false">Fake Incoming Call</string>
+
+ <!-- Call settings screen, Set voicemail dialog title -->
+ <string name="voicemail_settings_number_label">Voicemail number</string>
+
+ <!-- Notification strings -->
+ <!-- The "label" of the in-call Notification for a dialing call, used
+ as the format string for a Chronometer widget. [CHAR LIMIT=60] -->
+ <string name="notification_dialing">Dialing</string>
+ <!-- Missed call notification message used for a single missed call, including
+ the caller-id info from the missed call -->
+ <string name="notification_missedCallTicker">Missed call from <xliff:g id="missed_call_from">%s</xliff:g></string>
+ <!-- The "label" of the in-call Notification for an ongoing call. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_call">Ongoing call</string>
+ <!-- The "label" of the in-call Notification for an ongoing work call. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_work_call">Ongoing work call</string>
+ <!-- The "label" of the in-call Notification for an ongoing call, which is being made over
+ Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_call_wifi">Ongoing Wi-Fi call</string>
+ <!-- The "label" of the in-call Notification for an ongoing work call, which is being made
+ over Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_ongoing_work_call_wifi">Ongoing Wi-Fi work call</string>
+ <!-- The "label" of the in-call Notification for a call that's on hold -->
+ <string name="notification_on_hold">On hold</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing call. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_call">Incoming call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing call. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_work_call">Incoming work call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing call,
+ which is being made over Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_call_wifi">Incoming Wi-Fi call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing work call,
+ which is being made over Wi-Fi. [CHAR LIMIT=60] -->
+ <string name="notification_incoming_work_call_wifi">Incoming Wi-Fi work call</string>
+ <!-- The "label" of the in-call Notification for an incoming ringing spam call. -->
+ <string name="notification_incoming_spam_call">Incoming suspected spam call</string>
+ <!-- The "label" of the in-call Notification for upgrading an existing call to a video call. -->
+ <string name="notification_requesting_video_call">Incoming video request</string>
+ <!-- Label for the "Voicemail" notification item, when expanded. -->
+ <string name="notification_voicemail_title">New voicemail</string>
+ <!-- Label for the expanded "Voicemail" notification item,
+ including a count of messages. -->
+ <string name="notification_voicemail_title_count">New voicemail (<xliff:g id="count">%d</xliff:g>)</string>
+ <!-- Message displayed in the "Voicemail" notification item, allowing the user
+ to dial the indicated number. -->
+ <string name="notification_voicemail_text_format">Dial <xliff:g id="voicemail_number">%s</xliff:g></string>
+ <!-- Message displayed in the "Voicemail" notification item,
+ indicating that there's no voicemail number available -->
+ <string name="notification_voicemail_no_vm_number">Voicemail number unknown</string>
+ <!-- Label for the "No service" notification item, when expanded. -->
+ <string name="notification_network_selection_title">No service</string>
+ <!-- Label for the expanded "No service" notification item, including the
+ operator name set by user -->
+ <string name="notification_network_selection_text">Selected network (<xliff:g id="operator_name">%s</xliff:g>) unavailable</string>
+ <!-- Label for the "Answer call" action. This is the displayed label for the action that answers
+ an incoming call. [CHAR LIMIT=12] -->
+ <string name="notification_action_answer">Answer</string>
+ <!-- Label for "end call" Action.
+ It is displayed in the "Ongoing call" notification, which is shown
+ when the user is outside the in-call screen while the phone call is still
+ active. [CHAR LIMIT=12] -->
+ <string name="notification_action_end_call">Hang up</string>
+ <!-- Label for "Video Call" notification action. This is a displayed on the notification for an
+ incoming video call, and answers the call as a video call. [CHAR LIMIT=12] -->
+ <string name="notification_action_answer_video">Video</string>
+ <!-- Label for "Voice" notification action. This is a displayed on the notification for an
+ incoming video call, and answers the call as an audio call. [CHAR LIMIT=12] -->
+ <string name="notification_action_answer_voice">Voice</string>
+ <!-- Label for "Accept" notification action. This is somewhat generic, and may refer to
+ scenarios such as accepting an incoming call or accepting a video call request.
+ [CHAR LIMIT=12] -->
+ <string name="notification_action_accept">Accept</string>
+ <!-- Label for "Dismiss" notification action. This is somewhat generic, and may refer to
+ scenarios such as declining an incoming call or declining a video call request.
+ [CHAR LIMIT=12] -->
+ <string name="notification_action_dismiss">Decline</string>
+
+ <!-- The "label" of the in-call Notification for an ongoing external call.
+ External calls are a representation of a call which is in progress on the user's other
+ device (e.g. another phone or a watch).
+ [CHAR LIMIT=60] -->
+ <string name="notification_external_call">Ongoing call on another device</string>
+ <!-- The "label" of the in-call Notification for an ongoing external video call.
+ External calls are a representation of a call which is in progress on the user's other
+ device (e.g. another phone or a watch).
+ [CHAR LIMIT=60] -->
+ <string name="notification_external_video_call">Ongoing video call on another device</string>
+ <!-- Notification action displayed for external call notifications. External calls are a
+ representation of a call which is in progress on the user's other device (e.g. another
+ phone or a watch). The "take call" action initiates the process of pulling an external
+ call to the current device.
+ [CHAR LIMIT=30] -->
+ <string name="notification_take_call">Take Call</string>
+ <!-- Notification action displayed for external call notifications. External calls are a
+ representation of a call which is in progress on the user's other device (e.g. another
+ phone or a watch). The "take video call" action initiates the process of pulling an external
+ video call to the current device.
+ [CHAR LIMIT=30] -->
+ <string name="notification_take_video_call">Take Video Call</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_power_off">To place a call, first turn off Airplane mode.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog.
+ This string is currently unused (see comments in InCallActivity.java.) -->
+ <string name="incall_error_emergency_only">Not registered on network.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_out_of_service">Cellular network not available.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_no_phone_number_supplied">To place a call, enter a valid number.</string>
+ <!-- In-call screen: call failure message displayed in an error dialog -->
+ <string name="incall_error_call_failed">Can\'t call.</string>
+ <!-- In-call screen: status message displayed in a dialog when starting an MMI -->
+ <string name="incall_status_dialed_mmi">Starting MMI sequence\u2026</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_unknown">Service not supported.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_switch">Can\'t switch calls.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_separate">Can\'t separate call.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_transfer">Can\'t transfer.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_conference">Can\'t conference.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_reject">Can\'t reject call.</string>
+ <!-- In-call screen: message displayed in an error dialog -->
+ <string name="incall_error_supp_service_hangup">Can\'t release call(s).</string>
+
+ <!-- Dialog title for the "radio enable" UI for emergency calls -->
+ <string name="emergency_enable_radio_dialog_title">Emergency call</string>
+ <!-- Status message for the "radio enable" UI for emergency calls -->
+ <string name="emergency_enable_radio_dialog_message">Turning on radio\u2026</string>
+ <!-- Status message for the "radio enable" UI for emergency calls -->
+ <string name="emergency_enable_radio_dialog_retry">No service. Trying again\u2026</string>
+
+ <!-- Dialer text on Emergency Dialer -->
+ <!-- Emergency dialer: message displayed in an error dialog -->
+ <string name="dial_emergency_error">Can\'t call. <xliff:g id="non_emergency_number">%s</xliff:g> is not an emergency number.</string>
+ <!-- Emergency dialer: message displayed in an error dialog -->
+ <string name="dial_emergency_empty_error">Can\'t call. Dial an emergency number.</string>
+
+ <!-- Displayed in the text entry box in the dialer when in landscape mode to guide the user
+ to dial using the physical keyboard -->
+ <string name="dialerKeyboardHintText">Use keyboard to dial</string>
+
+ <!-- Message indicating that Video Started flowing for IMS-VT calls -->
+ <string name="player_started">Player Started</string>
+ <!-- Message indicating that Video Stopped flowing for IMS-VT calls -->
+ <string name="player_stopped">Player Stopped</string>
+ <!-- Message indicating that camera failure has occurred for the selected camera and
+ as result camera is not ready -->
+ <string name="camera_not_ready">Camera not ready</string>
+ <!-- Message indicating that camera is ready/available -->
+ <string name="camera_ready">Camera ready</string>
+ <!-- Message indicating unknown call session event -->
+ <string name="unknown_call_session_event">"Unkown call session event"</string>
+
+ <!-- For incoming calls, this is a string we can get from a CDMA network instead of
+ the actual phone number, to indicate there's no number present. DO NOT TRANSLATE. -->
+ <string-array name="absent_num" translatable="false">
+ <item>ABSENT NUMBER</item>
+ <item>ABSENTNUMBER</item>
+ </string-array>
+
+ <!-- Preference for Voicemail service provider under "Voicemail" settings.
+ [CHAR LIMIT=40] -->
+ <string name="voicemail_provider">Service</string>
+
+ <!-- Preference for Voicemail setting of each provider.
+ [CHAR LIMIT=40] -->
+ <string name="voicemail_settings">Setup</string>
+
+ <!-- String to display in voicemail number summary when no voicemail num is set -->
+ <string name="voicemail_number_not_set">&lt;Not set&gt;</string>
+
+ <!-- Title displayed above settings coming after voicemail in the call features screen -->
+ <string name="other_settings">Other call settings</string>
+
+ <!-- Use this to describe the separate conference call button; currently for screen readers through accessibility. -->
+ <string name="goPrivate">go private</string>
+ <!-- Use this to describe the select contact button in EditPhoneNumberPreference; currently for screen readers through accessibility. -->
+ <string name="selectContact">select contact</string>
+
+ <!-- Dialog title for the vibration settings for voicemail notifications [CHAR LIMIT=40] -->
+ <string msgid="8731372580674292759" name="voicemail_notification_vibrate_when_title">Vibrate</string>
+ <!-- Dialog title for the vibration settings for voice mail notifications [CHAR LIMIT=40]-->
+ <string msgid="8995274609647451109" name="voicemail_notification_vibarte_when_dialog_title">Vibrate</string>
+
+ <!-- Voicemail ringtone title. The user clicks on this preference to select
+ which sound to play when a voicemail notification is received.
+ [CHAR LIMIT=30] -->
+ <string name="voicemail_notification_ringtone_title">Sound</string>
+
+ <!-- The default value value for voicemail notification. -->
+ <string name="voicemail_notification_vibrate_when_default" translatable="false">never</string>
+
+ <!-- Actual values used in our code for voicemail notifications. DO NOT TRANSLATE -->
+ <string-array name="voicemail_notification_vibrate_when_values" translatable="false">
+ <item>always</item>
+ <item>silent</item>
+ <item>never</item>
+ </string-array>
+
+ <!-- Title for the category "ringtone", which is shown above ringtone and vibration
+ related settings.
+ [CHAR LIMIT=30] -->
+ <string name="preference_category_ringtone">Ringtone &amp; Vibrate</string>
+
+ <!-- Label for "Manage conference call" panel [CHAR LIMIT=40] -->
+ <string name="manageConferenceLabel">Manage conference call</string>
+
+ <!-- This can be used in any application wanting to disable the text "Emergency number" -->
+ <string name="emergency_call_dialog_number_for_display">Emergency number</string>
+
+ <!-- Used to inform the user that a call was received via a number other than the primary
+ phone number associated with their device. [CHAR LIMIT=16] -->
+ <string name="child_number">via <xliff:g example="650-555-1212" id="child_number">%s</xliff:g></string>
+
+ <!-- Title for the call context with a person-type contact. [CHAR LIMIT=40] -->
+ <string name="person_contact_context_title">Recent messages</string>
+
+ <!-- Title for the call context with a business-type contact. [CHAR LIMIT=40] -->
+ <string name="business_contact_context_title">Business info</string>
+
+ <!-- Distance strings for business caller ID context. -->
+
+ <!-- Used to inform the user how far away a location is in miles. [CHAR LIMIT=NONE] -->
+ <string name="distance_imperial_away"><xliff:g id="distance">%.1f</xliff:g> mi away</string>
+ <!-- Used to inform the user how far away a location is in kilometers. [CHAR LIMIT=NONE] -->
+ <string name="distance_metric_away"><xliff:g id="distance">%.1f</xliff:g> km away</string>
+ <!-- A shortened way to display a business address. Formatted [street address], [city/locality]. -->
+ <string name="display_address"><xliff:g id="street_address">%1$s</xliff:g>, <xliff:g id="locality">%2$s</xliff:g></string>
+ <!-- Used to indicate hours of operation for a location as a time span. e.g. "11 am - 9 pm" [CHAR LIMIT=NONE] -->
+ <string name="open_time_span"><xliff:g id="open_time">%1$s</xliff:g> - <xliff:g id="close_time">%2$s</xliff:g></string>
+ <!-- Used to indicate a series of opening hours for a location.
+ This first argument may be one or more time spans. e.g. "11 am - 9 pm, 9 pm - 11 pm"
+ The second argument is an additional time span. e.g. "11 pm - 1 am"
+ The string is used to build a list of opening hours.
+ [CHAR LIMIT=NONE] -->
+ <string name="opening_hours"><xliff:g id="earlier_times">%1$s</xliff:g>, <xliff:g id="later_time">%2$s</xliff:g></string>
+ <!-- Used to express when a location will open the next day. [CHAR LIMIT=NONE] -->
+ <string name="opens_tomorrow_at">Opens tomorrow at <xliff:g id="open_time">%s</xliff:g></string>
+ <!-- Used to express the next time at which a location will be open today. [CHAR LIMIT=NONE] -->
+ <string name="opens_today_at">Opens today at <xliff:g id="open_time">%s</xliff:g></string>
+ <!-- Used to express the next time at which a location will close today. [CHAR LIMIT=NONE] -->
+ <string name="closes_today_at">Closes at <xliff:g id="close_time">%s</xliff:g></string>
+ <!-- Used to express the next time at which a location closed today if it is already closed. [CHAR LIMIT=NONE] -->
+ <string name="closed_today_at">Closed today at <xliff:g id="close_time">%s</xliff:g></string>
+ <!-- Displayed when a place is open. -->
+ <string name="open_now">Open now</string>
+ <!-- Displayed when a place is closed. -->
+ <string name="closed_now">Closed now</string>
+
+ <!-- Title for the notification to the user after a call from an unknown number ends. [CHAR LIMIT=100] -->
+ <string name="non_spam_notification_title">Know <xliff:g id="number">%1$s</xliff:g>?</string>
+ <!-- Title for the notification to the user after a call from an spammer ends. [CHAR LIMIT=100] -->
+ <string name="spam_notification_title">Is <xliff:g id="number">%1$s</xliff:g> spam?</string>
+ <!-- Text for the toast shown after the user presses block/report spam. [CHAR LIMIT=100] -->
+ <string name="spam_notification_block_report_toast_text"><xliff:g id="number">%1$s</xliff:g> blocked and call was reported as spam.</string>
+ <!-- Text for the toast shown after the user presses not spam. [CHAR LIMIT=100] -->
+ <string name="spam_notification_not_spam_toast_text">Call from <xliff:g id="number">%1$s</xliff:g> reported as not spam.</string>
+ <!-- Text displayed in the collapsed notification to the user after a non-spam call ends. [CHAR LIMIT=100] -->
+ <string name="spam_notification_non_spam_call_collapsed_text">Tap to add to contacts or block spam number.</string>
+ <!-- Text displayed in the expanded notification to the user after a non-spam call ends. [CHAR LIMIT=NONE] -->
+ <string name="spam_notification_non_spam_call_expanded_text">This is the first time this number called you. If this call was spam, you can block this number and report it.</string>
+ <!-- Text displayed in the collapsed notification to the user after a spam call ends. [CHAR LIMIT=100] -->
+ <string name="spam_notification_spam_call_collapsed_text">Tap to report as NOT SPAM, or block it.</string>
+ <!-- Text displayed in the expanded notification to the user after a spam call ends. [CHAR LIMIT=NONE] -->
+ <string name="spam_notification_spam_call_expanded_text">We suspected this to be a spammer. If this call wasn\'t spam, tap "NOT SPAM" to report our mistake.</string>
+ <!-- Text for the reporting spam action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_report_spam_action_text">Block &amp; report</string>
+ <!-- Text for the adding to contacts action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_add_contact_action_text">Add contact</string>
+ <!-- Text for the reporting as not spam action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_not_spam_action_text">Not spam</string>
+ <!-- Text for the blocking spam action in the after call prompt. [CHAR LIMIT=20] -->
+ <string name="spam_notification_block_spam_action_text">Block number</string>
+ <!-- Text for the adding to contacts action in the after call dialog. [CHAR LIMIT=40] -->
+ <string name="spam_notification_dialog_add_contact_action_text">Add to contacts</string>
+ <!-- Text for the blocking and reporting spam action in the after call dialog. [CHAR LIMIT=40] -->
+ <string name="spam_notification_dialog_block_report_spam_action_text">Block &amp; report spam</string>
+ <!-- Text for the marking a call as not spam in the after call dialog. [CHAR LIMIT=40] -->
+ <string name="spam_notification_dialog_was_not_spam_action_text">Not spam</string>
+
+ <string name="callFailed_simError">No SIM or SIM error</string>
+
+ <string name="conference_caller_disconnect_content_description">End call</string>
+
+ <!-- Name for a conference call. Shown in the in call UI and in notifications. -->
+ <string name="conference_call_name">Conference call</string>
+
+ <!-- Name for a generic conference call. Shown in the in call UI. This is used in CDMA where we
+ don't know the precise state of participants in the conference. -->
+ <string name="generic_conference_call_name">In call</string>
+
+ <!-- Displayed when handover from WiFi to Lte occurs during a video call -->
+ <string name="video_call_wifi_to_lte_handover_toast">Continuing call using cellular data…</string>
+
+ <!-- Displayed when WiFi handover from LTE fails during a video call. -->
+ <string name="video_call_lte_to_wifi_failed_title">Couldn\'t switch to Wi-Fi network</string>
+ <string name="video_call_lte_to_wifi_failed_message">Video call will remain on cellular network. Standard
+ data charges may apply.
+ </string>
+ <string name="video_call_lte_to_wifi_failed_do_not_show">Do not show this again</string>
+
+</resources>
diff --git a/java/com/android/incallui/res/values/styles.xml b/java/com/android/incallui/res/values/styles.xml
new file mode 100644
index 000000000..96e3d4d59
--- /dev/null
+++ b/java/com/android/incallui/res/values/styles.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <drawable name="grayBg">#FF333333</drawable>
+
+ <!-- Theme for the InCallActivity activity. Should have a transparent background for the
+ circular reveal animation for a new outgoing call to work correctly. We don't just use
+ Theme.Black.NoTitleBar directly, since we want any popups or dialogs from the
+ InCallActivity to have the correct Material style. -->
+ <style name="Theme.InCallScreen" parent="@style/Theme.AppCompat.NoActionBar">
+ <item name="android:textColorPrimary">#ffffff</item>
+ <item name="android:textColorSecondary">#DDFFFFFF</item>
+ <item name="android:colorPrimary">@color/dialer_theme_color</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+
+ <item name="android:statusBarColor">@android:color/transparent</item>
+ <item name="android:navigationBarColor">@android:color/transparent</item>
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+
+ <item name="dialpad_key_button_touch_tint">@color/incall_dialpad_touch_tint</item>
+ <item name="dialpad_style">@style/InCallDialpad</item>
+ <item name="android:windowAnimationStyle">@null</item>
+ <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
+
+ <item name="android:windowBackground">@drawable/incall_background_gradient</item>
+ <item name="android:windowShowWallpaper">true</item>
+ </style>
+
+ <style name="Theme.InCallScreen.ManageConference" parent="DialerThemeBase">
+ </style>
+
+ <style name="InCallDialpad" parent="Dialpad.Light">
+ <item name="dialpad_key_number_margin_bottom">
+ @dimen/incall_dialpad_key_number_margin_bottom
+ </item>
+ <item name="dialpad_zero_key_number_margin_bottom">
+ @dimen/incall_dialpad_zero_key_number_margin_bottom
+ </item>
+ <item name="dialpad_digits_adjustable_text_size">
+ @dimen/incall_dialpad_digits_adjustable_text_size
+ </item>
+ <item name="dialpad_digits_adjustable_height">
+ @dimen/incall_dialpad_digits_adjustable_height
+ </item>
+ <item name="dialpad_key_numbers_size">
+ @dimen/incall_dialpad_key_numbers_size
+ </item>
+ <item name="dialpad_end_key_spacing">
+ @dimen/incall_end_call_spacing
+ </item>
+ </style>
+
+ <style name="AfterCallNotificationTheme" parent="@style/Theme.AppCompat.Light.Dialog.MinWidth">
+ <!-- This colorAccent is to style checkboxes in the dialogs -->
+ <item name="colorAccent">@color/dialer_theme_color</item>
+ <!-- This is needed to make any alert dialogs in this activity take up minimum space -->
+ <item name="android:alertDialogTheme">@style/AfterCallDialogStyle</item>
+ </style>
+
+ <style name="AfterCallDialogStyle" parent="@style/Theme.AppCompat.Light.Dialog.MinWidth">
+ <!-- This colorAccent is to style text in the dialogs -->
+ <item name="android:colorAccent">@color/dialer_theme_color</item>
+ </style>
+
+</resources>
diff --git a/java/com/android/incallui/ringtone/DialerRingtoneManager.java b/java/com/android/incallui/ringtone/DialerRingtoneManager.java
new file mode 100644
index 000000000..5ebd93378
--- /dev/null
+++ b/java/com/android/incallui/ringtone/DialerRingtoneManager.java
@@ -0,0 +1,134 @@
+/*
+ * 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.incallui.ringtone;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall.State;
+import java.util.Objects;
+
+/**
+ * Class that determines when ringtones should be played and can play the call waiting tone when
+ * necessary.
+ */
+public class DialerRingtoneManager {
+
+ /*
+ * Flag used to determine if the Dialer is responsible for playing ringtones for incoming calls.
+ * Once we're ready to enable Dialer Ringing, these flags should be removed.
+ */
+ private static final boolean IS_DIALER_RINGING_ENABLED = false;
+ private final InCallTonePlayer mInCallTonePlayer;
+ private final CallList mCallList;
+ private Boolean mIsDialerRingingEnabledForTesting;
+
+ /**
+ * Creates the DialerRingtoneManager with the given {@link InCallTonePlayer}.
+ *
+ * @param inCallTonePlayer the tone player used to play in-call tones.
+ * @param callList the CallList used to check for {@link State#CALL_WAITING}
+ * @throws NullPointerException if inCallTonePlayer or callList are null
+ */
+ public DialerRingtoneManager(
+ @NonNull InCallTonePlayer inCallTonePlayer, @NonNull CallList callList) {
+ mInCallTonePlayer = Objects.requireNonNull(inCallTonePlayer);
+ mCallList = Objects.requireNonNull(callList);
+ }
+
+ /**
+ * Determines if a ringtone should be played for the given call state (see {@link State}) and
+ * {@link Uri}.
+ *
+ * @param callState the call state for the call being checked.
+ * @param ringtoneUri the ringtone to potentially play.
+ * @return {@code true} if the ringtone should be played, {@code false} otherwise.
+ */
+ public boolean shouldPlayRingtone(int callState, @Nullable Uri ringtoneUri) {
+ return isDialerRingingEnabled()
+ && translateCallStateForCallWaiting(callState) == State.INCOMING
+ && ringtoneUri != null;
+ }
+
+ /**
+ * Determines if an incoming call should vibrate as well as ring.
+ *
+ * @param resolver {@link ContentResolver} used to look up the {@link
+ * Settings.System#VIBRATE_WHEN_RINGING} setting.
+ * @return {@code true} if the call should vibrate, {@code false} otherwise.
+ */
+ public boolean shouldVibrate(ContentResolver resolver) {
+ return Settings.System.getInt(resolver, Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+ }
+
+ /**
+ * The incoming callState is never set as {@link State#CALL_WAITING} because {@link
+ * DialerCall#translateState(int)} doesn't account for that case, check for it here
+ */
+ private int translateCallStateForCallWaiting(int callState) {
+ if (callState != State.INCOMING) {
+ return callState;
+ }
+ return mCallList.getActiveCall() == null ? State.INCOMING : State.CALL_WAITING;
+ }
+
+ private boolean isDialerRingingEnabled() {
+ boolean enabledFlag =
+ mIsDialerRingingEnabledForTesting != null
+ ? mIsDialerRingingEnabledForTesting
+ : IS_DIALER_RINGING_ENABLED;
+ return VERSION.SDK_INT >= VERSION_CODES.N && enabledFlag;
+ }
+
+ /**
+ * Determines if a call waiting tone should be played for the the given call state (see {@link
+ * State}).
+ *
+ * @param callState the call state for the call being checked.
+ * @return {@code true} if the call waiting tone should be played, {@code false} otherwise.
+ */
+ public boolean shouldPlayCallWaitingTone(int callState) {
+ return isDialerRingingEnabled()
+ && translateCallStateForCallWaiting(callState) == State.CALL_WAITING
+ && !mInCallTonePlayer.isPlayingTone();
+ }
+
+ /** Plays the call waiting tone. */
+ public void playCallWaitingTone() {
+ if (!isDialerRingingEnabled()) {
+ return;
+ }
+ mInCallTonePlayer.play(InCallTonePlayer.TONE_CALL_WAITING);
+ }
+
+ /** Stops playing the call waiting tone. */
+ public void stopCallWaitingTone() {
+ if (!isDialerRingingEnabled()) {
+ return;
+ }
+ mInCallTonePlayer.stop();
+ }
+
+ void setDialerRingingEnabledForTesting(boolean status) {
+ mIsDialerRingingEnabledForTesting = status;
+ }
+}
diff --git a/java/com/android/incallui/ringtone/InCallTonePlayer.java b/java/com/android/incallui/ringtone/InCallTonePlayer.java
new file mode 100644
index 000000000..c76b41d72
--- /dev/null
+++ b/java/com/android/incallui/ringtone/InCallTonePlayer.java
@@ -0,0 +1,168 @@
+/*
+ * 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.incallui.ringtone;
+
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.incallui.Log;
+import com.android.incallui.async.PausableExecutor;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Class responsible for playing in-call related tones in a background thread. This class only
+ * allows one tone to be played at a time.
+ */
+public class InCallTonePlayer {
+
+ public static final int TONE_CALL_WAITING = 4;
+
+ public static final int VOLUME_RELATIVE_HIGH_PRIORITY = 80;
+
+ @NonNull private final ToneGeneratorFactory mToneGeneratorFactory;
+ @NonNull private final PausableExecutor mExecutor;
+ private @Nullable CountDownLatch mNumPlayingTones;
+
+ /**
+ * Creates a new InCallTonePlayer.
+ *
+ * @param toneGeneratorFactory the {@link ToneGeneratorFactory} used to create {@link
+ * ToneGenerator}s.
+ * @param executor the {@link PausableExecutor} used to play tones in a background thread.
+ * @throws NullPointerException if audioModeProvider, toneGeneratorFactory, or executor are {@code
+ * null}.
+ */
+ public InCallTonePlayer(
+ @NonNull ToneGeneratorFactory toneGeneratorFactory, @NonNull PausableExecutor executor) {
+ mToneGeneratorFactory = Objects.requireNonNull(toneGeneratorFactory);
+ mExecutor = Objects.requireNonNull(executor);
+ }
+
+ /** @return {@code true} if a tone is currently playing, {@code false} otherwise. */
+ public boolean isPlayingTone() {
+ return mNumPlayingTones != null && mNumPlayingTones.getCount() > 0;
+ }
+
+ /**
+ * Plays the given tone in a background thread.
+ *
+ * @param tone the tone to play.
+ * @throws IllegalStateException if a tone is already playing.
+ * @throws IllegalArgumentException if the tone is invalid.
+ */
+ public void play(int tone) {
+ if (isPlayingTone()) {
+ throw new IllegalStateException("Tone already playing");
+ }
+ final ToneGeneratorInfo info = getToneGeneratorInfo(tone);
+ mNumPlayingTones = new CountDownLatch(1);
+ mExecutor.execute(
+ new Runnable() {
+ @Override
+ public void run() {
+ playOnBackgroundThread(info);
+ }
+ });
+ }
+
+ private ToneGeneratorInfo getToneGeneratorInfo(int tone) {
+ switch (tone) {
+ case TONE_CALL_WAITING:
+ /*
+ * DialerCall waiting tones play until they're stopped either by the user accepting or
+ * declining the call so the tone length is set at what's effectively forever. The
+ * tone is played at a high priority volume and through STREAM_VOICE_CALL since it's
+ * call related and using that stream will route it through bluetooth devices
+ * appropriately.
+ */
+ return new ToneGeneratorInfo(
+ ToneGenerator.TONE_SUP_CALL_WAITING,
+ VOLUME_RELATIVE_HIGH_PRIORITY,
+ Integer.MAX_VALUE,
+ AudioManager.STREAM_VOICE_CALL);
+ default:
+ throw new IllegalArgumentException("Bad tone: " + tone);
+ }
+ }
+
+ private void playOnBackgroundThread(ToneGeneratorInfo info) {
+ ToneGenerator toneGenerator = null;
+ try {
+ Log.v(this, "Starting tone " + info);
+ toneGenerator = mToneGeneratorFactory.newInCallToneGenerator(info.stream, info.volume);
+ toneGenerator.startTone(info.tone);
+ /*
+ * During tests, this will block until the tests call mExecutor.ackMilestone. This call
+ * allows for synchronization to the point where the tone has started playing.
+ */
+ mExecutor.milestone();
+ if (mNumPlayingTones != null) {
+ mNumPlayingTones.await(info.toneLengthMillis, TimeUnit.MILLISECONDS);
+ // Allows for synchronization to the point where the tone has completed playing.
+ mExecutor.milestone();
+ }
+ } catch (InterruptedException e) {
+ Log.w(this, "Interrupted while playing in-call tone.");
+ } finally {
+ if (toneGenerator != null) {
+ toneGenerator.release();
+ }
+ if (mNumPlayingTones != null) {
+ mNumPlayingTones.countDown();
+ }
+ // Allows for synchronization to the point where this background thread has cleaned up.
+ mExecutor.milestone();
+ }
+ }
+
+ /** Stops playback of the current tone. */
+ public void stop() {
+ if (mNumPlayingTones != null) {
+ mNumPlayingTones.countDown();
+ }
+ }
+
+ private static class ToneGeneratorInfo {
+
+ public final int tone;
+ public final int volume;
+ public final int toneLengthMillis;
+ public final int stream;
+
+ public ToneGeneratorInfo(int toneGeneratorType, int volume, int toneLengthMillis, int stream) {
+ this.tone = toneGeneratorType;
+ this.volume = volume;
+ this.toneLengthMillis = toneLengthMillis;
+ this.stream = stream;
+ }
+
+ @Override
+ public String toString() {
+ return "ToneGeneratorInfo{"
+ + "toneLengthMillis="
+ + toneLengthMillis
+ + ", tone="
+ + tone
+ + ", volume="
+ + volume
+ + '}';
+ }
+ }
+}
diff --git a/java/com/android/incallui/ringtone/ToneGeneratorFactory.java b/java/com/android/incallui/ringtone/ToneGeneratorFactory.java
new file mode 100644
index 000000000..cd7b11aa9
--- /dev/null
+++ b/java/com/android/incallui/ringtone/ToneGeneratorFactory.java
@@ -0,0 +1,34 @@
+/*
+ * 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.incallui.ringtone;
+
+import android.media.ToneGenerator;
+
+/** Factory used to create {@link ToneGenerator}s. */
+public class ToneGeneratorFactory {
+
+ /**
+ * Creates a new {@link ToneGenerator} to use while in a call.
+ *
+ * @param stream the stream through which to play tones.
+ * @param volume the volume at which to play tones.
+ * @return a new ToneGenerator.
+ */
+ public ToneGenerator newInCallToneGenerator(int stream, int volume) {
+ return new ToneGenerator(stream, volume);
+ }
+}
diff --git a/java/com/android/incallui/sessiondata/AndroidManifest.xml b/java/com/android/incallui/sessiondata/AndroidManifest.xml
new file mode 100644
index 000000000..11babd94d
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/AndroidManifest.xml
@@ -0,0 +1,18 @@
+<!--
+ ~ Copyright (C) 2016 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<manifest
+ package="com.android.incallui.sessiondata">
+</manifest>
diff --git a/java/com/android/incallui/sessiondata/AvatarPresenter.java b/java/com/android/incallui/sessiondata/AvatarPresenter.java
new file mode 100644
index 000000000..e7303b90a
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/AvatarPresenter.java
@@ -0,0 +1,31 @@
+/*
+ * 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.incallui.sessiondata;
+
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+
+/** Interface for interacting with Fragments that can be put in the data container */
+public interface AvatarPresenter {
+
+ @Nullable
+ ImageView getAvatarImageView();
+
+ int getAvatarSize();
+
+ boolean shouldShowAnonymousAvatar();
+}
diff --git a/java/com/android/incallui/sessiondata/MultimediaFragment.java b/java/com/android/incallui/sessiondata/MultimediaFragment.java
new file mode 100644
index 000000000..d6f671d58
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/MultimediaFragment.java
@@ -0,0 +1,231 @@
+/*
+ * 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.incallui.sessiondata;
+
+import android.graphics.drawable.Drawable;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.app.Fragment;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.multimedia.MultimediaData;
+import com.android.incallui.maps.StaticMapBinding;
+import com.android.incallui.maps.StaticMapFactory;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.DataSource;
+import com.bumptech.glide.load.engine.GlideException;
+import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
+import com.bumptech.glide.request.RequestListener;
+import com.bumptech.glide.request.target.Target;
+
+/**
+ * Displays info from {@link MultimediaData MultimediaData}.
+ *
+ * <p>Currently displays image, location (as a map), and message that come bundled with
+ * MultimediaData when calling {@link #newInstance(MultimediaData, boolean, boolean)}.
+ */
+public class MultimediaFragment extends Fragment implements AvatarPresenter {
+
+ private static final String ARG_SUBJECT = "subject";
+ private static final String ARG_IMAGE = "image";
+ private static final String ARG_LOCATION = "location";
+ private static final String ARG_INTERACTIVE = "interactive";
+ private static final String ARG_SHOW_AVATAR = "show_avatar";
+ private ImageView avatarImageView;
+ // TODO: add click listeners
+ @SuppressWarnings("unused")
+ private boolean isInteractive;
+
+ private boolean showAvatar;
+ private StaticMapFactory mapFactory;
+
+ public static MultimediaFragment newInstance(
+ @NonNull MultimediaData multimediaData, boolean isInteractive, boolean showAvatar) {
+ return newInstance(
+ multimediaData.getSubject(),
+ multimediaData.getImageUri(),
+ multimediaData.getLocation(),
+ isInteractive,
+ showAvatar);
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ public static MultimediaFragment newInstance(
+ @Nullable String subject,
+ @Nullable Uri imageUri,
+ @Nullable Location location,
+ boolean isInteractive,
+ boolean showAvatar) {
+ Bundle args = new Bundle();
+ args.putString(ARG_SUBJECT, subject);
+ args.putParcelable(ARG_IMAGE, imageUri);
+ args.putParcelable(ARG_LOCATION, location);
+ args.putBoolean(ARG_INTERACTIVE, isInteractive);
+ args.putBoolean(ARG_SHOW_AVATAR, showAvatar);
+ MultimediaFragment fragment = new MultimediaFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle bundle) {
+ super.onCreate(bundle);
+ isInteractive = getArguments().getBoolean(ARG_INTERACTIVE);
+ showAvatar = getArguments().getBoolean(ARG_SHOW_AVATAR);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ boolean hasImage = getImageUri() != null;
+ boolean hasSubject = !TextUtils.isEmpty(getSubject());
+ boolean hasMap = getLocation() != null;
+ if (hasMap) {
+ mapFactory = StaticMapBinding.get(getActivity().getApplication());
+ }
+ if (mapFactory != null) {
+ if (hasImage) {
+ if (hasSubject) {
+ return layoutInflater.inflate(
+ R.layout.fragment_composer_text_image_frag, viewGroup, false);
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_image_frag, viewGroup, false);
+ }
+ } else if (hasSubject) {
+ return layoutInflater.inflate(R.layout.fragment_composer_text_frag, viewGroup, false);
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_frag, viewGroup, false);
+ }
+ } else if (hasImage) {
+ if (hasSubject) {
+ return layoutInflater.inflate(R.layout.fragment_composer_text_image, viewGroup, false);
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_image, viewGroup, false);
+ }
+ } else {
+ return layoutInflater.inflate(R.layout.fragment_composer_text, viewGroup, false);
+ }
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ TextView messageText = (TextView) view.findViewById(R.id.answer_message_text);
+ if (messageText != null) {
+ messageText.setText(getSubject());
+ }
+ ImageView mainImage = (ImageView) view.findViewById(R.id.answer_message_image);
+ if (mainImage != null) {
+ Glide.with(this)
+ .load(getImageUri())
+ .transition(DrawableTransitionOptions.withCrossFade())
+ .listener(
+ new RequestListener<Drawable>() {
+ @Override
+ public boolean onLoadFailed(
+ @Nullable GlideException e,
+ Object model,
+ Target<Drawable> target,
+ boolean isFirstResource) {
+ view.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
+ LogUtil.e("MultimediaFragment.onLoadFailed", null, e);
+ // TODO(b/34720074) handle error cases nicely
+ return false; // Let Glide handle the rest
+ }
+
+ @Override
+ public boolean onResourceReady(
+ Drawable drawable,
+ Object model,
+ Target<Drawable> target,
+ DataSource dataSource,
+ boolean isFirstResource) {
+ view.findViewById(R.id.loading_spinner).setVisibility(View.GONE);
+ return false;
+ }
+ })
+ .into(mainImage);
+ mainImage.setClipToOutline(true);
+ }
+ FrameLayout fragmentHolder = (FrameLayout) view.findViewById(R.id.answer_message_frag);
+ if (fragmentHolder != null) {
+ fragmentHolder.setClipToOutline(true);
+ Fragment mapFragment =
+ Assert.isNotNull(mapFactory).getStaticMap(Assert.isNotNull(getLocation()));
+ getChildFragmentManager()
+ .beginTransaction()
+ .replace(R.id.answer_message_frag, mapFragment)
+ .commitNow();
+ }
+ avatarImageView = ((ImageView) view.findViewById(R.id.answer_message_avatar));
+ avatarImageView.setVisibility(showAvatar ? View.VISIBLE : View.GONE);
+
+ Holder parent = FragmentUtils.getParent(this, Holder.class);
+ if (parent != null) {
+ parent.updateAvatar(this);
+ }
+ }
+
+ @Nullable
+ @Override
+ public ImageView getAvatarImageView() {
+ return avatarImageView;
+ }
+
+ @Override
+ public int getAvatarSize() {
+ return getResources().getDimensionPixelSize(R.dimen.answer_message_avatar_size);
+ }
+
+ @Override
+ public boolean shouldShowAnonymousAvatar() {
+ return showAvatar;
+ }
+
+ @Nullable
+ public String getSubject() {
+ return getArguments().getString(ARG_SUBJECT);
+ }
+
+ @Nullable
+ public Uri getImageUri() {
+ return getArguments().getParcelable(ARG_IMAGE);
+ }
+
+ @Nullable
+ public Location getLocation() {
+ return getArguments().getParcelable(ARG_LOCATION);
+ }
+
+ /** Interface for notifying the fragment parent of changes. */
+ public interface Holder {
+ void updateAvatar(AvatarPresenter sessionDataScreen);
+ }
+}
diff --git a/java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml b/java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml
new file mode 100644
index 000000000..8826f904b
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/drawable/answer_data_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="16dp"/>
+ <solid android:color="@android:color/white"/>
+</shape>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml
new file mode 100644
index 000000000..ed2bee0d1
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_frag.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
new file mode 100644
index 000000000..7000f83b5
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_centerInParent="true"
+ android:layout_toEndOf="@+id/answer_message_avatar"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:adjustViewBounds="true"
+ android:scaleType="fitXY"/>
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/loading_spinner"
+ android:layout_centerInParent="true"/>
+</RelativeLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
new file mode 100644
index 000000000..9959f4dcc
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_image_frag.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml
new file mode 100644
index 000000000..c69973042
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml
new file mode 100644
index 000000000..5a1cf728b
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_frag.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
new file mode 100644
index 000000000..995565455
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="1"
+ android:layout_rowWeight="1"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
new file mode 100644
index 000000000..387c5cf68
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/layout/fragment_composer_text_image_frag.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingTop="16dp"
+ android:paddingStart="16dp"
+ android:paddingEnd="24dp"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/answer_message_avatar"
+ android:layout_width="@dimen/answer_message_avatar_size"
+ android:layout_height="@dimen/answer_message_avatar_size"
+ android:layout_rowSpan="2"
+ android:elevation="@dimen/answer_data_elevation"/>
+
+ <TextView
+ android:id="@id/answer_message_text"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="8dp"
+ android:layout_columnWeight="2"
+ android:layout_columnSpan="2"
+ android:layout_rowWeight="1"
+ android:padding="18dp"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:gravity="center_vertical"
+ android:maxLines="2"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Message"/>
+
+ <ImageView
+ android:id="@id/answer_message_image"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="1"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"
+ android:scaleType="centerCrop"/>
+
+ <FrameLayout
+ android:id="@id/answer_message_frag"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginStart="8dp"
+ android:layout_column="2"
+ android:layout_columnWeight="1"
+ android:layout_row="1"
+ android:layout_rowWeight="1"
+ android:background="@drawable/answer_data_background"
+ android:elevation="@dimen/answer_data_elevation"
+ android:outlineProvider="background"/>
+</GridLayout>
diff --git a/java/com/android/incallui/sessiondata/res/values/dimens.xml b/java/com/android/incallui/sessiondata/res/values/dimens.xml
new file mode 100644
index 000000000..76c7edb1b
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/values/dimens.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <dimen name="answer_message_avatar_size">40dp</dimen>
+ <dimen name="answer_data_elevation">2dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/sessiondata/res/values/ids.xml b/java/com/android/incallui/sessiondata/res/values/ids.xml
new file mode 100644
index 000000000..077474c81
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/values/ids.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <item name="answer_message_avatar" type="id"/>
+ <item name="answer_message_text" type="id"/>
+ <item name="answer_message_image" type="id"/>
+ <item name="answer_message_frag" type="id"/>
+</resources>
diff --git a/java/com/android/incallui/sessiondata/res/values/styles.xml b/java/com/android/incallui/sessiondata/res/values/styles.xml
new file mode 100644
index 000000000..dd898a4e2
--- /dev/null
+++ b/java/com/android/incallui/sessiondata/res/values/styles.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+
+<resources>
+ <style name="Dialer.Incall.TextAppearance.Message" parent="Dialer.Incall.TextAppearance">
+ <item name="android:fontFamily">sans-serif</item>
+ <item name="android:textColor">@android:color/black</item>
+ <item name="android:textSize">24sp</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/spam/NumberInCallHistoryTask.java b/java/com/android/incallui/spam/NumberInCallHistoryTask.java
new file mode 100644
index 000000000..a225606f6
--- /dev/null
+++ b/java/com/android/incallui/spam/NumberInCallHistoryTask.java
@@ -0,0 +1,107 @@
+/*
+ * 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.incallui.spam;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.provider.CallLog;
+import android.provider.CallLog.Calls;
+import android.support.annotation.NonNull;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.dialer.common.AsyncTaskExecutor;
+import com.android.dialer.common.AsyncTaskExecutors;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CallHistoryStatus;
+import java.util.Objects;
+
+/** Checks if the number is in the call history. */
+@TargetApi(VERSION_CODES.M)
+public class NumberInCallHistoryTask extends AsyncTask<Void, Void, Integer> {
+
+ public static final String TASK_ID = "number_in_call_history_status";
+
+ private final Context context;
+ private final Listener listener;
+ private final String number;
+ private final String countryIso;
+
+ public NumberInCallHistoryTask(
+ @NonNull Context context, @NonNull Listener listener, String number, String countryIso) {
+ this.context = Objects.requireNonNull(context);
+ this.listener = Objects.requireNonNull(listener);
+ this.number = number;
+ this.countryIso = countryIso;
+ }
+
+ public void submitTask() {
+ if (!PermissionsUtil.hasPhonePermissions(context)) {
+ return;
+ }
+ AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createThreadPoolExecutor();
+ asyncTaskExecutor.submit(TASK_ID, this);
+ }
+
+ @Override
+ @CallHistoryStatus
+ public Integer doInBackground(Void... params) {
+ String numberToQuery = number;
+ String fieldToQuery = Calls.NUMBER;
+ String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
+
+ // If we can normalize the number successfully, look in "normalized_number"
+ // field instead. Otherwise, look for number in "number" field.
+ if (!TextUtils.isEmpty(normalizedNumber)) {
+ numberToQuery = normalizedNumber;
+ fieldToQuery = Calls.CACHED_NORMALIZED_NUMBER;
+ }
+ try (Cursor cursor =
+ context
+ .getContentResolver()
+ .query(
+ TelecomUtil.getCallLogUri(context),
+ new String[] {CallLog.Calls._ID},
+ fieldToQuery + " = ?",
+ new String[] {numberToQuery},
+ null)) {
+ return cursor != null && cursor.getCount() > 0
+ ? DialerCall.CALL_HISTORY_STATUS_PRESENT
+ : DialerCall.CALL_HISTORY_STATUS_NOT_PRESENT;
+ } catch (SQLiteException e) {
+ LogUtil.e("NumberInCallHistoryTask.doInBackground", "query call log error", e);
+ return DialerCall.CALL_HISTORY_STATUS_UNKNOWN;
+ }
+ }
+
+ @Override
+ public void onPostExecute(@CallHistoryStatus Integer callHistoryStatus) {
+ listener.onComplete(callHistoryStatus);
+ }
+
+ /** Callback for the async task. */
+ public interface Listener {
+
+ void onComplete(@CallHistoryStatus int callHistoryStatus);
+ }
+}
diff --git a/java/com/android/incallui/spam/SpamCallListListener.java b/java/com/android/incallui/spam/SpamCallListListener.java
new file mode 100644
index 000000000..0897842de
--- /dev/null
+++ b/java/com/android/incallui/spam/SpamCallListListener.java
@@ -0,0 +1,364 @@
+/*
+ * 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.incallui.spam;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.telecom.DisconnectCause;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.blocking.FilteredNumberCompat;
+import com.android.dialer.blocking.FilteredNumbersUtil;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.logging.Logger;
+import com.android.dialer.logging.nano.ContactLookupResult;
+import com.android.dialer.logging.nano.DialerImpression;
+import com.android.dialer.spam.Spam;
+import com.android.incallui.R;
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.DialerCall.CallHistoryStatus;
+import com.android.incallui.call.DialerCall.SessionModificationState;
+import java.util.Random;
+
+/**
+ * Creates notifications after a call ends if the call matched the criteria (incoming, accepted,
+ * etc).
+ */
+public class SpamCallListListener implements CallList.Listener {
+
+ static final int NOTIFICATION_ID = 1;
+ private static final String TAG = "SpamCallListListener";
+ private final Context context;
+ private final Random random;
+
+ public SpamCallListListener(Context context) {
+ this.context = context;
+ this.random = new Random();
+ }
+
+ public SpamCallListListener(Context context, Random rand) {
+ this.context = context;
+ this.random = rand;
+ }
+
+ private static String pii(String pii) {
+ return com.android.incallui.Log.pii(pii);
+ }
+
+ @Override
+ public void onIncomingCall(final DialerCall call) {
+ String number = call.getNumber();
+ if (TextUtils.isEmpty(number)) {
+ return;
+ }
+ NumberInCallHistoryTask.Listener listener =
+ new NumberInCallHistoryTask.Listener() {
+ @Override
+ public void onComplete(@CallHistoryStatus int callHistoryStatus) {
+ call.setCallHistoryStatus(callHistoryStatus);
+ }
+ };
+ new NumberInCallHistoryTask(context, listener, number, GeoUtil.getCurrentCountryIso(context))
+ .submitTask();
+ }
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) {}
+
+ @Override
+ public void onSessionModificationStateChange(@SessionModificationState int newState) {}
+
+ @Override
+ public void onCallListChange(CallList callList) {}
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) {}
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) {}
+
+ @Override
+ public void onDisconnect(DialerCall call) {
+ if (!shouldShowAfterCallNotification(call)) {
+ return;
+ }
+ String e164Number =
+ PhoneNumberUtils.formatNumberToE164(
+ call.getNumber(), GeoUtil.getCurrentCountryIso(context));
+ if (!FilteredNumbersUtil.canBlockNumber(context, e164Number, call.getNumber())
+ || !FilteredNumberCompat.canAttemptBlockOperations(context)) {
+ return;
+ }
+ if (e164Number == null) {
+ return;
+ }
+ showNotification(call);
+ }
+
+ /** Posts the intent for displaying the after call spam notification to the user. */
+ private void showNotification(DialerCall call) {
+ if (call.isSpam()) {
+ maybeShowSpamCallNotification(call);
+ } else {
+ LogUtil.d(TAG, "Showing not spam notification for number=" + pii(call.getNumber()));
+ maybeShowNonSpamCallNotification(call);
+ }
+ }
+
+ /** Determines if the after call notification should be shown for the specified call. */
+ private boolean shouldShowAfterCallNotification(DialerCall call) {
+ if (!Spam.get(context).isSpamNotificationEnabled()) {
+ return false;
+ }
+
+ String number = call.getNumber();
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+
+ DialerCall.LogState logState = call.getLogState();
+ if (!logState.isIncoming) {
+ return false;
+ }
+
+ if (logState.duration <= 0) {
+ return false;
+ }
+
+ if (logState.contactLookupResult != ContactLookupResult.Type.NOT_FOUND
+ && logState.contactLookupResult != ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE) {
+ return false;
+ }
+
+ int callHistoryStatus = call.getCallHistoryStatus();
+ if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_PRESENT) {
+ return false;
+ } else if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_UNKNOWN) {
+ LogUtil.i(TAG, "DialerCall history status is unknown, returning false");
+ return false;
+ }
+
+ // Check if call disconnected because of either user hanging up
+ int disconnectCause = call.getDisconnectCause().getCode();
+ if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) {
+ return false;
+ }
+
+ LogUtil.i(TAG, "shouldShowAfterCallNotification, returning true");
+ return true;
+ }
+
+ /**
+ * Creates a notification builder with properties common among the two after call notifications.
+ */
+ private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
+ return new Notification.Builder(context)
+ .setContentIntent(
+ createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setPriority(Notification.PRIORITY_DEFAULT)
+ .setColor(context.getColor(R.color.dialer_theme_color))
+ .setSmallIcon(R.drawable.ic_call_end_white_24dp);
+ }
+
+ private CharSequence getDisplayNumber(DialerCall call) {
+ String formattedNumber =
+ PhoneNumberUtils.formatNumber(call.getNumber(), GeoUtil.getCurrentCountryIso(context));
+ return PhoneNumberUtilsCompat.createTtsSpannable(formattedNumber);
+ }
+
+ /** Display a notification with two actions: "add contact" and "report spam". */
+ private void showNonSpamCallNotification(DialerCall call) {
+ Notification.Builder notificationBuilder =
+ createAfterCallNotificationBuilder(call)
+ .setLargeIcon(Icon.createWithResource(context, R.drawable.unknown_notification_icon))
+ .setContentText(
+ context.getString(R.string.spam_notification_non_spam_call_collapsed_text))
+ .setStyle(
+ new Notification.BigTextStyle()
+ .bigText(
+ context.getString(R.string.spam_notification_non_spam_call_expanded_text)))
+ // Add contact
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_person_add_grey600_24dp,
+ context.getString(R.string.spam_notification_add_contact_action_text),
+ createActivityPendingIntent(
+ call, SpamNotificationActivity.ACTION_ADD_TO_CONTACTS))
+ .build())
+ // Block/report spam
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_block_grey600_24dp,
+ context.getString(R.string.spam_notification_report_spam_action_text),
+ createBlockReportSpamPendingIntent(call))
+ .build())
+ .setContentTitle(
+ context.getString(R.string.non_spam_notification_title, getDisplayNumber(call)));
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .notify(call.getNumber(), NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ private boolean shouldThrottleSpamNotification() {
+ int randomNumber = random.nextInt(100);
+ int thresholdForShowing = Spam.get(context).percentOfSpamNotificationsToShow();
+ if (thresholdForShowing == 0) {
+ LogUtil.d(
+ TAG,
+ "shouldThrottleSpamNotification, not showing - percentOfSpamNotificationsToShow is 0");
+ return true;
+ } else if (randomNumber < thresholdForShowing) {
+ LogUtil.d(
+ TAG,
+ "shouldThrottleSpamNotification, showing " + randomNumber + " < " + thresholdForShowing);
+ return false;
+ } else {
+ LogUtil.d(
+ TAG,
+ "shouldThrottleSpamNotification, not showing "
+ + randomNumber
+ + " >= "
+ + thresholdForShowing);
+ return true;
+ }
+ }
+
+ private boolean shouldThrottleNonSpamNotification() {
+ int randomNumber = random.nextInt(100);
+ int thresholdForShowing = Spam.get(context).percentOfNonSpamNotificationsToShow();
+ if (thresholdForShowing == 0) {
+ LogUtil.d(TAG, "Not showing non spam notification: percentOfNonSpamNotificationsToShow is 0");
+ return true;
+ } else if (randomNumber < thresholdForShowing) {
+ LogUtil.d(
+ TAG, "Showing non spam notification: " + randomNumber + " < " + thresholdForShowing);
+ return false;
+ } else {
+ LogUtil.d(
+ TAG, "Not showing non spam notification:" + randomNumber + " >= " + thresholdForShowing);
+ return true;
+ }
+ }
+
+ private void maybeShowSpamCallNotification(DialerCall call) {
+ if (shouldThrottleSpamNotification()) {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ } else {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ showSpamCallNotification(call);
+ }
+ }
+
+ private void maybeShowNonSpamCallNotification(DialerCall call) {
+ if (shouldThrottleNonSpamNotification()) {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.NON_SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ } else {
+ Logger.get(context)
+ .logCallImpression(
+ DialerImpression.Type.NON_SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
+ call.getUniqueCallId(),
+ call.getTimeAddedMs());
+ showNonSpamCallNotification(call);
+ }
+ }
+
+ /** Display a notification with the action "not spam". */
+ private void showSpamCallNotification(DialerCall call) {
+ Notification.Builder notificationBuilder =
+ createAfterCallNotificationBuilder(call)
+ .setLargeIcon(Icon.createWithResource(context, R.drawable.spam_notification_icon))
+ .setContentText(context.getString(R.string.spam_notification_spam_call_collapsed_text))
+ .setStyle(
+ new Notification.BigTextStyle()
+ .bigText(context.getString(R.string.spam_notification_spam_call_expanded_text)))
+ // Not spam
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_close_grey600_24dp,
+ context.getString(R.string.spam_notification_not_spam_action_text),
+ createNotSpamPendingIntent(call))
+ .build())
+ // Block/report spam
+ .addAction(
+ new Notification.Action.Builder(
+ R.drawable.ic_block_grey600_24dp,
+ context.getString(R.string.spam_notification_block_spam_action_text),
+ createBlockReportSpamPendingIntent(call))
+ .build())
+ .setContentTitle(
+ context.getString(R.string.spam_notification_title, getDisplayNumber(call)));
+ ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
+ .notify(call.getNumber(), NOTIFICATION_ID, notificationBuilder.build());
+ }
+
+ /**
+ * Creates a pending intent for block/report spam action. If enabled, this intent is forwarded to
+ * the {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
+ */
+ private PendingIntent createBlockReportSpamPendingIntent(DialerCall call) {
+ String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_SPAM;
+ return Spam.get(context).isDialogEnabledForSpamNotification()
+ ? createActivityPendingIntent(call, action)
+ : createServicePendingIntent(call, action);
+ }
+
+ /**
+ * Creates a pending intent for not spam action. If enabled, this intent is forwarded to the
+ * {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
+ */
+ private PendingIntent createNotSpamPendingIntent(DialerCall call) {
+ String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_NOT_SPAM;
+ return Spam.get(context).isDialogEnabledForSpamNotification()
+ ? createActivityPendingIntent(call, action)
+ : createServicePendingIntent(call, action);
+ }
+
+ /** Creates a pending intent for {@link SpamNotificationService}. */
+ private PendingIntent createServicePendingIntent(DialerCall call, String action) {
+ Intent intent =
+ SpamNotificationService.createServiceIntent(context, call, action, NOTIFICATION_ID);
+ return PendingIntent.getService(
+ context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
+ }
+
+ /** Creates a pending intent for {@link SpamNotificationActivity}. */
+ private PendingIntent createActivityPendingIntent(DialerCall call, String action) {
+ Intent intent =
+ SpamNotificationActivity.createActivityIntent(context, call, action, NOTIFICATION_ID);
+ return PendingIntent.getActivity(
+ context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
+ }
+}
diff --git a/java/com/android/incallui/spam/SpamNotificationActivity.java b/java/com/android/incallui/spam/SpamNotificationActivity.java
new file mode 100644
index 000000000..88d6bdfda
--- /dev/null
+++ b/java/com/android/incallui/spam/SpamNotificationActivity.java
@@ -0,0 +1,483 @@
+/*
+ * 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.incallui.spam;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentActivity;
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
+import com.android.dialer.blocking.BlockReportSpamDialogs;
+import com.android.dialer.blocking.BlockedNumbersMigrator;
+import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
+import com.android.dialer.blocking.FilteredNumberCompat;
+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;
+import com.android.incallui.R;
+import com.android.incallui.call.DialerCall;
+
+/** Creates the after call notification dialogs. */
+public class SpamNotificationActivity extends FragmentActivity {
+
+ /** Action to add number to contacts. */
+ static final String ACTION_ADD_TO_CONTACTS = "com.android.incallui.spam.ACTION_ADD_TO_CONTACTS";
+ /** Action to show dialog. */
+ static final String ACTION_SHOW_DIALOG = "com.android.incallui.spam.ACTION_SHOW_DIALOG";
+ /** Action to mark a number as spam. */
+ static final String ACTION_MARK_NUMBER_AS_SPAM =
+ "com.android.incallui.spam.ACTION_MARK_NUMBER_AS_SPAM";
+ /** Action to mark a number as not spam. */
+ static final String ACTION_MARK_NUMBER_AS_NOT_SPAM =
+ "com.android.incallui.spam.ACTION_MARK_NUMBER_AS_NOT_SPAM";
+
+ private static final String TAG = "SpamNotifications";
+ private static final String EXTRA_NOTIFICATION_ID = "notification_id";
+ private static final String EXTRA_CALL_INFO = "call_info";
+
+ private static final String CALL_INFO_KEY_PHONE_NUMBER = "phone_number";
+ private static final String CALL_INFO_KEY_IS_SPAM = "is_spam";
+ private static final String CALL_INFO_KEY_CALL_ID = "call_id";
+ private static final String CALL_INFO_KEY_START_TIME_MILLIS = "call_start_time_millis";
+ private static final String CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE = "contact_lookup_result_type";
+ private final DialogInterface.OnDismissListener dismissListener =
+ new DialogInterface.OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+ };
+ private FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler;
+
+ /**
+ * Creates an intent to start this activity.
+ *
+ * @return Intent intent that starts this activity.
+ */
+ public static Intent createActivityIntent(
+ Context context, DialerCall call, String action, int notificationId) {
+ Intent intent = new Intent(context, SpamNotificationActivity.class);
+ intent.setAction(action);
+ // This ensures only one activity of this kind exists at a time.
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
+ intent.putExtra(EXTRA_CALL_INFO, newCallInfoBundle(call));
+ return intent;
+ }
+
+ /** Creates the intent to insert a contact. */
+ private static Intent createInsertContactsIntent(String number) {
+ Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION);
+ // This ensures that the edit contact number field gets updated if called more than once.
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.setType(ContactsContract.RawContacts.CONTENT_TYPE);
+ intent.putExtra(ContactsContract.Intents.Insert.PHONE, number);
+ return intent;
+ }
+
+ /** Returns the formatted version of the given number. */
+ private static String getFormattedNumber(String number) {
+ return PhoneNumberUtilsCompat.createTtsSpannable(number).toString();
+ }
+
+ private static void logCallImpression(Context context, Bundle bundle, int impression) {
+ Logger.get(context)
+ .logCallImpression(
+ impression,
+ bundle.getString(CALL_INFO_KEY_CALL_ID),
+ bundle.getLong(CALL_INFO_KEY_START_TIME_MILLIS, 0));
+ }
+
+ private static Bundle newCallInfoBundle(DialerCall call) {
+ Bundle bundle = new Bundle();
+ bundle.putString(CALL_INFO_KEY_PHONE_NUMBER, call.getNumber());
+ bundle.putBoolean(CALL_INFO_KEY_IS_SPAM, call.isSpam());
+ bundle.putString(CALL_INFO_KEY_CALL_ID, call.getUniqueCallId());
+ bundle.putLong(CALL_INFO_KEY_START_TIME_MILLIS, call.getTimeAddedMs());
+ bundle.putInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, call.getLogState().contactLookupResult);
+ return bundle;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ LogUtil.i(TAG, "onCreate");
+ super.onCreate(savedInstanceState);
+ setFinishOnTouchOutside(true);
+ filteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(this);
+ cancelNotification();
+ }
+
+ @Override
+ protected void onResume() {
+ LogUtil.i(TAG, "onResume");
+ super.onResume();
+ Intent intent = getIntent();
+ String number = getCallInfo().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ boolean isSpam = getCallInfo().getBoolean(CALL_INFO_KEY_IS_SPAM);
+ int contactLookupResultType = getCallInfo().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0);
+ switch (intent.getAction()) {
+ case ACTION_ADD_TO_CONTACTS:
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS);
+ startActivity(createInsertContactsIntent(number));
+ finish();
+ break;
+ case ACTION_MARK_NUMBER_AS_SPAM:
+ assertDialogsEnabled();
+ maybeShowBlockReportSpamDialog(number, contactLookupResultType);
+ break;
+ case ACTION_MARK_NUMBER_AS_NOT_SPAM:
+ assertDialogsEnabled();
+ maybeShowNotSpamDialog(number, contactLookupResultType);
+ break;
+ case ACTION_SHOW_DIALOG:
+ if (isSpam) {
+ showSpamFullDialog();
+ } else {
+ showNonSpamDialog();
+ }
+ break;
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ LogUtil.d(TAG, "onPause");
+ // Finish activity on pause (e.g: orientation change or back button pressed)
+ filteredNumberAsyncQueryHandler = null;
+ if (!isFinishing()) {
+ finish();
+ }
+ super.onPause();
+ }
+
+ /** Creates and displays the dialog for whitelisting a number. */
+ private void maybeShowNotSpamDialog(final String number, final int contactLookupResultType) {
+ if (Spam.get(this).isDialogEnabledForSpamNotification()) {
+ BlockReportSpamDialogs.ReportNotSpamDialogFragment.newInstance(
+ getFormattedNumber(number),
+ new BlockReportSpamDialogs.OnConfirmListener() {
+ @Override
+ public void onClick() {
+ reportNotSpamAndFinish(number, contactLookupResultType);
+ }
+ },
+ dismissListener)
+ .show(getFragmentManager(), BlockReportSpamDialogs.NOT_SPAM_DIALOG_TAG);
+ } else {
+ reportNotSpamAndFinish(number, contactLookupResultType);
+ }
+ }
+
+ /** Creates and displays the dialog for blocking/reporting a number as spam. */
+ private void maybeShowBlockReportSpamDialog(
+ final String number, final int contactLookupResultType) {
+ if (Spam.get(this).isDialogEnabledForSpamNotification()) {
+ maybeShowBlockNumberMigrationDialog(
+ new BlockedNumbersMigrator.Listener() {
+ @Override
+ public void onComplete() {
+ BlockReportSpamDialogs.BlockReportSpamDialogFragment.newInstance(
+ getFormattedNumber(number),
+ Spam.get(SpamNotificationActivity.this).isDialogReportSpamCheckedByDefault(),
+ new BlockReportSpamDialogs.OnSpamDialogClickListener() {
+ @Override
+ public void onClick(boolean isSpamChecked) {
+ blockReportNumberAndFinish(
+ number, isSpamChecked, contactLookupResultType);
+ }
+ },
+ dismissListener)
+ .show(getFragmentManager(), BlockReportSpamDialogs.BLOCK_REPORT_SPAM_DIALOG_TAG);
+ }
+ });
+ } else {
+ blockReportNumberAndFinish(number, true, contactLookupResultType);
+ }
+ }
+
+ /**
+ * Displays the dialog for the first time unknown calls with actions "Add contact", "Block/report
+ * spam", and "Dismiss".
+ */
+ private void showNonSpamDialog() {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG);
+ FirstTimeNonSpamCallDialogFragment.newInstance(getCallInfo())
+ .show(getSupportFragmentManager(), FirstTimeNonSpamCallDialogFragment.TAG);
+ }
+
+ /**
+ * Displays the dialog for first time spam calls with actions "Not spam", "Block", and "Dismiss".
+ */
+ private void showSpamFullDialog() {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG);
+ FirstTimeSpamCallDialogFragment.newInstance(getCallInfo())
+ .show(getSupportFragmentManager(), FirstTimeSpamCallDialogFragment.TAG);
+ }
+
+ /** Checks if the user has migrated to the new blocking and display a dialog if necessary. */
+ private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) {
+ if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog(
+ this, getFragmentManager(), listener)) {
+ listener.onComplete();
+ }
+ }
+
+ /** Block and report the number as spam. */
+ private void blockReportNumberAndFinish(
+ String number, boolean reportAsSpam, int contactLookupResultType) {
+ if (reportAsSpam) {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM);
+ Spam.get(this)
+ .reportSpamFromAfterCallNotification(
+ number,
+ getCountryIso(),
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ }
+
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER);
+ filteredNumberAsyncQueryHandler.blockNumber(null, number, getCountryIso());
+ // TODO: DialerCall finish() after block/reporting async tasks complete (b/28441936)
+ finish();
+ }
+
+ /** Report the number as not spam. */
+ private void reportNotSpamAndFinish(String number, int contactLookupResultType) {
+ logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM);
+ Spam.get(this)
+ .reportNotSpamFromAfterCallNotification(
+ number,
+ getCountryIso(),
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ // TODO: DialerCall finish() after async task completes (b/28441936)
+ finish();
+ }
+
+ /** Cancels the notification associated with the number. */
+ private void cancelNotification() {
+ int notificationId = getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 1);
+ String number = getCallInfo().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
+ .cancel(number, notificationId);
+ }
+
+ private String getCountryIso() {
+ return GeoUtil.getCurrentCountryIso(this);
+ }
+
+ private void assertDialogsEnabled() {
+ if (!Spam.get(this).isDialogEnabledForSpamNotification()) {
+ throw new IllegalStateException(
+ "Cannot start this activity with given action because dialogs are not enabled.");
+ }
+ }
+
+ private Bundle getCallInfo() {
+ return getIntent().getBundleExtra(EXTRA_CALL_INFO);
+ }
+
+ private void logCallImpression(int impression) {
+ logCallImpression(this, getCallInfo(), impression);
+ }
+
+ /** Dialog that displays "Not spam", "Block/report spam" and "Dismiss". */
+ public static class FirstTimeSpamCallDialogFragment extends DialogFragment {
+
+ public static final String TAG = "FirstTimeSpamDialog";
+
+ private boolean dismissed;
+ private Context applicationContext;
+
+ private static DialogFragment newInstance(Bundle bundle) {
+ FirstTimeSpamCallDialogFragment fragment = new FirstTimeSpamCallDialogFragment();
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public void onPause() {
+ dismiss();
+ super.onPause();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ logCallImpression(
+ applicationContext,
+ getArguments(),
+ DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG);
+ super.onDismiss(dialog);
+ // If dialog was not dismissed by user pressing one of the buttons, finish activity
+ if (!dismissed && getActivity() != null && !getActivity().isFinishing()) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ applicationContext = context.getApplicationContext();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ final SpamNotificationActivity spamNotificationActivity =
+ (SpamNotificationActivity) getActivity();
+ final String number = getArguments().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ final int contactLookupResultType =
+ getArguments().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0);
+
+ return new AlertDialog.Builder(getActivity())
+ .setCancelable(false)
+ .setTitle(getString(R.string.spam_notification_title, getFormattedNumber(number)))
+ .setMessage(getString(R.string.spam_notification_spam_call_expanded_text))
+ .setNeutralButton(
+ getString(R.string.notification_action_dismiss),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ })
+ .setPositiveButton(
+ getString(R.string.spam_notification_dialog_was_not_spam_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ spamNotificationActivity.maybeShowNotSpamDialog(number, contactLookupResultType);
+ }
+ })
+ .setNegativeButton(
+ getString(R.string.spam_notification_block_spam_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ spamNotificationActivity.maybeShowBlockReportSpamDialog(
+ number, contactLookupResultType);
+ }
+ })
+ .create();
+ }
+ }
+
+ /** Dialog that displays "Add contact", "Block/report spam" and "Dismiss". */
+ public static class FirstTimeNonSpamCallDialogFragment extends DialogFragment {
+
+ public static final String TAG = "FirstTimeNonSpamDialog";
+
+ private boolean dismissed;
+ private Context context;
+
+ private static DialogFragment newInstance(Bundle bundle) {
+ FirstTimeNonSpamCallDialogFragment fragment = new FirstTimeNonSpamCallDialogFragment();
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public void onPause() {
+ // Dismiss on pause e.g: orientation change
+ dismiss();
+ super.onPause();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ super.onDismiss(dialog);
+ logCallImpression(
+ context,
+ getArguments(),
+ DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG);
+ // If dialog was not dismissed by user pressing one of the buttons, finish activity
+ if (!dismissed && getActivity() != null && !getActivity().isFinishing()) {
+ getActivity().finish();
+ }
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ this.context = context.getApplicationContext();
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ super.onCreateDialog(savedInstanceState);
+ final SpamNotificationActivity spamNotificationActivity =
+ (SpamNotificationActivity) getActivity();
+ final String number = getArguments().getString(CALL_INFO_KEY_PHONE_NUMBER);
+ final int contactLookupResultType =
+ getArguments().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0);
+ return new AlertDialog.Builder(getActivity())
+ .setTitle(getString(R.string.non_spam_notification_title, getFormattedNumber(number)))
+ .setCancelable(false)
+ .setMessage(getString(R.string.spam_notification_non_spam_call_expanded_text))
+ .setNeutralButton(
+ getString(R.string.notification_action_dismiss),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ }
+ })
+ .setPositiveButton(
+ getString(R.string.spam_notification_dialog_add_contact_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ startActivity(createInsertContactsIntent(number));
+ }
+ })
+ .setNegativeButton(
+ getString(R.string.spam_notification_dialog_block_report_spam_action_text),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismissed = true;
+ dismiss();
+ spamNotificationActivity.maybeShowBlockReportSpamDialog(
+ number, contactLookupResultType);
+ }
+ })
+ .create();
+ }
+ }
+}
diff --git a/java/com/android/incallui/spam/SpamNotificationService.java b/java/com/android/incallui/spam/SpamNotificationService.java
new file mode 100644
index 000000000..bf107f789
--- /dev/null
+++ b/java/com/android/incallui/spam/SpamNotificationService.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.incallui.spam;
+
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.provider.CallLog;
+import android.support.annotation.Nullable;
+import com.android.contacts.common.GeoUtil;
+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;
+import com.android.incallui.call.DialerCall;
+
+/**
+ * This service determines if the device is locked/unlocked and takes an action based on the state.
+ * A service is used to to determine this, as opposed to an activity, because the user must unlock
+ * the device before a notification can start an activity. This is not the case for a service, and
+ * intents can be sent to this service even from the lock screen. This allows users to quickly
+ * report a number as spam or not spam from their lock screen.
+ */
+public class SpamNotificationService extends Service {
+
+ private static final String TAG = "SpamNotificationSvc";
+
+ private static final String EXTRA_PHONE_NUMBER = "service_phone_number";
+ private static final String EXTRA_CALL_ID = "service_call_id";
+ private static final String EXTRA_CALL_START_TIME_MILLIS = "service_call_start_time_millis";
+ private static final String EXTRA_NOTIFICATION_ID = "service_notification_id";
+ private static final String EXTRA_CONTACT_LOOKUP_RESULT_TYPE =
+ "service_contact_lookup_result_type";
+ /** Creates an intent to start this service. */
+ public static Intent createServiceIntent(
+ Context context, DialerCall call, String action, int notificationId) {
+ Intent intent = new Intent(context, SpamNotificationService.class);
+ intent.setAction(action);
+ intent.putExtra(EXTRA_PHONE_NUMBER, call.getNumber());
+ intent.putExtra(EXTRA_CALL_ID, call.getUniqueCallId());
+ intent.putExtra(EXTRA_CALL_START_TIME_MILLIS, call.getTimeAddedMs());
+ intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId);
+ intent.putExtra(EXTRA_CONTACT_LOOKUP_RESULT_TYPE, call.getLogState().contactLookupResult);
+ return intent;
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ // Return null because clients cannot bind to this service
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ LogUtil.d(TAG, "onStartCommand");
+ if (intent == null) {
+ LogUtil.d(TAG, "Null intent");
+ stopSelf();
+ // Return {@link #START_NOT_STICKY} so service is not restarted.
+ return START_NOT_STICKY;
+ }
+ String number = intent.getStringExtra(EXTRA_PHONE_NUMBER);
+ int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, 1);
+ String countryIso = GeoUtil.getCurrentCountryIso(this);
+ int contactLookupResultType = intent.getIntExtra(EXTRA_CONTACT_LOOKUP_RESULT_TYPE, 0);
+
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE))
+ .cancel(number, notificationId);
+
+ switch (intent.getAction()) {
+ case SpamNotificationActivity.ACTION_MARK_NUMBER_AS_SPAM:
+ logCallImpression(
+ intent, DialerImpression.Type.SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_SPAM);
+ Spam.get(this)
+ .reportSpamFromAfterCallNotification(
+ number,
+ countryIso,
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ new FilteredNumberAsyncQueryHandler(this).blockNumber(null, number, countryIso);
+ break;
+ case SpamNotificationActivity.ACTION_MARK_NUMBER_AS_NOT_SPAM:
+ logCallImpression(
+ intent, DialerImpression.Type.SPAM_NOTIFICATION_SERVICE_ACTION_MARK_NUMBER_AS_NOT_SPAM);
+ Spam.get(this)
+ .reportNotSpamFromAfterCallNotification(
+ number,
+ countryIso,
+ CallLog.Calls.INCOMING_TYPE,
+ ReportingLocation.Type.FEEDBACK_PROMPT,
+ contactLookupResultType);
+ break;
+ }
+ // TODO: call stopSelf() after async tasks complete (b/28441936)
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ LogUtil.d(TAG, "onDestroy");
+ }
+
+ private void logCallImpression(Intent intent, int impression) {
+ Logger.get(this)
+ .logCallImpression(
+ impression,
+ intent.getStringExtra(EXTRA_CALL_ID),
+ intent.getLongExtra(EXTRA_CALL_START_TIME_MILLIS, 0));
+ }
+}
diff --git a/java/com/android/incallui/util/AccessibilityUtil.java b/java/com/android/incallui/util/AccessibilityUtil.java
new file mode 100644
index 000000000..65753484a
--- /dev/null
+++ b/java/com/android/incallui/util/AccessibilityUtil.java
@@ -0,0 +1,35 @@
+/*
+ * 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.incallui.util;
+
+import android.content.Context;
+import android.view.accessibility.AccessibilityManager;
+
+public class AccessibilityUtil {
+
+ public static boolean isAccessibilityEnabled(Context context) {
+ AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ return accessibilityManager.isEnabled();
+ }
+
+ public static boolean isTouchExplorationEnabled(Context context) {
+ AccessibilityManager accessibilityManager =
+ context.getSystemService(AccessibilityManager.class);
+ return accessibilityManager.isTouchExplorationEnabled();
+ }
+}
diff --git a/java/com/android/incallui/util/TelecomCallUtil.java b/java/com/android/incallui/util/TelecomCallUtil.java
new file mode 100644
index 000000000..8855543b1
--- /dev/null
+++ b/java/com/android/incallui/util/TelecomCallUtil.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.incallui.util;
+
+import android.net.Uri;
+import android.telecom.Call;
+import android.telephony.PhoneNumberUtils;
+
+/**
+ * Class to provide a standard interface for obtaining information from the underlying
+ * android.telecom.Call. Much of this should be obtained through the incall.Call, but on occasion we
+ * need to interact with the telecom.Call directly (eg. call blocking, before the incall.Call has
+ * been created).
+ */
+public class TelecomCallUtil {
+
+ // Whether the call handle is an emergency number.
+ public static boolean isEmergencyCall(Call call) {
+ Uri handle = call.getDetails().getHandle();
+ return PhoneNumberUtils.isEmergencyNumber(handle == null ? "" : handle.getSchemeSpecificPart());
+ }
+
+ public static String getNumber(Call call) {
+ if (call == null) {
+ return null;
+ }
+ if (call.getDetails().getGatewayInfo() != null) {
+ return call.getDetails().getGatewayInfo().getOriginalAddress().getSchemeSpecificPart();
+ }
+ Uri handle = getHandle(call);
+ return handle == null ? null : handle.getSchemeSpecificPart();
+ }
+
+ public static Uri getHandle(Call call) {
+ return call == null ? null : call.getDetails().getHandle();
+ }
+}
diff --git a/java/com/android/incallui/video/bindings/VideoBindings.java b/java/com/android/incallui/video/bindings/VideoBindings.java
new file mode 100644
index 000000000..934ff078a
--- /dev/null
+++ b/java/com/android/incallui/video/bindings/VideoBindings.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.incallui.video.bindings;
+
+import com.android.incallui.video.impl.VideoCallFragment;
+import com.android.incallui.video.protocol.VideoCallScreen;
+
+/** Bindings for video module. */
+public class VideoBindings {
+
+ public static VideoCallScreen createVideoCallScreen() {
+ return new VideoCallFragment();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/AndroidManifest.xml b/java/com/android/incallui/video/impl/AndroidManifest.xml
new file mode 100644
index 000000000..a36828e29
--- /dev/null
+++ b/java/com/android/incallui/video/impl/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.video.impl">
+</manifest>
diff --git a/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java b/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java
new file mode 100644
index 000000000..291fce4a0
--- /dev/null
+++ b/java/com/android/incallui/video/impl/CameraPermissionDialogFragment.java
@@ -0,0 +1,62 @@
+/*
+ * 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.incallui.video.impl;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v4.app.DialogFragment;
+import com.android.dialer.common.FragmentUtils;
+
+/** Dialog fragment to ask for camera permission from user. */
+public class CameraPermissionDialogFragment extends DialogFragment {
+
+ static CameraPermissionDialogFragment newInstance() {
+ CameraPermissionDialogFragment fragment = new CameraPermissionDialogFragment();
+ return fragment;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle bundle) {
+ return new AlertDialog.Builder(getContext())
+ .setTitle(R.string.camera_permission_dialog_title)
+ .setMessage(R.string.camera_permission_dialog_message)
+ .setPositiveButton(
+ R.string.camera_permission_dialog_positive_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ VideoCallFragment fragment =
+ FragmentUtils.getParentUnsafe(
+ CameraPermissionDialogFragment.this, VideoCallFragment.class);
+ fragment.onCameraPermissionGranted();
+ }
+ })
+ .setNegativeButton(
+ R.string.camera_permission_dialog_negative_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .create();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/CheckableImageButton.java b/java/com/android/incallui/video/impl/CheckableImageButton.java
new file mode 100644
index 000000000..320f0571a
--- /dev/null
+++ b/java/com/android/incallui/video/impl/CheckableImageButton.java
@@ -0,0 +1,222 @@
+/*
+ * 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.incallui.video.impl;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.SoundEffectConstants;
+import android.widget.Checkable;
+import android.widget.ImageButton;
+
+/** Image button that maintains a checked state. */
+public class CheckableImageButton extends ImageButton implements Checkable {
+
+ private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
+
+ /** Callback interface to notify when the button's checked state has changed */
+ public interface OnCheckedChangeListener {
+
+ void onCheckedChanged(CheckableImageButton button, boolean isChecked);
+ }
+
+ private boolean broadcasting;
+ private boolean isChecked;
+ private OnCheckedChangeListener onCheckedChangeListener;
+ private CharSequence contentDescriptionChecked;
+ private CharSequence contentDescriptionUnchecked;
+
+ public CheckableImageButton(Context context) {
+ this(context, null);
+ }
+
+ public CheckableImageButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public CheckableImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs);
+ }
+
+ private void init(Context context, AttributeSet attrs) {
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CheckableImageButton);
+ setChecked(typedArray.getBoolean(R.styleable.CheckableImageButton_android_checked, false));
+ contentDescriptionChecked =
+ typedArray.getText(R.styleable.CheckableImageButton_contentDescriptionChecked);
+ contentDescriptionUnchecked =
+ typedArray.getText(R.styleable.CheckableImageButton_contentDescriptionUnchecked);
+ typedArray.recycle();
+
+ updateContentDescription();
+ setClickable(true);
+ setFocusable(true);
+ }
+
+ @Override
+ public void setChecked(boolean checked) {
+ performSetChecked(checked);
+ }
+
+ /**
+ * Called when the state of the button should be updated, this should not be the result of user
+ * interaction.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void performSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ isChecked = checked;
+ CharSequence contentDescription = updateContentDescription();
+ announceForAccessibility(contentDescription);
+ refreshDrawableState();
+ }
+
+ private CharSequence updateContentDescription() {
+ CharSequence contentDescription =
+ isChecked ? contentDescriptionChecked : contentDescriptionUnchecked;
+ setContentDescription(contentDescription);
+ return contentDescription;
+ }
+
+ /**
+ * Called when the user interacts with a button. This should not result in the button updating
+ * state, rather the request should be propagated to the associated listener.
+ *
+ * @param checked {@code true} if the button should be in the checked state, {@code false}
+ * otherwise.
+ */
+ private void userRequestedSetChecked(boolean checked) {
+ if (isChecked() == checked) {
+ return;
+ }
+ if (broadcasting) {
+ return;
+ }
+ broadcasting = true;
+ if (onCheckedChangeListener != null) {
+ onCheckedChangeListener.onCheckedChanged(this, checked);
+ }
+ broadcasting = false;
+ }
+
+ @Override
+ public boolean isChecked() {
+ return isChecked;
+ }
+
+ @Override
+ public void toggle() {
+ userRequestedSetChecked(!isChecked());
+ }
+
+ @Override
+ public int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+ if (isChecked()) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ invalidate();
+ }
+
+ public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+ this.onCheckedChangeListener = listener;
+ }
+
+ @Override
+ public boolean performClick() {
+ if (!isCheckable()) {
+ return super.performClick();
+ }
+
+ toggle();
+ final boolean handled = super.performClick();
+ if (!handled) {
+ // View only makes a sound effect if the onClickListener was
+ // called, so we'll need to make one here instead.
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return handled;
+ }
+
+ private boolean isCheckable() {
+ return onCheckedChangeListener != null;
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ SavedState savedState = (SavedState) state;
+ super.onRestoreInstanceState(savedState.getSuperState());
+ performSetChecked(savedState.isChecked);
+ requestLayout();
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ return new SavedState(isChecked(), super.onSaveInstanceState());
+ }
+
+ private static class SavedState extends BaseSavedState {
+
+ public final boolean isChecked;
+
+ private SavedState(boolean isChecked, Parcelable superState) {
+ super(superState);
+ this.isChecked = isChecked;
+ }
+
+ protected SavedState(Parcel in) {
+ super(in);
+ isChecked = in.readByte() != 0;
+ }
+
+ public static final Creator<SavedState> CREATOR =
+ new Creator<SavedState>() {
+ @Override
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ @Override
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeByte((byte) (isChecked ? 1 : 0));
+ }
+ }
+}
diff --git a/java/com/android/incallui/video/impl/SpeakerButtonController.java b/java/com/android/incallui/video/impl/SpeakerButtonController.java
new file mode 100644
index 000000000..e12032abf
--- /dev/null
+++ b/java/com/android/incallui/video/impl/SpeakerButtonController.java
@@ -0,0 +1,118 @@
+/*
+ * 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.incallui.video.impl;
+
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.telecom.CallAudioState;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+
+/** Manages a single button. */
+public class SpeakerButtonController implements OnCheckedChangeListener, OnClickListener {
+
+ @NonNull private final InCallButtonUiDelegate inCallButtonUiDelegate;
+ @NonNull private final VideoCallScreenDelegate videoCallScreenDelegate;
+
+ @NonNull private CheckableImageButton button;
+
+ @DrawableRes private int icon = R.drawable.quantum_ic_volume_up_white_36;
+
+ private boolean isChecked;
+ private boolean checkable;
+ private boolean isEnabled;
+ private CharSequence contentDescription;
+
+ public SpeakerButtonController(
+ @NonNull CheckableImageButton button,
+ @NonNull InCallButtonUiDelegate inCallButtonUiDelegate,
+ @NonNull VideoCallScreenDelegate videoCallScreenDelegate) {
+ this.inCallButtonUiDelegate = Assert.isNotNull(inCallButtonUiDelegate);
+ this.videoCallScreenDelegate = Assert.isNotNull(videoCallScreenDelegate);
+ this.button = Assert.isNotNull(button);
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ }
+
+ public void updateButtonState() {
+ button.setVisibility(View.VISIBLE);
+ button.setEnabled(isEnabled);
+ button.setChecked(isChecked);
+ button.setOnClickListener(checkable ? null : this);
+ button.setOnCheckedChangeListener(checkable ? this : null);
+ button.setImageResource(icon);
+ button.setContentDescription(contentDescription);
+ }
+
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("SpeakerButtonController.setSupportedAudio", "audioState: " + audioState);
+
+ @StringRes int contentDescriptionResId;
+ if ((audioState.getSupportedRouteMask() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ checkable = false;
+ isChecked = false;
+
+ if ((audioState.getRoute() & CallAudioState.ROUTE_BLUETOOTH)
+ == CallAudioState.ROUTE_BLUETOOTH) {
+ icon = R.drawable.quantum_ic_bluetooth_audio_white_36;
+ contentDescriptionResId = R.string.incall_content_description_bluetooth;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_SPEAKER)
+ == CallAudioState.ROUTE_SPEAKER) {
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ } else if ((audioState.getRoute() & CallAudioState.ROUTE_WIRED_HEADSET)
+ == CallAudioState.ROUTE_WIRED_HEADSET) {
+ icon = R.drawable.quantum_ic_headset_white_36;
+ contentDescriptionResId = R.string.incall_content_description_headset;
+ } else {
+ icon = R.drawable.ic_phone_audio_white_36dp;
+ contentDescriptionResId = R.string.incall_content_description_earpiece;
+ }
+ } else {
+ checkable = true;
+ isChecked = audioState.getRoute() == CallAudioState.ROUTE_SPEAKER;
+ icon = R.drawable.quantum_ic_volume_up_white_36;
+ contentDescriptionResId = R.string.incall_content_description_speaker;
+ }
+
+ contentDescription = button.getContext().getText(contentDescriptionResId);
+ updateButtonState();
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
+ LogUtil.i("SpeakerButtonController.onCheckedChanged", null);
+ inCallButtonUiDelegate.toggleSpeakerphone();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+
+ @Override
+ public void onClick(View view) {
+ LogUtil.i("SpeakerButtonController.onClick", null);
+ inCallButtonUiDelegate.showAudioRouteSelector();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java b/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java
new file mode 100644
index 000000000..372b56b4e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/SwitchOnHoldCallController.java
@@ -0,0 +1,91 @@
+/*
+ * 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.incallui.video.impl;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.view.View;
+import android.view.View.OnClickListener;
+import com.android.dialer.common.Assert;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+
+/** Manages the swap button and on hold banner. */
+public class SwitchOnHoldCallController implements OnClickListener {
+
+ @NonNull private InCallScreenDelegate inCallScreenDelegate;
+ @NonNull private VideoCallScreenDelegate videoCallScreenDelegate;
+
+ @NonNull private View switchOnHoldButton;
+
+ @NonNull private View onHoldBanner;
+
+ private boolean isVisible;
+
+ private boolean isEnabled;
+
+ @Nullable private SecondaryInfo secondaryInfo;
+
+ public SwitchOnHoldCallController(
+ @NonNull View switchOnHoldButton,
+ @NonNull View onHoldBanner,
+ @NonNull InCallScreenDelegate inCallScreenDelegate,
+ @NonNull VideoCallScreenDelegate videoCallScreenDelegate) {
+ this.switchOnHoldButton = Assert.isNotNull(switchOnHoldButton);
+ switchOnHoldButton.setOnClickListener(this);
+ this.onHoldBanner = Assert.isNotNull(onHoldBanner);
+ this.inCallScreenDelegate = Assert.isNotNull(inCallScreenDelegate);
+ this.videoCallScreenDelegate = Assert.isNotNull(videoCallScreenDelegate);
+ }
+
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ updateButtonState();
+ }
+
+ public void setVisible(boolean isVisible) {
+ this.isVisible = isVisible;
+ updateButtonState();
+ }
+
+ public void setOnScreen() {
+ isVisible = hasSecondaryInfo();
+ updateButtonState();
+ }
+
+ public void setSecondaryInfo(@Nullable SecondaryInfo secondaryInfo) {
+ this.secondaryInfo = secondaryInfo;
+ isVisible = hasSecondaryInfo();
+ }
+
+ private boolean hasSecondaryInfo() {
+ return secondaryInfo != null && secondaryInfo.shouldShow;
+ }
+
+ public void updateButtonState() {
+ switchOnHoldButton.setEnabled(isEnabled);
+ switchOnHoldButton.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
+ onHoldBanner.setVisibility(isVisible ? View.VISIBLE : View.INVISIBLE);
+ }
+
+ @Override
+ public void onClick(View view) {
+ inCallScreenDelegate.onSecondaryInfoClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+}
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
new file mode 100644
index 000000000..77a67d032
--- /dev/null
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -0,0 +1,1215 @@
+/*
+ * 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.incallui.video.impl;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Outline;
+import android.graphics.Point;
+import android.graphics.drawable.Animatable;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.renderscript.Allocation;
+import android.renderscript.Element;
+import android.renderscript.RenderScript;
+import android.renderscript.ScriptIntrinsicBlur;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.LinearOutSlowInInterpolator;
+import android.telecom.CallAudioState;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnSystemUiVisibilityChangeListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.MarginLayoutParams;
+import android.view.ViewOutlineProvider;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.FragmentUtils;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.compat.ActivityCompat;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment;
+import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
+import com.android.incallui.call.VideoUtils;
+import com.android.incallui.contactgrid.ContactGridManager;
+import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.protocol.InCallButtonIds;
+import com.android.incallui.incall.protocol.InCallButtonIdsExtension;
+import com.android.incallui.incall.protocol.InCallButtonUi;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegate;
+import com.android.incallui.incall.protocol.InCallButtonUiDelegateFactory;
+import com.android.incallui.incall.protocol.InCallScreen;
+import com.android.incallui.incall.protocol.InCallScreenDelegate;
+import com.android.incallui.incall.protocol.InCallScreenDelegateFactory;
+import com.android.incallui.incall.protocol.PrimaryCallState;
+import com.android.incallui.incall.protocol.PrimaryInfo;
+import com.android.incallui.incall.protocol.SecondaryInfo;
+import com.android.incallui.video.impl.CheckableImageButton.OnCheckedChangeListener;
+import com.android.incallui.video.protocol.VideoCallScreen;
+import com.android.incallui.video.protocol.VideoCallScreenDelegate;
+import com.android.incallui.video.protocol.VideoCallScreenDelegateFactory;
+import com.android.incallui.videosurface.bindings.VideoSurfaceBindings;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Contains UI elements for a video call. */
+public class VideoCallFragment extends Fragment
+ implements InCallScreen,
+ InCallButtonUi,
+ VideoCallScreen,
+ OnClickListener,
+ OnCheckedChangeListener,
+ AudioRouteSelectorPresenter,
+ OnSystemUiVisibilityChangeListener {
+
+ private static final float BLUR_PREVIEW_RADIUS = 16.0f;
+ private static final float BLUR_PREVIEW_SCALE_FACTOR = 1.0f;
+ private static final float BLUR_REMOTE_RADIUS = 25.0f;
+ private static final float BLUR_REMOTE_SCALE_FACTOR = 0.25f;
+ private static final float ASPECT_RATIO_MATCH_THRESHOLD = 0.2f;
+
+ private static final int CAMERA_PERMISSION_REQUEST_CODE = 1;
+ private static final String CAMERA_PERMISSION_DIALOG_FRAMENT_TAG =
+ "CameraPermissionDialogFragment";
+ private static final long CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS = 2000L;
+ private static final long VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS = 2000L;
+
+ private final ViewOutlineProvider circleOutlineProvider =
+ new ViewOutlineProvider() {
+ @Override
+ public void getOutline(View view, Outline outline) {
+ int x = view.getWidth() / 2;
+ int y = view.getHeight() / 2;
+ int radius = Math.min(x, y);
+ outline.setOval(x - radius, y - radius, x + radius, y + radius);
+ }
+ };
+ private InCallScreenDelegate inCallScreenDelegate;
+ private VideoCallScreenDelegate videoCallScreenDelegate;
+ private InCallButtonUiDelegate inCallButtonUiDelegate;
+ private View endCallButton;
+ private CheckableImageButton speakerButton;
+ private SpeakerButtonController speakerButtonController;
+ private CheckableImageButton muteButton;
+ private CheckableImageButton cameraOffButton;
+ private ImageButton swapCameraButton;
+ private View switchOnHoldButton;
+ private View onHoldContainer;
+ private SwitchOnHoldCallController switchOnHoldCallController;
+ private TextView remoteVideoOff;
+ private ImageView remoteOffBlurredImageView;
+ private View mutePreviewOverlay;
+ private View previewOffOverlay;
+ private ImageView previewOffBlurredImageView;
+ private View controls;
+ private View controlsContainer;
+ private TextureView previewTextureView;
+ private TextureView remoteTextureView;
+ private View greenScreenBackgroundView;
+ private View fullscreenBackgroundView;
+ private boolean shouldShowRemote;
+ private boolean shouldShowPreview;
+ private boolean isInFullscreenMode;
+ private boolean isInGreenScreenMode;
+ private boolean hasInitializedScreenModes;
+ private boolean isRemotelyHeld;
+ private ContactGridManager contactGridManager;
+ private SecondaryInfo savedSecondaryInfo;
+ private final Runnable cameraPermissionDialogRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ if (videoCallScreenDelegate.shouldShowCameraPermissionDialog()) {
+ LogUtil.i("VideoCallFragment.cameraPermissionDialogRunnable", "showing dialog");
+ checkCameraPermission();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ LogUtil.i("VideoCallFragment.onCreate", null);
+
+ inCallButtonUiDelegate =
+ FragmentUtils.getParent(this, InCallButtonUiDelegateFactory.class)
+ .newInCallButtonUiDelegate();
+ if (savedInstanceState != null) {
+ inCallButtonUiDelegate.onRestoreInstanceState(savedInstanceState);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission granted.");
+ videoCallScreenDelegate.onCameraPermissionGranted();
+ } else {
+ LogUtil.i("VideoCallFragment.onRequestPermissionsResult", "Camera permission denied.");
+ }
+ }
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(
+ LayoutInflater layoutInflater, @Nullable ViewGroup viewGroup, @Nullable Bundle bundle) {
+ LogUtil.i("VideoCallFragment.onCreateView", null);
+
+ View view =
+ layoutInflater.inflate(
+ isLandscape() ? R.layout.frag_videocall_land : R.layout.frag_videocall,
+ viewGroup,
+ false);
+ contactGridManager =
+ new ContactGridManager(view, null /* no avatar */, 0, false /* showAnonymousAvatar */);
+
+ controls = view.findViewById(R.id.videocall_video_controls);
+ controls.setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ controlsContainer = view.findViewById(R.id.videocall_video_controls_container);
+ speakerButton = (CheckableImageButton) view.findViewById(R.id.videocall_speaker_button);
+ muteButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_button);
+ muteButton.setOnCheckedChangeListener(this);
+ mutePreviewOverlay = view.findViewById(R.id.videocall_video_preview_mute_overlay);
+ cameraOffButton = (CheckableImageButton) view.findViewById(R.id.videocall_mute_video);
+ cameraOffButton.setOnCheckedChangeListener(this);
+ previewOffOverlay = view.findViewById(R.id.videocall_video_preview_off_overlay);
+ previewOffBlurredImageView =
+ (ImageView) view.findViewById(R.id.videocall_preview_off_blurred_image_view);
+ swapCameraButton = (ImageButton) view.findViewById(R.id.videocall_switch_video);
+ swapCameraButton.setOnClickListener(this);
+ view.findViewById(R.id.videocall_switch_controls)
+ .setVisibility(
+ ActivityCompat.isInMultiWindowMode(getActivity()) ? View.GONE : View.VISIBLE);
+ switchOnHoldButton = view.findViewById(R.id.videocall_switch_on_hold);
+ onHoldContainer = view.findViewById(R.id.videocall_on_hold_banner);
+ remoteVideoOff = (TextView) view.findViewById(R.id.videocall_remote_video_off);
+ remoteVideoOff.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+ remoteOffBlurredImageView =
+ (ImageView) view.findViewById(R.id.videocall_remote_off_blurred_image_view);
+ endCallButton = view.findViewById(R.id.videocall_end_call);
+ endCallButton.setOnClickListener(this);
+ previewTextureView = (TextureView) view.findViewById(R.id.videocall_video_preview);
+ previewTextureView.setClipToOutline(true);
+ previewOffOverlay.setOnClickListener(
+ new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ checkCameraPermission();
+ }
+ });
+ remoteTextureView = (TextureView) view.findViewById(R.id.videocall_video_remote);
+ greenScreenBackgroundView = view.findViewById(R.id.videocall_green_screen_background);
+ fullscreenBackgroundView = view.findViewById(R.id.videocall_fullscreen_background);
+
+ // We need the texture view size to be able to scale the remote video. At this point the view
+ // layout won't be complete so add a layout listener.
+ ViewTreeObserver observer = remoteTextureView.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(
+ new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ LogUtil.i("VideoCallFragment.onGlobalLayout", null);
+ updateRemoteVideoScaling();
+ updatePreviewVideoScaling();
+ updateVideoOffViews();
+ // Remove the listener so we don't continually re-layout.
+ ViewTreeObserver observer = remoteTextureView.getViewTreeObserver();
+ if (observer.isAlive()) {
+ observer.removeOnGlobalLayoutListener(this);
+ }
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle bundle) {
+ super.onViewCreated(view, bundle);
+ LogUtil.i("VideoCallFragment.onViewCreated", null);
+
+ inCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, InCallScreenDelegateFactory.class)
+ .newInCallScreenDelegate();
+ videoCallScreenDelegate =
+ FragmentUtils.getParentUnsafe(this, VideoCallScreenDelegateFactory.class)
+ .newVideoCallScreenDelegate();
+
+ speakerButtonController =
+ new SpeakerButtonController(speakerButton, inCallButtonUiDelegate, videoCallScreenDelegate);
+ switchOnHoldCallController =
+ new SwitchOnHoldCallController(
+ switchOnHoldButton, onHoldContainer, inCallScreenDelegate, videoCallScreenDelegate);
+
+ videoCallScreenDelegate.initVideoCallScreenDelegate(getContext(), this);
+
+ inCallScreenDelegate.onInCallScreenDelegateInit(this);
+ inCallScreenDelegate.onInCallScreenReady();
+ inCallButtonUiDelegate.onInCallButtonUiReady(this);
+
+ view.setOnSystemUiVisibilityChangeListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ inCallButtonUiDelegate.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ LogUtil.i("VideoCallFragment.onDestroyView", null);
+ inCallButtonUiDelegate.onInCallButtonUiUnready();
+ inCallScreenDelegate.onInCallScreenUnready();
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (savedSecondaryInfo != null) {
+ setSecondary(savedSecondaryInfo);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ LogUtil.i("VideoCallFragment.onResume", null);
+ inCallScreenDelegate.onInCallScreenResumed();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ LogUtil.i("VideoCallFragment.onStart", null);
+ inCallButtonUiDelegate.refreshMuteState();
+ videoCallScreenDelegate.onVideoCallScreenUiReady();
+ getView().postDelayed(cameraPermissionDialogRunnable, CAMERA_PERMISSION_DIALOG_DELAY_IN_MILLIS);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ LogUtil.i("VideoCallFragment.onPause", null);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ LogUtil.i("VideoCallFragment.onStop", null);
+ getView().removeCallbacks(cameraPermissionDialogRunnable);
+ videoCallScreenDelegate.onVideoCallScreenUiUnready();
+ }
+
+ private void exitFullscreenMode() {
+ LogUtil.i("VideoCallFragment.exitFullscreenMode", null);
+
+ if (!getView().isAttachedToWindow()) {
+ LogUtil.i("VideoCallFragment.exitFullscreenMode", "not attached");
+ return;
+ }
+
+ showSystemUI();
+
+ LinearOutSlowInInterpolator linearOutSlowInInterpolator = new LinearOutSlowInInterpolator();
+
+ // Animate the controls to the shown state.
+ controls
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .start();
+
+ // Animate onHold to the shown state.
+ switchOnHoldButton
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ switchOnHoldCallController.setOnScreen();
+ }
+ });
+
+ View contactGridView = contactGridManager.getContainerView();
+ // Animate contact grid to the shown state.
+ contactGridView
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ contactGridManager.show();
+ }
+ });
+
+ endCallButton
+ .animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(linearOutSlowInInterpolator)
+ .alpha(1)
+ .withStartAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ endCallButton.setVisibility(View.VISIBLE);
+ }
+ })
+ .start();
+
+ // Animate all the preview controls up to make room for the navigation bar.
+ // In green screen mode we don't need this because the preview takes up the whole screen and has
+ // a fixed position.
+ if (!isInGreenScreenMode) {
+ Point previewOffsetStartShown = getPreviewOffsetStartShown();
+ for (View view : getAllPreviewRelatedViews()) {
+ // Animate up with the preview offset above the navigation bar.
+ view.animate()
+ .translationX(previewOffsetStartShown.x)
+ .translationY(previewOffsetStartShown.y)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .start();
+ }
+ }
+
+ updateOverlayBackground();
+ }
+
+ private void showSystemUI() {
+ View view = getView();
+ if (view != null) {
+ // Code is more expressive with all flags present, even though some may be combined
+ //noinspection PointlessBitwiseExpression
+ view.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+
+ /** Set view flags to hide the system UI. System UI will return on any touch event */
+ private void hideSystemUI() {
+ View view = getView();
+ if (view != null) {
+ view.setSystemUiVisibility(
+ View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }
+ }
+
+ private Point getControlsOffsetEndHidden(View controls) {
+ if (isLandscape()) {
+ return new Point(0, getOffsetBottom(controls));
+ } else {
+ return new Point(getOffsetStart(controls), 0);
+ }
+ }
+
+ private Point getSwitchOnHoldOffsetEndHidden(View swapCallButton) {
+ if (isLandscape()) {
+ return new Point(0, getOffsetTop(swapCallButton));
+ } else {
+ return new Point(getOffsetEnd(swapCallButton), 0);
+ }
+ }
+
+ private Point getContactGridOffsetEndHidden(View view) {
+ return new Point(0, getOffsetTop(view));
+ }
+
+ private Point getEndCallOffsetEndHidden(View endCallButton) {
+ if (isLandscape()) {
+ return new Point(getOffsetEnd(endCallButton), 0);
+ } else {
+ return new Point(0, ((MarginLayoutParams) endCallButton.getLayoutParams()).bottomMargin);
+ }
+ }
+
+ private Point getPreviewOffsetStartShown() {
+ // No insets in multiwindow mode, and rootWindowInsets will get the display's insets.
+ if (ActivityCompat.isInMultiWindowMode(getActivity())) {
+ return new Point();
+ }
+ if (isLandscape()) {
+ int stableInsetEnd =
+ getView().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+ ? getView().getRootWindowInsets().getStableInsetLeft()
+ : -getView().getRootWindowInsets().getStableInsetRight();
+ return new Point(stableInsetEnd, 0);
+ } else {
+ return new Point(0, -getView().getRootWindowInsets().getStableInsetBottom());
+ }
+ }
+
+ private View[] getAllPreviewRelatedViews() {
+ return new View[] {
+ previewTextureView, previewOffOverlay, previewOffBlurredImageView, mutePreviewOverlay,
+ };
+ }
+
+ private int getOffsetTop(View view) {
+ return -(view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).topMargin);
+ }
+
+ private int getOffsetBottom(View view) {
+ return view.getHeight() + ((MarginLayoutParams) view.getLayoutParams()).bottomMargin;
+ }
+
+ private int getOffsetStart(View view) {
+ int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginStart();
+ if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ offset = -offset;
+ }
+ return -offset;
+ }
+
+ private int getOffsetEnd(View view) {
+ int offset = view.getWidth() + ((MarginLayoutParams) view.getLayoutParams()).getMarginEnd();
+ if (view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+ offset = -offset;
+ }
+ return offset;
+ }
+
+ private void enterFullscreenMode() {
+ LogUtil.i("VideoCallFragment.enterFullscreenMode", null);
+
+ hideSystemUI();
+
+ Interpolator fastOutLinearInInterpolator = new FastOutLinearInInterpolator();
+
+ // Animate controls to the hidden state.
+ Point offset = getControlsOffsetEndHidden(controls);
+ controls
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0)
+ .start();
+
+ // Animate onHold to the hidden state.
+ offset = getSwitchOnHoldOffsetEndHidden(switchOnHoldButton);
+ switchOnHoldButton
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0);
+
+ View contactGridView = contactGridManager.getContainerView();
+ // Animate contact grid to the hidden state.
+ offset = getContactGridOffsetEndHidden(contactGridView);
+ contactGridView
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0);
+
+ offset = getEndCallOffsetEndHidden(endCallButton);
+ // Use a fast out interpolator to quickly fade out the button. This is important because the
+ // button can't draw under the navigation bar which means that it'll look weird if it just
+ // abruptly disappears when it reaches the edge of the naivgation bar.
+ endCallButton
+ .animate()
+ .translationX(offset.x)
+ .translationY(offset.y)
+ .setInterpolator(fastOutLinearInInterpolator)
+ .alpha(0)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ endCallButton.setVisibility(View.INVISIBLE);
+ }
+ })
+ .setInterpolator(new FastOutLinearInInterpolator())
+ .start();
+
+ // Animate all the preview controls down now that the navigation bar is hidden.
+ // In green screen mode we don't need this because the preview takes up the whole screen and has
+ // a fixed position.
+ if (!isInGreenScreenMode) {
+ for (View view : getAllPreviewRelatedViews()) {
+ // Animate down with the navigation bar hidden.
+ view.animate()
+ .translationX(0)
+ .translationY(0)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .start();
+ }
+ }
+ updateOverlayBackground();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == endCallButton) {
+ LogUtil.i("VideoCallFragment.onClick", "end call button clicked");
+ inCallButtonUiDelegate.onEndCallClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ } else if (v == swapCameraButton) {
+ if (swapCameraButton.getDrawable() instanceof Animatable) {
+ ((Animatable) swapCameraButton.getDrawable()).start();
+ }
+ inCallButtonUiDelegate.toggleCameraClicked();
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ }
+
+ @Override
+ public void onCheckedChanged(CheckableImageButton button, boolean isChecked) {
+ if (button == cameraOffButton) {
+ if (!isChecked && !VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ LogUtil.i("VideoCallFragment.onCheckedChanged", "show camera permission dialog");
+ checkCameraPermission();
+ } else {
+ inCallButtonUiDelegate.pauseVideoClicked(isChecked);
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ } else if (button == muteButton) {
+ inCallButtonUiDelegate.muteClicked(isChecked);
+ videoCallScreenDelegate.resetAutoFullscreenTimer();
+ }
+ }
+
+ @Override
+ public void showVideoViews(
+ boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld) {
+ LogUtil.i(
+ "VideoCallFragment.showVideoViews",
+ "showPreview: %b, shouldShowRemote: %b",
+ shouldShowPreview,
+ shouldShowRemote);
+ this.shouldShowPreview = shouldShowPreview;
+ this.shouldShowRemote = shouldShowRemote;
+ this.isRemotelyHeld = isRemotelyHeld;
+
+ videoCallScreenDelegate.getLocalVideoSurfaceTexture().attachToTextureView(previewTextureView);
+ videoCallScreenDelegate.getRemoteVideoSurfaceTexture().attachToTextureView(remoteTextureView);
+
+ updateVideoOffViews();
+ updateRemoteVideoScaling();
+ }
+
+ /**
+ * This method scales the video feed inside the texture view, it doesn't change the texture view's
+ * size. In the old UI we would change the view size to match the aspect ratio of the video. In
+ * the new UI the view is always square (with the circular clip) so we have to do additional work
+ * to make sure the non-square video doesn't look squished.
+ */
+ @Override
+ public void onLocalVideoDimensionsChanged() {
+ LogUtil.i("VideoCallFragment.onLocalVideoDimensionsChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ @Override
+ public void onLocalVideoOrientationChanged() {
+ LogUtil.i("VideoCallFragment.onLocalVideoOrientationChanged", null);
+ updatePreviewVideoScaling();
+ }
+
+ /** Called when the remote video's dimensions change. */
+ @Override
+ public void onRemoteVideoDimensionsChanged() {
+ LogUtil.i("VideoCallFragment.onRemoteVideoDimensionsChanged", null);
+ updateRemoteVideoScaling();
+ }
+
+ @Override
+ public void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen) {
+ LogUtil.i(
+ "VideoCallFragment.updateFullscreenAndGreenScreenMode",
+ "shouldShowFullscreen: %b, shouldShowGreenScreen: %b",
+ shouldShowFullscreen,
+ shouldShowGreenScreen);
+
+ if (getActivity() == null) {
+ LogUtil.i("VideoCallFragment.updateFullscreenAndGreenScreenMode", "not attached to activity");
+ return;
+ }
+
+ // Check if anything is actually going to change. The first time this function is called we
+ // force a change by checking the hasInitializedScreenModes flag. We also force both fullscreen
+ // and green screen modes to update even if only one has changed. That's because they both
+ // depend on each other.
+ if (hasInitializedScreenModes
+ && shouldShowGreenScreen == isInGreenScreenMode
+ && shouldShowFullscreen == isInFullscreenMode) {
+ LogUtil.i(
+ "VideoCallFragment.updateFullscreenAndGreenScreenMode", "no change to screen modes");
+ return;
+ }
+ hasInitializedScreenModes = true;
+ isInGreenScreenMode = shouldShowGreenScreen;
+ isInFullscreenMode = shouldShowFullscreen;
+
+ if (getView().isAttachedToWindow() && !ActivityCompat.isInMultiWindowMode(getActivity())) {
+ controlsContainer.onApplyWindowInsets(getView().getRootWindowInsets());
+ }
+ if (shouldShowGreenScreen) {
+ enterGreenScreenMode();
+ } else {
+ exitGreenScreenMode();
+ }
+ if (shouldShowFullscreen) {
+ enterFullscreenMode();
+ } else {
+ exitFullscreenMode();
+ }
+ updateVideoOffViews();
+
+ OnHoldFragment onHoldFragment =
+ ((OnHoldFragment)
+ getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner));
+ if (onHoldFragment != null) {
+ onHoldFragment.setPadTopInset(!isInFullscreenMode);
+ }
+ }
+
+ @Override
+ public Fragment getVideoCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public void showButton(@InCallButtonIds int buttonId, boolean show) {
+ LogUtil.v(
+ "VideoCallFragment.showButton",
+ "buttonId: %s, show: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ show);
+ if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
+ speakerButtonController.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
+ muteButton.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ cameraOffButton.setEnabled(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ switchOnHoldCallController.setVisible(show);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_CAMERA) {
+ swapCameraButton.setEnabled(show);
+ }
+ }
+
+ @Override
+ public void enableButton(@InCallButtonIds int buttonId, boolean enable) {
+ LogUtil.v(
+ "VideoCallFragment.setEnabled",
+ "buttonId: %s, enable: %b",
+ InCallButtonIdsExtension.toString(buttonId),
+ enable);
+ if (buttonId == InCallButtonIds.BUTTON_AUDIO) {
+ speakerButtonController.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_MUTE) {
+ muteButton.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_PAUSE_VIDEO) {
+ cameraOffButton.setEnabled(enable);
+ } else if (buttonId == InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY) {
+ switchOnHoldCallController.setEnabled(enable);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ LogUtil.v("VideoCallFragment.setEnabled", "enabled: " + enabled);
+ speakerButtonController.setEnabled(enabled);
+ muteButton.setEnabled(enabled);
+ cameraOffButton.setEnabled(enabled);
+ switchOnHoldCallController.setEnabled(enabled);
+ }
+
+ @Override
+ public void setHold(boolean value) {
+ LogUtil.i("VideoCallFragment.setHold", "value: " + value);
+ }
+
+ @Override
+ public void setCameraSwitched(boolean isBackFacingCamera) {
+ LogUtil.i("VideoCallFragment.setCameraSwitched", "isBackFacingCamera: " + isBackFacingCamera);
+ }
+
+ @Override
+ public void setVideoPaused(boolean isPaused) {
+ LogUtil.i("VideoCallFragment.setVideoPaused", "isPaused: " + isPaused);
+ cameraOffButton.setChecked(isPaused);
+ }
+
+ @Override
+ public void setAudioState(CallAudioState audioState) {
+ LogUtil.i("VideoCallFragment.setAudioState", "audioState: " + audioState);
+ speakerButtonController.setAudioState(audioState);
+ muteButton.setChecked(audioState.isMuted());
+ updateMutePreviewOverlayVisibility();
+ }
+
+ @Override
+ public void updateButtonStates() {
+ LogUtil.i("VideoCallFragment.updateButtonState", null);
+ speakerButtonController.updateButtonState();
+ switchOnHoldCallController.updateButtonState();
+ }
+
+ @Override
+ public void updateInCallButtonUiColors() {}
+
+ @Override
+ public Fragment getInCallButtonUiFragment() {
+ return this;
+ }
+
+ @Override
+ public void showAudioRouteSelector() {
+ LogUtil.i("VideoCallFragment.showAudioRouteSelector", null);
+ AudioRouteSelectorDialogFragment.newInstance(inCallButtonUiDelegate.getCurrentAudioState())
+ .show(getChildFragmentManager(), null);
+ }
+
+ @Override
+ public void onAudioRouteSelected(int audioRoute) {
+ LogUtil.i("VideoCallFragment.onAudioRouteSelected", "audioRoute: " + audioRoute);
+ inCallButtonUiDelegate.setAudioRoute(audioRoute);
+ }
+
+ @Override
+ public void setPrimary(@NonNull PrimaryInfo primaryInfo) {
+ LogUtil.i("VideoCallFragment.setPrimary", primaryInfo.toString());
+ contactGridManager.setPrimary(primaryInfo);
+ }
+
+ @Override
+ public void setSecondary(@NonNull SecondaryInfo secondaryInfo) {
+ LogUtil.i("VideoCallFragment.setSecondary", secondaryInfo.toString());
+ if (!isAdded()) {
+ savedSecondaryInfo = secondaryInfo;
+ return;
+ }
+ savedSecondaryInfo = null;
+ switchOnHoldCallController.setSecondaryInfo(secondaryInfo);
+ updateButtonStates();
+ FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
+ Fragment oldBanner = getChildFragmentManager().findFragmentById(R.id.videocall_on_hold_banner);
+ if (secondaryInfo.shouldShow) {
+ OnHoldFragment onHoldFragment = OnHoldFragment.newInstance(secondaryInfo);
+ onHoldFragment.setPadTopInset(!isInFullscreenMode);
+ transaction.replace(R.id.videocall_on_hold_banner, onHoldFragment);
+ } else {
+ if (oldBanner != null) {
+ transaction.remove(oldBanner);
+ }
+ }
+ transaction.setCustomAnimations(R.anim.abc_slide_in_top, R.anim.abc_slide_out_top);
+ transaction.commitAllowingStateLoss();
+ }
+
+ @Override
+ public void setCallState(@NonNull PrimaryCallState primaryCallState) {
+ LogUtil.i("VideoCallFragment.setCallState", primaryCallState.toString());
+ contactGridManager.setCallState(primaryCallState);
+ }
+
+ @Override
+ public void setEndCallButtonEnabled(boolean enabled, boolean animate) {
+ LogUtil.i("VideoCallFragment.setEndCallButtonEnabled", "enabled: " + enabled);
+ }
+
+ @Override
+ public void showManageConferenceCallButton(boolean visible) {
+ LogUtil.i("VideoCallFragment.showManageConferenceCallButton", "visible: " + visible);
+ }
+
+ @Override
+ public boolean isManageConferenceVisible() {
+ LogUtil.i("VideoCallFragment.isManageConferenceVisible", null);
+ return false;
+ }
+
+ @Override
+ public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ contactGridManager.dispatchPopulateAccessibilityEvent(event);
+ }
+
+ @Override
+ public void showNoteSentToast() {
+ LogUtil.i("VideoCallFragment.showNoteSentToast", null);
+ }
+
+ @Override
+ public void updateInCallScreenColors() {
+ LogUtil.i("VideoCallFragment.updateColors", null);
+ }
+
+ @Override
+ public void onInCallScreenDialpadVisibilityChange(boolean isShowing) {
+ LogUtil.i("VideoCallFragment.onInCallScreenDialpadVisibilityChange", null);
+ }
+
+ @Override
+ public int getAnswerAndDialpadContainerResourceId() {
+ return 0;
+ }
+
+ @Override
+ public Fragment getInCallScreenFragment() {
+ return this;
+ }
+
+ @Override
+ public boolean isShowingLocationUi() {
+ return false;
+ }
+
+ @Override
+ public void showLocationUi(Fragment locationUi) {
+ LogUtil.e("VideoCallFragment.showLocationUi", "Emergency video calling not supported");
+ // Do nothing
+ }
+
+ private void updatePreviewVideoScaling() {
+ if (previewTextureView.getWidth() == 0 || previewTextureView.getHeight() == 0) {
+ LogUtil.i("VideoCallFragment.updatePreviewVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+ VideoSurfaceTexture localVideoSurfaceTexture =
+ videoCallScreenDelegate.getLocalVideoSurfaceTexture();
+ Point cameraDimensions = localVideoSurfaceTexture.getSurfaceDimensions();
+ if (cameraDimensions == null) {
+ LogUtil.i(
+ "VideoCallFragment.updatePreviewVideoScaling", "camera dimensions haven't been set");
+ return;
+ }
+ if (isLandscape()) {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ previewTextureView,
+ cameraDimensions.x,
+ cameraDimensions.y,
+ videoCallScreenDelegate.getDeviceOrientation());
+ } else {
+ VideoSurfaceBindings.scaleVideoAndFillView(
+ previewTextureView,
+ cameraDimensions.y,
+ cameraDimensions.x,
+ videoCallScreenDelegate.getDeviceOrientation());
+ }
+ }
+
+ private void updateRemoteVideoScaling() {
+ VideoSurfaceTexture remoteVideoSurfaceTexture =
+ videoCallScreenDelegate.getRemoteVideoSurfaceTexture();
+ Point videoSize = remoteVideoSurfaceTexture.getSourceVideoDimensions();
+ if (videoSize == null) {
+ LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "video size is null");
+ return;
+ }
+ if (remoteTextureView.getWidth() == 0 || remoteTextureView.getHeight() == 0) {
+ LogUtil.i("VideoCallFragment.updateRemoteVideoScaling", "view layout hasn't finished yet");
+ return;
+ }
+
+ // If the video and display aspect ratio's are close then scale video to fill display
+ float videoAspectRatio = ((float) videoSize.x) / videoSize.y;
+ float displayAspectRatio =
+ ((float) remoteTextureView.getWidth()) / remoteTextureView.getHeight();
+ float delta = Math.abs(videoAspectRatio - displayAspectRatio);
+ float sum = videoAspectRatio + displayAspectRatio;
+ if (delta / sum < ASPECT_RATIO_MATCH_THRESHOLD) {
+ VideoSurfaceBindings.scaleVideoAndFillView(remoteTextureView, videoSize.x, videoSize.y, 0);
+ } else {
+ VideoSurfaceBindings.scaleVideoMaintainingAspectRatio(
+ remoteTextureView, videoSize.x, videoSize.y);
+ }
+ }
+
+ private boolean isLandscape() {
+ // Choose orientation based on display orientation, not window orientation
+ int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
+ return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
+ }
+
+ private void enterGreenScreenMode() {
+ LogUtil.i("VideoCallFragment.enterGreenScreenMode", null);
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT);
+ params.addRule(RelativeLayout.ALIGN_PARENT_START);
+ params.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ previewTextureView.setLayoutParams(params);
+ previewTextureView.setOutlineProvider(null);
+ updatePreviewVideoScaling();
+ updateOverlayBackground();
+ contactGridManager.setIsMiddleRowVisible(true);
+ updateMutePreviewOverlayVisibility();
+
+ previewOffBlurredImageView.setLayoutParams(params);
+ previewOffBlurredImageView.setOutlineProvider(null);
+ previewOffBlurredImageView.setClipToOutline(false);
+ }
+
+ private void exitGreenScreenMode() {
+ LogUtil.i("VideoCallFragment.exitGreenScreenMode", null);
+ Resources resources = getResources();
+ RelativeLayout.LayoutParams params =
+ new RelativeLayout.LayoutParams(
+ (int) resources.getDimension(R.dimen.videocall_preview_width),
+ (int) resources.getDimension(R.dimen.videocall_preview_height));
+ params.setMargins(
+ 0, 0, 0, (int) resources.getDimension(R.dimen.videocall_preview_margin_bottom));
+ if (isLandscape()) {
+ params.addRule(RelativeLayout.ALIGN_PARENT_END);
+ params.setMarginEnd((int) resources.getDimension(R.dimen.videocall_preview_margin_end));
+ } else {
+ params.addRule(RelativeLayout.ALIGN_PARENT_START);
+ params.setMarginStart((int) resources.getDimension(R.dimen.videocall_preview_margin_start));
+ }
+ params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
+ previewTextureView.setLayoutParams(params);
+ previewTextureView.setOutlineProvider(circleOutlineProvider);
+ updatePreviewVideoScaling();
+ updateOverlayBackground();
+ contactGridManager.setIsMiddleRowVisible(false);
+ updateMutePreviewOverlayVisibility();
+
+ previewOffBlurredImageView.setLayoutParams(params);
+ previewOffBlurredImageView.setOutlineProvider(circleOutlineProvider);
+ previewOffBlurredImageView.setClipToOutline(true);
+ }
+
+ private void updateVideoOffViews() {
+ // Always hide the preview off and remote off views in green screen mode.
+ boolean previewEnabled = isInGreenScreenMode || shouldShowPreview;
+ previewOffOverlay.setVisibility(previewEnabled ? View.GONE : View.VISIBLE);
+ updateBlurredImageView(
+ previewTextureView,
+ previewOffBlurredImageView,
+ shouldShowPreview,
+ BLUR_PREVIEW_RADIUS,
+ BLUR_PREVIEW_SCALE_FACTOR);
+
+ boolean remoteEnabled = isInGreenScreenMode || shouldShowRemote;
+ boolean isResumed = remoteEnabled && !isRemotelyHeld;
+ if (isResumed) {
+ boolean wasRemoteVideoOff =
+ TextUtils.equals(
+ remoteVideoOff.getText(),
+ remoteVideoOff.getResources().getString(R.string.videocall_remote_video_off));
+ // The text needs to be updated and hidden after enough delay in order to be announced by
+ // talkback.
+ remoteVideoOff.setText(
+ wasRemoteVideoOff
+ ? R.string.videocall_remote_video_on
+ : R.string.videocall_remotely_resumed);
+ remoteVideoOff.postDelayed(
+ new Runnable() {
+ @Override
+ public void run() {
+ remoteVideoOff.setVisibility(View.GONE);
+ }
+ },
+ VIDEO_OFF_VIEW_FADE_OUT_DELAY_IN_MILLIS);
+ } else {
+ remoteVideoOff.setText(
+ isRemotelyHeld ? R.string.videocall_remotely_held : R.string.videocall_remote_video_off);
+ remoteVideoOff.setVisibility(View.VISIBLE);
+ }
+ LogUtil.i("VideoCallFragment.updateVideoOffViews", "calling updateBlurredImageView");
+ updateBlurredImageView(
+ remoteTextureView,
+ remoteOffBlurredImageView,
+ shouldShowRemote,
+ BLUR_REMOTE_RADIUS,
+ BLUR_REMOTE_SCALE_FACTOR);
+ }
+
+ private void updateBlurredImageView(
+ TextureView textureView,
+ ImageView blurredImageView,
+ boolean isVideoEnabled,
+ float blurRadius,
+ float scaleFactor) {
+ boolean didBlur = false;
+ long startTimeMillis = SystemClock.elapsedRealtime();
+ if (!isVideoEnabled) {
+ int width = Math.round(textureView.getWidth() * scaleFactor);
+ int height = Math.round(textureView.getHeight() * scaleFactor);
+ // This call takes less than 10 milliseconds.
+ Bitmap bitmap = textureView.getBitmap(width, height);
+ if (bitmap != null) {
+ // TODO: When the view is first displayed after a rotation the bitmap is empty
+ // and thus this blur has no effect.
+ // This call can take 100 milliseconds.
+ blur(getContext(), bitmap, blurRadius);
+
+ // TODO: Figure out why only have to apply the transform in landscape mode
+ if (width > height) {
+ bitmap =
+ Bitmap.createBitmap(
+ bitmap,
+ 0,
+ 0,
+ bitmap.getWidth(),
+ bitmap.getHeight(),
+ textureView.getTransform(null),
+ true);
+ }
+
+ blurredImageView.setImageBitmap(bitmap);
+ blurredImageView.setVisibility(View.VISIBLE);
+ didBlur = true;
+ }
+ }
+ if (!didBlur) {
+ blurredImageView.setImageBitmap(null);
+ blurredImageView.setVisibility(View.GONE);
+ }
+
+ LogUtil.i(
+ "VideoCallFragment.updateBlurredImageView",
+ "didBlur: %b, took %d millis",
+ didBlur,
+ (SystemClock.elapsedRealtime() - startTimeMillis));
+ }
+
+ private void updateOverlayBackground() {
+ if (isInGreenScreenMode) {
+ // We want to darken the preview view to make text and buttons readable. The fullscreen
+ // background is below the preview view so use the green screen background instead.
+ animateSetVisibility(greenScreenBackgroundView, View.VISIBLE);
+ animateSetVisibility(fullscreenBackgroundView, View.GONE);
+ } else if (!isInFullscreenMode) {
+ // We want to darken the remote view to make text and buttons readable. The green screen
+ // background is above the preview view so it would darken the preview too. Use the fullscreen
+ // background instead.
+ animateSetVisibility(greenScreenBackgroundView, View.GONE);
+ animateSetVisibility(fullscreenBackgroundView, View.VISIBLE);
+ } else {
+ animateSetVisibility(greenScreenBackgroundView, View.GONE);
+ animateSetVisibility(fullscreenBackgroundView, View.GONE);
+ }
+ }
+
+ private void updateMutePreviewOverlayVisibility() {
+ // Normally the mute overlay shows on the bottom right of the preview bubble. In green screen
+ // mode the preview is fullscreen so there's no where to anchor it.
+ mutePreviewOverlay.setVisibility(
+ muteButton.isChecked() && !isInGreenScreenMode ? View.VISIBLE : View.GONE);
+ }
+
+ private static void animateSetVisibility(final View view, final int visibility) {
+ if (view.getVisibility() == visibility) {
+ return;
+ }
+
+ int startAlpha;
+ int endAlpha;
+ if (visibility == View.GONE) {
+ startAlpha = 1;
+ endAlpha = 0;
+ } else if (visibility == View.VISIBLE) {
+ startAlpha = 0;
+ endAlpha = 1;
+ } else {
+ Assert.fail();
+ return;
+ }
+
+ view.setAlpha(startAlpha);
+ view.setVisibility(View.VISIBLE);
+ view.animate()
+ .alpha(endAlpha)
+ .withEndAction(
+ new Runnable() {
+ @Override
+ public void run() {
+ view.setVisibility(visibility);
+ }
+ })
+ .start();
+ }
+
+ private static void blur(Context context, Bitmap image, float blurRadius) {
+ RenderScript renderScript = RenderScript.create(context);
+ ScriptIntrinsicBlur blurScript =
+ ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript));
+ Allocation allocationIn = Allocation.createFromBitmap(renderScript, image);
+ Allocation allocationOut = Allocation.createFromBitmap(renderScript, image);
+ blurScript.setRadius(blurRadius);
+ blurScript.setInput(allocationIn);
+ blurScript.forEach(allocationOut);
+ allocationOut.copyTo(image);
+ }
+
+ @Override
+ public void onSystemUiVisibilityChange(int visibility) {
+ boolean navBarVisible = (visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0;
+ videoCallScreenDelegate.onSystemUiVisibilityChange(navBarVisible);
+ }
+
+ protected void onCameraPermissionGranted() {
+ videoCallScreenDelegate.onCameraPermissionGranted();
+ }
+
+ private void checkCameraPermission() {
+ // Checks if user has consent of camera permission and the permission is granted.
+ // If camera permission is revoked, shows system permission dialog.
+ // If camera permission is granted but user doesn't have consent of camera permission
+ // (which means it's first time making video call), shows custom dialog instead. This
+ // will only be shown to user once.
+ if (!VideoUtils.hasCameraPermissionAndAllowedByUser(getContext())) {
+ videoCallScreenDelegate.onCameraPermissionDialogShown();
+ if (!VideoUtils.hasCameraPermission(getContext())) {
+ requestPermissions(new String[] {permission.CAMERA}, CAMERA_PERMISSION_REQUEST_CODE);
+ } else {
+ CameraPermissionDialogFragment.newInstance()
+ .show(getChildFragmentManager(), CAMERA_PERMISSION_DIALOG_FRAMENT_TAG);
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml b/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml
new file mode 100644
index 000000000..b46607b1b
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/color/videocall_button_icon_tint.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:color="#ff000000" android:state_checked="true"/>
+ <item android:color="#ffffffff"/>
+</selector>
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png
new file mode 100644
index 000000000..b5c6f0a87
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..2ab2f21a7
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..2deaadd76
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..c4147fa62
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png
new file mode 100644
index 000000000..c59e21504
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..95d6824f5
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..9a525a374
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-hdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png
new file mode 100644
index 000000000..f3427a02e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..c3ff7b2bb
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..c75281332
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..fd16baef7
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png
new file mode 100644
index 000000000..3fe2446e3
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..1ff3e7c25
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..aa7289af1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-mdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..491547189
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..799a78ebb
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..4d5e03320
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..62cd1a477
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png
new file mode 100644
index 000000000..c68ad909a
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..e5c3fc48d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..583c3de82
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xhdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..19a9344e9
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png
new file mode 100644
index 000000000..5a7702bbc
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png
new file mode 100644
index 000000000..a0be8d17d
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png
new file mode 100644
index 000000000..5671bfa06
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_checked_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png
new file mode 100644
index 000000000..527b3c47e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_default.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png
new file mode 100644
index 000000000..996185890
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_disabled.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png
new file mode 100644
index 000000000..56295b10f
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxhdpi/video_button_bg_pressed.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png b/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png
new file mode 100644
index 000000000..529c0a4d5
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable-xxxhdpi/ic_switch_camera.png
Binary files differ
diff --git a/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml b/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml
new file mode 100644
index 000000000..ee514c776
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable/videocall_background_circle_white.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="#80888888">
+ <item>
+ <shape
+ android:shape="oval">
+ <solid android:color="@color/incall_button_white"/>
+ </shape>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml b/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml
new file mode 100644
index 000000000..5e4841327
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/drawable/videocall_video_button_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/incall_button_ripple">
+ <item android:id="@android:id/mask">
+ <inset android:inset="5dp">
+ <shape android:shape="oval">
+ <solid android:color="@android:color/white"/>
+ </shape>
+ </inset>
+ </item>
+ <item>
+ <selector>
+ <item
+ android:drawable="@drawable/video_button_bg_checked_pressed"
+ android:state_checked="true"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_checked"
+ android:state_checked="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_pressed"
+ android:state_pressed="true"/>
+ <item
+ android:drawable="@drawable/video_button_bg_default"/>
+ </selector>
+ </item>
+</ripple>
diff --git a/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml b/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml
new file mode 100644
index 000000000..1fb1bb088
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout-v21/switch_camera_button.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/videocall_switch_video"
+ style="@style/Incall.Button.VideoCall"
+ android:contentDescription="@string/incall_content_description_swap_video"
+ android:src="@drawable/front_back_switch_button_animation"/>
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
new file mode 100644
index 000000000..dc663dda1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall.xml
@@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black"
+ android:orientation="vertical">
+
+ <TextureView
+ android:id="@+id/videocall_video_remote"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_remote_off_blurred_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitCenter"/>
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/videocall_remote_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:padding="64dp"
+ android:text="@string/videocall_remote_video_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <View
+ android:id="@+id/videocall_fullscreen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <TextureView
+ android:id="@+id/videocall_video_preview"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_preview_off_blurred_image_view"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:scaleType="center"/>
+
+ <View
+ android:id="@+id/videocall_green_screen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_off_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignLeft="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:layout_alignTop="@+id/videocall_video_preview"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ android:visibility="gone"
+ android:importantForAccessibility="no"
+ tools:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_mute_overlay"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:background="@drawable/videocall_background_circle_white"
+ android:contentDescription="@string/incall_content_description_muted"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_black_24"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <include
+ layout="@layout/videocall_controls"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <FrameLayout
+ android:id="@+id/videocall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml b/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml
new file mode 100644
index 000000000..2353deea1
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/frag_videocall_land.xml
@@ -0,0 +1,111 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@android:color/black">
+
+ <TextureView
+ android:id="@+id/videocall_video_remote"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_remote_off_blurred_image_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:scaleType="fitCenter"/>
+
+ <TextView
+ android:gravity="center"
+ android:id="@+id/videocall_remote_video_off"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_centerInParent="true"
+ android:accessibilityTraversalBefore="@+id/videocall_speaker_button"
+ android:drawablePadding="8dp"
+ android:drawableTop="@drawable/quantum_ic_videocam_off_white_36"
+ android:padding="64dp"
+ android:text="@string/videocall_remote_video_off"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <View
+ android:id="@+id/videocall_fullscreen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <TextureView
+ android:id="@+id/videocall_video_preview"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/videocall_preview_off_blurred_image_view"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:scaleType="center"/>
+
+ <View
+ android:id="@+id/videocall_green_screen_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:background="@color/videocall_overlay_background_color"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_off_overlay"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignLeft="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:layout_alignTop="@+id/videocall_video_preview"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ android:visibility="gone"
+ android:importantForAccessibility="no"
+ tools:visibility="visible"/>
+
+ <ImageView
+ android:id="@+id/videocall_video_preview_mute_overlay"
+ android:layout_width="32dp"
+ android:layout_height="32dp"
+ android:layout_alignBottom="@+id/videocall_video_preview"
+ android:layout_alignRight="@+id/videocall_video_preview"
+ android:background="@drawable/videocall_background_circle_white"
+ android:contentDescription="@string/incall_content_description_muted"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_black_24"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <include
+ layout="@layout/videocall_controls_land"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
+
+ <FrameLayout
+ android:id="@+id/videocall_on_hold_banner"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml b/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml
new file mode 100644
index 000000000..87c2e1b6c
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/switch_camera_button.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ImageButton xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/videocall_switch_video"
+ style="@style/Incall.Button.VideoCall"
+ android:contentDescription="@string/incall_content_description_swap_video"
+ android:src="@drawable/ic_switch_camera"/>
diff --git a/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
new file mode 100644
index 000000000..ad984f36e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/video_contact_grid.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:paddingTop="16dp"
+ android:orientation="vertical">
+
+ <include
+ layout="@layout/incall_contactgrid_top_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ <!-- We have to keep deprecated singleLine to allow long text being truncated with ellipses.
+ b/31396406 -->
+ <com.android.incallui.autoresizetext.AutoResizeTextView
+ android:id="@id/contactgrid_contact_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:singleLine="true"
+ android:textAppearance="@style/Dialer.Incall.TextAppearance.Large"
+ app:autoResizeText_minTextSize="28sp"
+ tools:text="Jake Peralta"
+ tools:ignore="Deprecated"/>
+
+ <include
+ layout="@layout/incall_contactgrid_bottom_row"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+</LinearLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml b/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml
new file mode 100644
index 000000000..b3141bdf3
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/videocall_controls.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/videocall_video_controls_container"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/incall_contact_grid"
+ layout="@layout/video_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- This placeholder matches the position of the preview UI and is used to
+ anchor video buttons. This is needed in greenscreen mode when the
+ preview is fullscreen but we want the controls to be positioned as
+ normal. -->
+ <Space
+ android:id="@+id/videocall_video_preview_placeholder"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginBottom="@dimen/videocall_preview_margin_bottom"
+ android:layout_marginStart="@dimen/videocall_preview_margin_start"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentStart="true"
+ android:visibility="invisible"/>
+
+ <LinearLayout
+ android:id="@+id/videocall_video_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_above="@+id/videocall_video_preview_placeholder"
+ android:layout_alignEnd="@+id/videocall_video_preview_placeholder"
+ android:layout_alignStart="@+id/videocall_video_preview_placeholder"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:visibility="invisible"
+ tools:visibility="visible">
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_speaker_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:checked="true"
+ android:src="@drawable/quantum_ic_volume_up_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_speaker"
+ app:contentDescriptionUnchecked="@string/incall_content_description_earpiece"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:src="@drawable/quantum_ic_mic_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_muted"
+ app:contentDescriptionUnchecked="@string/incall_content_description_unmuted"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_video"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_video_off"
+ app:contentDescriptionUnchecked="@string/incall_content_description_video_on"
+ />
+ <include
+ layout="@layout/switch_camera_button"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginBottom="@dimen/videocall_button_spacing"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/videocall_switch_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="36dp"
+ android:layout_marginEnd="24dp"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true">
+ <ImageButton
+ android:id="@+id/videocall_switch_on_hold"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:contentDescription="@string/incall_content_description_swap_calls"
+ android:src="@drawable/quantum_ic_swap_calls_white_36"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/videocall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginBottom="36dp"
+ android:layout_alignParentBottom="true"
+ android:layout_centerHorizontal="true"
+ android:contentDescription="@string/incall_content_description_end_call"
+ android:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml b/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml
new file mode 100644
index 000000000..d71b3c00e
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/layout/videocall_controls_land.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/videocall_video_controls_container"
+ android:fitsSystemWindows="true"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <include
+ android:id="@+id/incall_contact_grid"
+ layout="@layout/video_contact_grid"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginStart="24dp"
+ android:layout_marginEnd="24dp"/>
+
+ <!-- This placeholder matches the position of the preview UI and is used to
+ anchor video buttons. This is needed in greenscreen mode when the
+ preview is fullscreen but we want the controls to be positioned as
+ normal. -->
+ <Space
+ android:id="@+id/videocall_video_preview_placeholder"
+ android:layout_width="@dimen/videocall_preview_width"
+ android:layout_height="@dimen/videocall_preview_height"
+ android:layout_marginEnd="@dimen/videocall_preview_margin_end"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:visibility="invisible"/>
+
+ <LinearLayout
+ android:id="@+id/videocall_video_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignBottom="@+id/videocall_video_preview_placeholder"
+ android:layout_alignTop="@+id/videocall_video_preview_placeholder"
+ android:layout_toStartOf="@+id/videocall_video_preview_placeholder"
+ android:gravity="center_horizontal"
+ android:orientation="horizontal"
+ android:visibility="invisible"
+ tools:visibility="visible">
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_speaker_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:checked="true"
+ android:src="@drawable/quantum_ic_volume_up_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_speaker"
+ app:contentDescriptionUnchecked="@string/incall_content_description_earpiece"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_button"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_mic_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_muted"
+ app:contentDescriptionUnchecked="@string/incall_content_description_unmuted"
+ />
+ <com.android.incallui.video.impl.CheckableImageButton
+ android:id="@+id/videocall_mute_video"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"
+ android:scaleType="center"
+ android:src="@drawable/quantum_ic_videocam_off_white_36"
+ app:contentDescriptionChecked="@string/incall_content_description_video_off"
+ app:contentDescriptionUnchecked="@string/incall_content_description_video_on"
+ />
+ <include
+ layout="@layout/switch_camera_button"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:layout_marginEnd="24dp"/>
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/videocall_switch_controls"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="36dp"
+ android:layout_marginEnd="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true">
+ <ImageButton
+ android:id="@+id/videocall_switch_on_hold"
+ style="@style/Incall.Button.VideoCall"
+ android:layout_width="@dimen/videocall_button_size"
+ android:layout_height="@dimen/videocall_button_size"
+ android:contentDescription="@string/incall_content_description_swap_calls"
+ android:src="@drawable/quantum_ic_swap_calls_white_36"
+ android:visibility="gone"
+ tools:visibility="visible"
+ />
+ </FrameLayout>
+
+ <ImageButton
+ android:id="@+id/videocall_end_call"
+ style="@style/Incall.Button.End"
+ android:layout_marginEnd="36dp"
+ android:layout_alignParentEnd="true"
+ android:layout_centerVertical="true"
+ android:contentDescription="@string/incall_content_description_end_call"
+ android:visibility="visible"
+ tools:visibility="visible"/>
+
+</RelativeLayout>
diff --git a/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml b/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml
new file mode 100644
index 000000000..b1a86a0fa
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values-h580dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_button_spacing">16dp</dimen>
+ <dimen name="videocall_button_size">72dp</dimen>
+ <dimen name="videocall_preview_width">88dp</dimen>
+ <dimen name="videocall_preview_height">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml b/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml
new file mode 100644
index 000000000..b1a86a0fa
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values-w460dp/dimens.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_button_spacing">16dp</dimen>
+ <dimen name="videocall_button_size">72dp</dimen>
+ <dimen name="videocall_preview_width">88dp</dimen>
+ <dimen name="videocall_preview_height">88dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/attrs.xml b/java/com/android/incallui/video/impl/res/values/attrs.xml
new file mode 100644
index 000000000..e4cd8af89
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <declare-styleable name="CheckableImageButton">
+ <attr name="android:checked"/>
+ <attr name="contentDescriptionChecked" format="reference|string"/>
+ <attr name="contentDescriptionUnchecked" format="reference|string"/>
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/dimens.xml b/java/com/android/incallui/video/impl/res/values/dimens.xml
new file mode 100644
index 000000000..45860036f
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/dimens.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <dimen name="videocall_preview_width">72dp</dimen>
+ <dimen name="videocall_preview_height">72dp</dimen>
+ <dimen name="videocall_preview_margin_bottom">24dp</dimen>
+ <dimen name="videocall_preview_margin_start">24dp</dimen>
+ <dimen name="videocall_preview_margin_end">24dp</dimen>
+ <dimen name="videocall_button_spacing">8dp</dimen>
+ <dimen name="videocall_button_size">60dp</dimen>
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/strings.xml b/java/com/android/incallui/video/impl/res/values/strings.xml
new file mode 100644
index 000000000..2b72b8004
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/strings.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!-- Text indicates the video from remote party is off. [CHAR LIMIT=40] -->
+ <string name="videocall_remote_video_off">Their video is off</string>
+
+ <!-- Text indicates the video from remote party is on. [CHAR LIMIT=40] -->
+ <string name="videocall_remote_video_on">Their video is on</string>
+
+ <!-- Text indicates the call is held by remote party. [CHAR LIMIT=20] -->
+ <string name="videocall_remotely_held">Call on hold</string>
+
+ <!-- Text indicates the call is resumed from held by remote party. [CHAR LIMIT=20] -->
+ <string name="videocall_remotely_resumed">Call resumed</string>
+
+ <!-- Title of dialog to ask user for camera permission. [CHAR LIMIT=30] -->
+ <string name="camera_permission_dialog_title">Allow video?</string>
+
+ <!-- Message of dialog to ask user for camera permission. [CHAR LIMIT=100] -->
+ <string name="camera_permission_dialog_message">The Phone app wants to use your camera for video calls.</string>
+
+ <!-- Text of button to be confirmed for camera permission by user. [CHAR LIMIT=20] -->
+ <string name="camera_permission_dialog_positive_button">Allow</string>
+
+ <!-- Text of button to be declined for camera permission by user. [CHAR LIMIT=20] -->
+ <string name="camera_permission_dialog_negative_button">Deny</string>
+
+</resources>
diff --git a/java/com/android/incallui/video/impl/res/values/styles.xml b/java/com/android/incallui/video/impl/res/values/styles.xml
new file mode 100644
index 000000000..b94400875
--- /dev/null
+++ b/java/com/android/incallui/video/impl/res/values/styles.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Incall.Button.VideoCall" parent="Widget.AppCompat.ImageButton">
+ <item name="android:background">@drawable/videocall_video_button_background</item>
+ <item name="android:scaleType">center</item>
+ <item name="android:tint">@color/videocall_button_icon_tint</item>
+ <item name="android:tintMode">src_atop</item>
+ <item name="android:stateListAnimator">@animator/disabled_alpha</item>
+ </style>
+</resources>
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreen.java b/java/com/android/incallui/video/protocol/VideoCallScreen.java
new file mode 100644
index 000000000..0eaf692e2
--- /dev/null
+++ b/java/com/android/incallui/video/protocol/VideoCallScreen.java
@@ -0,0 +1,36 @@
+/*
+ * 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.incallui.video.protocol;
+
+import android.support.v4.app.Fragment;
+
+/** Interface for call video call module. */
+public interface VideoCallScreen {
+
+ void showVideoViews(boolean shouldShowPreview, boolean shouldShowRemote, boolean isRemotelyHeld);
+
+ void onLocalVideoDimensionsChanged();
+
+ void onLocalVideoOrientationChanged();
+
+ void onRemoteVideoDimensionsChanged();
+
+ void updateFullscreenAndGreenScreenMode(
+ boolean shouldShowFullscreen, boolean shouldShowGreenScreen);
+
+ Fragment getVideoCallScreenFragment();
+}
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java b/java/com/android/incallui/video/protocol/VideoCallScreenDelegate.java
new file mode 100644
index 000000000..bbd86ee6a
--- /dev/null
+++ b/java/com/android/incallui/video/protocol/VideoCallScreenDelegate.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.incallui.video.protocol;
+
+import android.content.Context;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Callbacks from the module out to the container. */
+public interface VideoCallScreenDelegate {
+
+ void initVideoCallScreenDelegate(Context context, VideoCallScreen videoCallScreen);
+
+ void onVideoCallScreenUiReady();
+
+ void onVideoCallScreenUiUnready();
+
+ void cancelAutoFullScreen();
+
+ void resetAutoFullscreenTimer();
+
+ void onSystemUiVisibilityChange(boolean visible);
+
+ void onCameraPermissionGranted();
+
+ boolean shouldShowCameraPermissionDialog();
+
+ void onCameraPermissionDialogShown();
+
+ VideoSurfaceTexture getLocalVideoSurfaceTexture();
+
+ VideoSurfaceTexture getRemoteVideoSurfaceTexture();
+
+ int getDeviceOrientation();
+}
diff --git a/java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java b/java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java
new file mode 100644
index 000000000..285857a23
--- /dev/null
+++ b/java/com/android/incallui/video/protocol/VideoCallScreenDelegateFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.incallui.video.protocol;
+
+/** Callbacks from the module out to the container. */
+public interface VideoCallScreenDelegateFactory {
+
+ VideoCallScreenDelegate newVideoCallScreenDelegate();
+}
diff --git a/java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java b/java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java
new file mode 100644
index 000000000..96fccb451
--- /dev/null
+++ b/java/com/android/incallui/videosurface/bindings/VideoSurfaceBindings.java
@@ -0,0 +1,44 @@
+/*
+ * 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.incallui.videosurface.bindings;
+
+import android.view.TextureView;
+import com.android.incallui.videosurface.impl.VideoScale;
+import com.android.incallui.videosurface.impl.VideoSurfaceTextureImpl;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+
+/** Bindings for video surface module. */
+public class VideoSurfaceBindings {
+
+ public static VideoSurfaceTexture createLocalVideoSurfaceTexture() {
+ return new VideoSurfaceTextureImpl(VideoSurfaceTexture.SURFACE_TYPE_LOCAL);
+ }
+
+ public static VideoSurfaceTexture createRemoteVideoSurfaceTexture() {
+ return new VideoSurfaceTextureImpl(VideoSurfaceTexture.SURFACE_TYPE_REMOTE);
+ }
+
+ public static void scaleVideoAndFillView(
+ TextureView textureView, float videoWidth, float videoHeight, float rotationDegrees) {
+ VideoScale.scaleVideoAndFillView(textureView, videoWidth, videoHeight, rotationDegrees);
+ }
+
+ public static void scaleVideoMaintainingAspectRatio(
+ TextureView textureView, int videoWidth, int videoHeight) {
+ VideoScale.scaleVideoMaintainingAspectRatio(textureView, videoWidth, videoHeight);
+ }
+}
diff --git a/java/com/android/incallui/videosurface/impl/VideoScale.java b/java/com/android/incallui/videosurface/impl/VideoScale.java
new file mode 100644
index 000000000..1444f5900
--- /dev/null
+++ b/java/com/android/incallui/videosurface/impl/VideoScale.java
@@ -0,0 +1,147 @@
+/*
+ * 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.incallui.videosurface.impl;
+
+import android.graphics.Matrix;
+import android.view.TextureView;
+import com.android.dialer.common.LogUtil;
+
+/** Utilities to scale the preview and remote video. */
+public class VideoScale {
+ /**
+ * Scales the video in the given view such that the video takes up the entire view. To maintain
+ * aspect ratio the video will be scaled to be larger than the view.
+ */
+ public static void scaleVideoAndFillView(
+ TextureView textureView, float videoWidth, float videoHeight, float rotationDegrees) {
+ float viewWidth = textureView.getWidth();
+ float viewHeight = textureView.getHeight();
+ float viewAspectRatio = viewWidth / viewHeight;
+ float videoAspectRatio = videoWidth / videoHeight;
+ float scaleWidth = 1.0f;
+ float scaleHeight = 1.0f;
+
+ if (viewAspectRatio > videoAspectRatio) {
+ // Scale to exactly fit the width of the video. The top and bottom will be cropped.
+ float scaleFactor = viewWidth / videoWidth;
+ float desiredScaledHeight = videoHeight * scaleFactor;
+ scaleHeight = desiredScaledHeight / viewHeight;
+ } else {
+ // Scale to exactly fit the height of the video. The sides will be cropped.
+ float scaleFactor = viewHeight / videoHeight;
+ float desiredScaledWidth = videoWidth * scaleFactor;
+ scaleWidth = desiredScaledWidth / viewWidth;
+ }
+
+ if (rotationDegrees == 90.0f || rotationDegrees == 270.0f) {
+ // We're in landscape mode but the camera feed is still drawing in portrait mode. Normally,
+ // scale of 1.0 means that the video feed stretches to fit the view. In this case the X axis
+ // is scaled to fit the height and the Y axis is scaled to fit the width.
+ float scaleX = scaleWidth;
+ float scaleY = scaleHeight;
+ scaleWidth = viewHeight / viewWidth * scaleY;
+ scaleHeight = viewWidth / viewHeight * scaleX;
+
+ // This flips the view vertically. Without this the camera feed would be upside down.
+ scaleWidth = scaleWidth * -1.0f;
+ // This flips the view horizontally. Without this the camera feed would be mirrored (left
+ // side would appear on right).
+ scaleHeight = scaleHeight * -1.0f;
+ }
+
+ LogUtil.i(
+ "VideoScale.scaleVideoAndFillView",
+ "view: %f x %f, video: %f x %f scale: %f x %f, rotation: %f",
+ viewWidth,
+ viewHeight,
+ videoWidth,
+ videoHeight,
+ scaleWidth,
+ scaleHeight,
+ rotationDegrees);
+
+ Matrix transform = new Matrix();
+ transform.setScale(
+ scaleWidth,
+ scaleHeight,
+ // This performs the scaling from the horizontal middle of the view.
+ viewWidth / 2.0f,
+ // This perform the scaling from vertical middle of the view.
+ viewHeight / 2.0f);
+ if (rotationDegrees != 0) {
+ transform.postRotate(rotationDegrees, viewWidth / 2.0f, viewHeight / 2.0f);
+ }
+ textureView.setTransform(transform);
+ }
+
+ /**
+ * Scales the video in the given view such that all of the video is visible. This will result in
+ * black bars on the top and bottom or the sides of the video.
+ */
+ public static void scaleVideoMaintainingAspectRatio(
+ TextureView textureView, int videoWidth, int videoHeight) {
+ int viewWidth = textureView.getWidth();
+ int viewHeight = textureView.getHeight();
+ float scaleWidth = 1.0f;
+ float scaleHeight = 1.0f;
+
+ if (viewWidth > viewHeight) {
+ // Landscape layout.
+ if (viewHeight * videoWidth > viewWidth * videoHeight) {
+ // Current display height is too much. Correct it.
+ int desiredHeight = viewWidth * videoHeight / videoWidth;
+ scaleWidth = (float) desiredHeight / (float) viewHeight;
+ } else if (viewHeight * videoWidth < viewWidth * videoHeight) {
+ // Current display width is too much. Correct it.
+ int desiredWidth = viewHeight * videoWidth / videoHeight;
+ scaleWidth = (float) desiredWidth / (float) viewWidth;
+ }
+ } else {
+ // Portrait layout.
+ if (viewHeight * videoWidth > viewWidth * videoHeight) {
+ // Current display height is too much. Correct it.
+ int desiredHeight = viewWidth * videoHeight / videoWidth;
+ scaleHeight = (float) desiredHeight / (float) viewHeight;
+ } else if (viewHeight * videoWidth < viewWidth * videoHeight) {
+ // Current display width is too much. Correct it.
+ int desiredWidth = viewHeight * videoWidth / videoHeight;
+ scaleHeight = (float) desiredWidth / (float) viewWidth;
+ }
+ }
+
+ LogUtil.i(
+ "VideoScale.scaleVideoMaintainingAspectRatio",
+ "view: %d x %d, video: %d x %d scale: %f x %f",
+ viewWidth,
+ viewHeight,
+ videoWidth,
+ videoHeight,
+ scaleWidth,
+ scaleHeight);
+ Matrix transform = new Matrix();
+ transform.setScale(
+ scaleWidth,
+ scaleHeight,
+ // This performs the scaling from the horizontal middle of the view.
+ viewWidth / 2.0f,
+ // This perform the scaling from vertical middle of the view.
+ viewHeight / 2.0f);
+ textureView.setTransform(transform);
+ }
+
+ private VideoScale() {}
+}
diff --git a/java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java b/java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java
new file mode 100644
index 000000000..21160cadb
--- /dev/null
+++ b/java/com/android/incallui/videosurface/impl/VideoSurfaceTextureImpl.java
@@ -0,0 +1,249 @@
+/*
+ * 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.incallui.videosurface.impl;
+
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import com.android.dialer.common.LogUtil;
+import com.android.incallui.videosurface.protocol.VideoSurfaceDelegate;
+import com.android.incallui.videosurface.protocol.VideoSurfaceTexture;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Represents a {@link TextureView} and its associated {@link SurfaceTexture} and {@link Surface}.
+ * Used to manage the lifecycle of these objects across device orientation changes.
+ */
+public class VideoSurfaceTextureImpl implements VideoSurfaceTexture {
+ @SurfaceType private final int surfaceType;
+ private VideoSurfaceDelegate delegate;
+ private TextureView textureView;
+ private Surface savedSurface;
+ private SurfaceTexture savedSurfaceTexture;
+ private Point surfaceDimensions;
+ private Point sourceVideoDimensions;
+ private boolean isDoneWithSurface;
+
+ public VideoSurfaceTextureImpl(@SurfaceType int surfaceType) {
+ this.surfaceType = surfaceType;
+ }
+
+ @Override
+ public void setDelegate(VideoSurfaceDelegate delegate) {
+ LogUtil.i("VideoSurfaceTextureImpl.setDelegate", "delegate: " + delegate + " " + toString());
+ this.delegate = delegate;
+ }
+
+ @Override
+ public int getSurfaceType() {
+ return surfaceType;
+ }
+
+ @Override
+ public Surface getSavedSurface() {
+ return savedSurface;
+ }
+
+ @Override
+ public void setSurfaceDimensions(Point surfaceDimensions) {
+ LogUtil.i(
+ "VideoSurfaceTextureImpl.setSurfaceDimensions",
+ "surfaceDimensions: " + surfaceDimensions + " " + toString());
+ this.surfaceDimensions = surfaceDimensions;
+ if (surfaceDimensions != null && savedSurfaceTexture != null) {
+ savedSurfaceTexture.setDefaultBufferSize(surfaceDimensions.x, surfaceDimensions.y);
+ }
+ }
+
+ @Override
+ public Point getSurfaceDimensions() {
+ return surfaceDimensions;
+ }
+
+ @Override
+ public void setSourceVideoDimensions(Point sourceVideoDimensions) {
+ this.sourceVideoDimensions = sourceVideoDimensions;
+ }
+
+ @Override
+ public Point getSourceVideoDimensions() {
+ return sourceVideoDimensions;
+ }
+
+ @Override
+ public void attachToTextureView(TextureView textureView) {
+ if (this.textureView == textureView) {
+ return;
+ }
+ LogUtil.i("VideoSurfaceTextureImpl.attachToTextureView", toString());
+
+ if (this.textureView != null) {
+ this.textureView.setOnClickListener(null);
+ // Don't clear the surface texture listener. This is important because our listener prevents
+ // the surface from being released so that it can be reused later.
+ }
+
+ this.textureView = textureView;
+ textureView.setSurfaceTextureListener(new SurfaceTextureListener());
+ textureView.setOnClickListener(new OnClickListener());
+
+ boolean areSameSurfaces = Objects.equals(savedSurfaceTexture, textureView.getSurfaceTexture());
+ LogUtil.i("VideoSurfaceTextureImpl.attachToTextureView", "areSameSurfaces: " + areSameSurfaces);
+ if (savedSurfaceTexture != null && !areSameSurfaces) {
+ textureView.setSurfaceTexture(savedSurfaceTexture);
+ if (surfaceDimensions != null && createSurface(surfaceDimensions.x, surfaceDimensions.y)) {
+ onSurfaceCreated();
+ }
+ }
+ isDoneWithSurface = false;
+ }
+
+ @Override
+ public void setDoneWithSurface() {
+ LogUtil.i("VideoSurfaceTextureImpl.setDoneWithSurface", toString());
+ isDoneWithSurface = true;
+ if (textureView != null && textureView.isAvailable()) {
+ return;
+ }
+ if (savedSurface != null) {
+ onSurfaceReleased();
+ savedSurface.release();
+ savedSurface = null;
+ }
+ if (savedSurfaceTexture != null) {
+ savedSurfaceTexture.release();
+ savedSurfaceTexture = null;
+ }
+ }
+
+ private boolean createSurface(int width, int height) {
+ LogUtil.i(
+ "VideoSurfaceTextureImpl.createSurface",
+ "width: " + width + ", height: " + height + " " + toString());
+ if (savedSurfaceTexture != null) {
+ savedSurfaceTexture.setDefaultBufferSize(width, height);
+ savedSurface = new Surface(savedSurfaceTexture);
+ return true;
+ }
+ return false;
+ }
+
+ private void onSurfaceCreated() {
+ if (delegate != null) {
+ delegate.onSurfaceCreated(this);
+ } else {
+ LogUtil.e("VideoSurfaceTextureImpl.onSurfaceCreated", "delegate is null. " + toString());
+ }
+ }
+
+ private void onSurfaceReleased() {
+ if (delegate != null) {
+ delegate.onSurfaceReleased(this);
+ } else {
+ LogUtil.e("VideoSurfaceTextureImpl.onSurfaceReleased", "delegate is null. " + toString());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "VideoSurfaceTextureImpl<%s%s%s%s>",
+ (surfaceType == SURFACE_TYPE_LOCAL ? "local, " : "remote, "),
+ (savedSurface == null ? "no-surface, " : ""),
+ (savedSurfaceTexture == null ? "no-texture, " : ""),
+ (surfaceDimensions == null
+ ? "(-1 x -1)"
+ : (surfaceDimensions.x + " x " + surfaceDimensions.y)));
+ }
+
+ private class SurfaceTextureListener implements TextureView.SurfaceTextureListener {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture newSurfaceTexture, int width, int height) {
+ LogUtil.i(
+ "SurfaceTextureListener.onSurfaceTextureAvailable",
+ "newSurfaceTexture: "
+ + newSurfaceTexture
+ + " "
+ + VideoSurfaceTextureImpl.this.toString());
+
+ // Where there is no saved {@link SurfaceTexture} available, use the newly created one.
+ // If a saved {@link SurfaceTexture} is available, we are re-creating after an
+ // orientation change.
+ boolean surfaceCreated;
+ if (savedSurfaceTexture == null) {
+ savedSurfaceTexture = newSurfaceTexture;
+ surfaceCreated = createSurface(width, height);
+ } else {
+ // A saved SurfaceTexture was found.
+ LogUtil.i(
+ "SurfaceTextureListener.onSurfaceTextureAvailable", "replacing with cached surface...");
+ textureView.setSurfaceTexture(savedSurfaceTexture);
+ surfaceCreated = true;
+ }
+
+ // Inform the delegate that the surface is available.
+ if (surfaceCreated) {
+ onSurfaceCreated();
+ }
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture destroyedSurfaceTexture) {
+ LogUtil.i(
+ "SurfaceTextureListener.onSurfaceTextureDestroyed",
+ "destroyedSurfaceTexture: "
+ + destroyedSurfaceTexture
+ + " "
+ + VideoSurfaceTextureImpl.this.toString());
+ if (delegate != null) {
+ delegate.onSurfaceDestroyed(VideoSurfaceTextureImpl.this);
+ } else {
+ LogUtil.e("SurfaceTextureListener.onSurfaceTextureDestroyed", "delegate is null");
+ }
+
+ if (isDoneWithSurface) {
+ onSurfaceReleased();
+ if (savedSurface != null) {
+ savedSurface.release();
+ savedSurface = null;
+ }
+ }
+ return isDoneWithSurface;
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {}
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
+ }
+
+ private class OnClickListener implements View.OnClickListener {
+ @Override
+ public void onClick(View view) {
+ if (delegate != null) {
+ delegate.onSurfaceClick(VideoSurfaceTextureImpl.this);
+ } else {
+ LogUtil.e("OnClickListener.onClick", "delegate is null");
+ }
+ }
+ }
+}
diff --git a/java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java b/java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.java
new file mode 100644
index 000000000..8fa585a72
--- /dev/null
+++ b/java/com/android/incallui/videosurface/protocol/VideoSurfaceDelegate.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.incallui.videosurface.protocol;
+
+/** Callbacks from the video surface. */
+public interface VideoSurfaceDelegate {
+
+ void onSurfaceCreated(VideoSurfaceTexture videoCallSurface);
+
+ void onSurfaceReleased(VideoSurfaceTexture videoCallSurface);
+
+ void onSurfaceDestroyed(VideoSurfaceTexture videoCallSurface);
+
+ void onSurfaceClick(VideoSurfaceTexture videoCallSurface);
+}
diff --git a/java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java b/java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java
new file mode 100644
index 000000000..411b45f56
--- /dev/null
+++ b/java/com/android/incallui/videosurface/protocol/VideoSurfaceTexture.java
@@ -0,0 +1,57 @@
+/*
+ * 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.incallui.videosurface.protocol;
+
+import android.graphics.Point;
+import android.support.annotation.IntDef;
+import android.view.Surface;
+import android.view.TextureView;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Represents a surface texture for a video feed. */
+public interface VideoSurfaceTexture {
+
+ /** Whether this represents the preview or remote display. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ SURFACE_TYPE_LOCAL,
+ SURFACE_TYPE_REMOTE,
+ })
+ @interface SurfaceType {}
+
+ int SURFACE_TYPE_LOCAL = 1;
+ int SURFACE_TYPE_REMOTE = 2;
+
+ void setDelegate(VideoSurfaceDelegate delegate);
+
+ int getSurfaceType();
+
+ Surface getSavedSurface();
+
+ void setSurfaceDimensions(Point surfaceDimensions);
+
+ Point getSurfaceDimensions();
+
+ void setSourceVideoDimensions(Point sourceVideoDimensions);
+
+ Point getSourceVideoDimensions();
+
+ void attachToTextureView(TextureView textureView);
+
+ void setDoneWithSurface();
+}
diff --git a/java/com/android/incallui/wifi/AndroidManifest.xml b/java/com/android/incallui/wifi/AndroidManifest.xml
new file mode 100644
index 000000000..843f8f3e6
--- /dev/null
+++ b/java/com/android/incallui/wifi/AndroidManifest.xml
@@ -0,0 +1,3 @@
+<manifest
+ package="com.android.incallui.wifi">
+</manifest>
diff --git a/java/com/android/incallui/wifi/EnableWifiCallingPrompt.java b/java/com/android/incallui/wifi/EnableWifiCallingPrompt.java
new file mode 100644
index 000000000..85603bfb1
--- /dev/null
+++ b/java/com/android/incallui/wifi/EnableWifiCallingPrompt.java
@@ -0,0 +1,82 @@
+/*
+ * 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.incallui.wifi;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.telecom.DisconnectCause;
+import android.util.Pair;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+
+/** Prompts the user to enable Wi-Fi calling. */
+public class EnableWifiCallingPrompt {
+ // This is a hidden constant in android.telecom.DisconnectCause. Telecom sets this as a disconnect
+ // reason if it wants us to prompt the user to enable Wi-Fi calling. In Android-O we might
+ // consider using a more explicit way to signal this.
+ private static final String REASON_WIFI_ON_BUT_WFC_OFF = "REASON_WIFI_ON_BUT_WFC_OFF";
+ private static final String ACTION_WIFI_CALLING_SETTINGS =
+ "android.settings.WIFI_CALLING_SETTINGS";
+ private static final String ANDROID_SETTINGS_PACKAGE = "com.android.settings";
+
+ public static boolean shouldShowPrompt(@NonNull DisconnectCause cause) {
+ Assert.isNotNull(cause);
+ if (cause.getReason() != null && cause.getReason().startsWith(REASON_WIFI_ON_BUT_WFC_OFF)) {
+ LogUtil.i(
+ "EnableWifiCallingPrompt.shouldShowPrompt",
+ "showing prompt for disconnect cause: %s",
+ cause);
+ return true;
+ }
+ return false;
+ }
+
+ @NonNull
+ public static Pair<Dialog, CharSequence> createDialog(
+ final @NonNull Context context, @NonNull DisconnectCause cause) {
+ Assert.isNotNull(context);
+ Assert.isNotNull(cause);
+ CharSequence message = cause.getDescription();
+ Dialog dialog =
+ new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setPositiveButton(
+ R.string.incall_enable_wifi_calling_button,
+ new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ openWifiCallingSettings(context);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .create();
+ return new Pair<Dialog, CharSequence>(dialog, message);
+ }
+
+ private static void openWifiCallingSettings(@NonNull Context context) {
+ LogUtil.i("EnableWifiCallingPrompt.openWifiCallingSettings", "opening settings");
+ context.startActivity(
+ new Intent(ACTION_WIFI_CALLING_SETTINGS).setPackage(ANDROID_SETTINGS_PACKAGE));
+ }
+
+ private EnableWifiCallingPrompt() {}
+}
diff --git a/java/com/android/incallui/wifi/res/values/strings.xml b/java/com/android/incallui/wifi/res/values/strings.xml
new file mode 100644
index 000000000..1b52b9fdc
--- /dev/null
+++ b/java/com/android/incallui/wifi/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Button to enable Wi-Fi calling. This is displayed in a dialog after a phone call disconnects
+ because there is no cellular service.
+ [CHAR LIMIT=20] -->
+ <string name="incall_enable_wifi_calling_button">Enable</string>
+
+</resources>
diff --git a/java/com/android/voicemailomtp/ActivationTask.java b/java/com/android/voicemailomtp/ActivationTask.java
new file mode 100644
index 000000000..7de81e685
--- /dev/null
+++ b/java/com/android/voicemailomtp/ActivationTask.java
@@ -0,0 +1,305 @@
+/*
+ * 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.voicemailomtp;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
+import com.android.voicemailomtp.scheduling.BaseTask;
+import com.android.voicemailomtp.scheduling.RetryPolicy;
+import com.android.voicemailomtp.sms.StatusMessage;
+import com.android.voicemailomtp.sms.StatusSmsFetcher;
+import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
+import com.android.voicemailomtp.sync.OmtpVvmSyncService;
+import com.android.voicemailomtp.sync.SyncTask;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Task to activate the visual voicemail service. A request to activate VVM will be sent to the
+ * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If
+ * the user is not provisioned provisioning will be attempted. Activation happens when the phone
+ * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier
+ * spontaneously sent a STATUS SMS.
+ */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class ActivationTask extends BaseTask {
+
+ private static final String TAG = "VvmActivationTask";
+
+ private static final int RETRY_TIMES = 4;
+ private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+ private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle";
+
+ @Nullable
+ private static DeviceProvisionedObserver sDeviceProvisionedObserver;
+
+ private final RetryPolicy mRetryPolicy;
+
+ private Bundle mMessageData;
+
+ public ActivationTask() {
+ super(TASK_ACTIVATION);
+ mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+ addPolicy(mRetryPolicy);
+ }
+
+ /**
+ * Has the user gone through the setup wizard yet.
+ */
+ private static boolean isDeviceProvisioned(Context context) {
+ return Settings.Global.getInt(
+ context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) == 1;
+ }
+
+ /**
+ * @param messageData The optional bundle from {@link android.provider.VoicemailContract#
+ * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task will
+ * request a status SMS itself.
+ */
+ public static void start(Context context, PhoneAccountHandle phoneAccountHandle,
+ @Nullable Bundle messageData) {
+ if (!isDeviceProvisioned(context)) {
+ VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing");
+ // Activation might need information such as system language to be set, so wait until
+ // the setup wizard is finished. The data bundle from the SMS will be re-requested upon
+ // activation.
+ queueActivationAfterProvisioned(context, phoneAccountHandle);
+ return;
+ }
+
+ Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle);
+ if (messageData != null) {
+ intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData);
+ }
+ context.startService(intent);
+ }
+
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE);
+ }
+
+ @Override
+ public Intent createRestartIntent() {
+ Intent intent = super.createRestartIntent();
+ // mMessageData is discarded, request a fresh STATUS SMS for retries.
+ return intent;
+ }
+
+ @Override
+ @WorkerThread
+ public void onExecuteInBackgroundThread() {
+ Assert.isNotMainThread();
+
+ PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ // This should never happen
+ VvmLog.e(TAG, "null PhoneAccountHandle");
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper helper =
+ new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle);
+ if (!helper.isValid()) {
+ VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle);
+ VoicemailStatus.disable(getContext(), phoneAccountHandle);
+ return;
+ }
+
+ // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm
+ // content provider URI which we will use. On some occasions, setting that URI will
+ // fail, so we will perform a few attempts to ensure that the vvm content provider has
+ // a good chance of being started up.
+ if (!VoicemailStatus.edit(getContext(), phoneAccountHandle)
+ .setType(helper.getVvmType())
+ .apply()) {
+ VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType());
+ fail();
+ }
+ VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType());
+
+ if (!OmtpVvmSourceManager.getInstance(getContext())
+ .isVvmSourceRegistered(phoneAccountHandle)) {
+ // This account has not been activated before during the lifetime of this boot.
+ VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(),
+ phoneAccountHandle);
+ if (preferences.getString(OmtpConstants.SERVER_ADDRESS, null) == null) {
+ // Only show the "activating" message if activation has not been completed before.
+ // Subsequent activations are more of a status check and usually does not
+ // concern the user.
+ helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
+ OmtpEvents.CONFIG_ACTIVATING);
+ } else {
+ // The account has been activated on this device before. Pretend it is already
+ // activated. If there are any activation error it will overwrite this status.
+ helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
+ OmtpEvents.CONFIG_ACTIVATING_SUBSEQUENT);
+ }
+
+ }
+ if (!hasSignal(getContext(), phoneAccountHandle)) {
+ VvmLog.i(TAG, "Service lost during activation, aborting");
+ // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING
+ // event.
+ helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle),
+ OmtpEvents.NOTIFICATION_SERVICE_LOST);
+ // Don't retry, a new activation will be started after the signal returned.
+ return;
+ }
+
+ helper.activateSmsFilter();
+ VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor();
+
+ VisualVoicemailProtocol protocol = helper.getProtocol();
+
+ Bundle data;
+ if (mMessageData != null) {
+ // The content of STATUS SMS is provided to launch this task, no need to request it
+ // again.
+ data = mMessageData;
+ } else {
+ try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(),
+ phoneAccountHandle)) {
+ protocol.startActivation(helper, fetcher.getSentIntent());
+ // Both the fetcher and OmtpMessageReceiver will be triggered, but
+ // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be
+ // rejected because the task is still running.
+ data = fetcher.get();
+ } catch (TimeoutException e) {
+ // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS
+ // handleEvent() will do the logging.
+ helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT);
+ fail();
+ return;
+ } catch (CancellationException e) {
+ VvmLog.e(TAG, "Unable to send status request SMS");
+ fail();
+ return;
+ } catch (InterruptedException | ExecutionException | IOException e) {
+ VvmLog.e(TAG, "can't get future STATUS SMS", e);
+ fail();
+ return;
+ }
+ }
+
+ StatusMessage message = new StatusMessage(data);
+ VvmLog.d(TAG, "STATUS SMS received: st=" + message.getProvisioningStatus()
+ + ", rc=" + message.getReturnCode());
+
+ if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) {
+ VvmLog.d(TAG, "subscriber ready, no activation required");
+ updateSource(getContext(), phoneAccountHandle, status, message);
+ } else {
+ if (helper.supportsProvisioning()) {
+ VvmLog.i(TAG, "Subscriber not ready, start provisioning");
+ helper.startProvisioning(this, phoneAccountHandle, status, message, data);
+
+ } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) {
+ VvmLog.i(TAG, "Subscriber new but provisioning is not supported");
+ // Ignore the non-ready state and attempt to use the provided info as is.
+ // This is probably caused by not completing the new user tutorial.
+ updateSource(getContext(), phoneAccountHandle, status, message);
+ } else {
+ VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported");
+ helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE);
+ }
+ }
+ }
+
+ public static void updateSource(Context context, PhoneAccountHandle phone,
+ VoicemailStatus.Editor status, StatusMessage message) {
+ OmtpVvmSourceManager vvmSourceManager =
+ OmtpVvmSourceManager.getInstance(context);
+
+ if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) {
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone);
+ helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS);
+
+ // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+ VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phone);
+ message.putStatus(prefs.edit()).apply();
+
+ // Add the source to indicate that it is active.
+ vvmSourceManager.addSource(phone);
+
+ SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC);
+ } else {
+ VvmLog.e(TAG, "Visual voicemail not available for subscriber.");
+ }
+ }
+
+ private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) {
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class)
+ .createForPhoneAccountHandle(phoneAccountHandle);
+ return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE;
+ }
+
+ private static void queueActivationAfterProvisioned(Context context,
+ PhoneAccountHandle phoneAccountHandle) {
+ if (sDeviceProvisionedObserver == null) {
+ sDeviceProvisionedObserver = new DeviceProvisionedObserver(context);
+ context.getContentResolver()
+ .registerContentObserver(Settings.Global.getUriFor(Global.DEVICE_PROVISIONED),
+ false, sDeviceProvisionedObserver);
+ }
+ sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle);
+ }
+
+ private static class DeviceProvisionedObserver extends ContentObserver {
+
+ private final Context mContext;
+ private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>();
+
+ private DeviceProvisionedObserver(Context context) {
+ super(null);
+ mContext = context;
+ }
+
+ public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) {
+ mPhoneAccountHandles.add(phoneAccountHandle);
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ if (isDeviceProvisioned(mContext)) {
+ VvmLog.i(TAG, "device provisioned, resuming activation");
+ for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) {
+ start(mContext, phoneAccountHandle, null);
+ }
+ mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver);
+ sDeviceProvisionedObserver = null;
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/AndroidManifest.xml b/java/com/android/voicemailomtp/AndroidManifest.xml
new file mode 100644
index 000000000..282a923d2
--- /dev/null
+++ b/java/com/android/voicemailomtp/AndroidManifest.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ package="com.android.voicemailomtp"
+>
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25" />
+
+ <application
+ android:allowBackup="false"
+ android:supportsRtl="true"
+ android:usesCleartextTraffic="true"
+ android:defaultToDeviceProtectedStorage="true"
+ android:directBootAware="true">
+
+ <activity android:name="com.android.voicemailomtp.settings.VoicemailSettingsActivity"
+ android:label="@string/voicemail_settings_label">
+ <intent-filter >
+ <!-- DO NOT RENAME. There are existing apps which use this string. -->
+ <action android:name="com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ <intent-filter>
+ <action android:name="android.telephony.action.CONFIGURE_VOICEMAIL" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <receiver android:name="com.android.voicemailomtp.sms.OmtpMessageReceiver"
+ android:exported="false"
+ androidprv:systemUserOnly="true">
+ <intent-filter>
+ <action android:name="com.android.vociemailomtp.sms.sms_received"/>
+ </intent-filter>
+ </receiver>
+
+ <receiver
+ android:name="com.android.voicemailomtp.fetch.FetchVoicemailReceiver"
+ android:exported="true"
+ android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+ androidprv:systemUserOnly="true">
+ <intent-filter>
+ <action android:name="android.intent.action.FETCH_VOICEMAIL" />
+ <data
+ android:scheme="content"
+ android:host="com.android.voicemail"
+ android:mimeType="vnd.android.cursor.item/voicemail" />
+ </intent-filter>
+ </receiver>
+ <receiver
+ android:name="com.android.voicemailomtp.sync.OmtpVvmSyncReceiver"
+ android:exported="true"
+ android:permission="com.android.voicemail.permission.READ_VOICEMAIL"
+ androidprv:systemUserOnly="true">
+ <intent-filter>
+ <action android:name="android.provider.action.SYNC_VOICEMAIL"/>
+ </intent-filter>
+ </receiver>
+ <receiver
+ android:name="com.android.voicemailomtp.sync.VoicemailProviderChangeReceiver"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.PROVIDER_CHANGED" />
+ <data
+ android:scheme="content"
+ android:host="com.android.voicemail"
+ android:mimeType="vnd.android.cursor.dir/voicemails"/>
+ </intent-filter>
+ </receiver>
+
+ <service
+ android:name="com.android.voicemailomtp.scheduling.TaskSchedulerService"
+ android:exported="false" />
+
+ <service
+ android:name="com.android.voicemailomtp.OmtpService"
+ android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.telephony.VisualVoicemailService"/>
+ </intent-filter>
+ </service>
+ <activity android:name=".settings.VoicemailChangePinActivity"
+ android:exported="false"
+ android:windowSoftInputMode="stateVisible|adjustResize">
+ </activity>
+ </application>
+</manifest>
diff --git a/java/com/android/voicemailomtp/Assert.java b/java/com/android/voicemailomtp/Assert.java
new file mode 100644
index 000000000..1d295bed1
--- /dev/null
+++ b/java/com/android/voicemailomtp/Assert.java
@@ -0,0 +1,62 @@
+/*
+ * 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.voicemailomtp;
+
+import android.os.Looper;
+
+/**
+ * Assertions which will result in program termination.
+ */
+public class Assert {
+
+ private static Boolean sIsMainThreadForTest;
+
+ public static void isTrue(boolean condition) {
+ if (!condition) {
+ throw new AssertionError("Expected condition to be true");
+ }
+ }
+
+ public static void isMainThread() {
+ if (sIsMainThreadForTest != null) {
+ isTrue(sIsMainThreadForTest);
+ return;
+ }
+ isTrue(Looper.getMainLooper().equals(Looper.myLooper()));
+ }
+
+ public static void isNotMainThread() {
+ if (sIsMainThreadForTest != null) {
+ isTrue(!sIsMainThreadForTest);
+ return;
+ }
+ isTrue(!Looper.getMainLooper().equals(Looper.myLooper()));
+ }
+
+ public static void fail() {
+ throw new AssertionError("Fail");
+ }
+
+ /**
+ * Override the main thread status for tests. Set to null to revert to normal behavior
+ */
+ @NeededForTesting
+ public static void setIsMainThreadForTesting(Boolean isMainThread) {
+ sIsMainThreadForTest = isMainThread;
+ }
+}
diff --git a/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java b/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java
new file mode 100644
index 000000000..6a4b5104a
--- /dev/null
+++ b/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java
@@ -0,0 +1,202 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.Context;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.OmtpEvents.Type;
+
+public class DefaultOmtpEventHandler {
+
+ private static final String TAG = "DefErrorCodeHandler";
+
+ public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event.getType()) {
+ case Type.CONFIGURATION:
+ handleConfigurationEvent(context, status, event);
+ break;
+ case Type.DATA_CHANNEL:
+ handleDataChannelEvent(context, status, event);
+ break;
+ case Type.NOTIFICATION_CHANNEL:
+ handleNotificationChannelEvent(context, config, status, event);
+ break;
+ case Type.OTHER:
+ handleOtherEvent(context, status, event);
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+ }
+ }
+
+ private static void handleConfigurationEvent(Context context, VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event) {
+ case CONFIG_DEFAULT_PIN_REPLACED:
+ case CONFIG_REQUEST_STATUS_SUCCESS:
+ case CONFIG_PIN_SET:
+ status
+ .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case CONFIG_ACTIVATING:
+ // Wipe all errors from the last activation. All errors shown should be new errors
+ // for this activation.
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
+ break;
+ case CONFIG_ACTIVATING_SUBSEQUENT:
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_OK)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply();
+ break;
+ case CONFIG_SERVICE_NOT_AVAILABLE:
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_FAILED)
+ .apply();
+ break;
+ case CONFIG_STATUS_SMS_TIME_OUT:
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_FAILED)
+ .apply();
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid configuration event " + event);
+ }
+ }
+
+ private static void handleDataChannelEvent(Context context, VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event) {
+ case DATA_IMAP_OPERATION_STARTED:
+ case DATA_IMAP_OPERATION_COMPLETED:
+ status
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+ .apply();
+ break;
+
+ case DATA_NO_CONNECTION:
+ status
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+ .apply();
+ break;
+
+ case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+ status
+ .setDataChannelState(
+ Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED)
+ .apply();
+ break;
+ case DATA_INVALID_PORT:
+ status
+ .setDataChannelState(
+ VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+ .apply();
+ break;
+ case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+ status
+ .setDataChannelState(
+ VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR)
+ .apply();
+ break;
+ case DATA_SSL_INVALID_HOST_NAME:
+ case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+ case DATA_IOE_ON_OPEN:
+ case DATA_GENERIC_IMAP_IOE:
+ status
+ .setDataChannelState(
+ VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR)
+ .apply();
+ break;
+ case DATA_BAD_IMAP_CREDENTIAL:
+ case DATA_AUTH_UNKNOWN_USER:
+ case DATA_AUTH_UNKNOWN_DEVICE:
+ case DATA_AUTH_INVALID_PASSWORD:
+ case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+ case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+ case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+ case DATA_AUTH_USER_IS_BLOCKED:
+ status
+ .setDataChannelState(
+ VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION)
+ .apply();
+ break;
+
+ case DATA_REJECTED_SERVER_RESPONSE:
+ case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+ case DATA_MAILBOX_OPEN_FAILED:
+ case DATA_SSL_EXCEPTION:
+ case DATA_ALL_SOCKET_CONNECTION_FAILED:
+ status
+ .setDataChannelState(
+ VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR)
+ .apply();
+ break;
+
+ default:
+ VvmLog.wtf(TAG, "invalid data channel event " + event);
+ }
+ }
+
+ private static void handleNotificationChannelEvent(Context context,
+ OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case NOTIFICATION_IN_SERVICE:
+ status
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK)
+ // Clear the error state. A sync should follow signal return so any error
+ // will be reposted.
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_OK)
+ .apply();
+ break;
+ case NOTIFICATION_SERVICE_LOST:
+ status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION);
+ if (config.isCellularDataRequired()) {
+ status.setDataChannelState(
+ Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED);
+ }
+ status.apply();
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid notification channel event " + event);
+ }
+ }
+
+ private static void handleOtherEvent(Context context, VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event) {
+ case OTHER_SOURCE_REMOVED:
+ status
+ .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+ .setNotificationChannelState(
+ Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+ .apply();
+ break;
+ default:
+ VvmLog.wtf(TAG, "invalid other event " + event);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/voicemailomtp/NeededForTesting.java
new file mode 100644
index 000000000..20517fed8
--- /dev/null
+++ b/java/com/android/voicemailomtp/NeededForTesting.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.voicemailomtp;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.SOURCE)
+public @interface NeededForTesting {
+
+}
diff --git a/java/com/android/voicemailomtp/OmtpConstants.java b/java/com/android/voicemailomtp/OmtpConstants.java
new file mode 100644
index 000000000..da2b998b6
--- /dev/null
+++ b/java/com/android/voicemailomtp/OmtpConstants.java
@@ -0,0 +1,248 @@
+/*
+ * 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.voicemailomtp;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec. <p> In essence this is
+ * a programmatic representation of the relevant portions of OMTP spec.
+ */
+public class OmtpConstants {
+ public static final String SMS_FIELD_SEPARATOR = ";";
+ public static final String SMS_KEY_VALUE_SEPARATOR = "=";
+ public static final String SMS_PREFIX_SEPARATOR = ":";
+
+ public static final String SYNC_SMS_PREFIX = "SYNC";
+ public static final String STATUS_SMS_PREFIX = "STATUS";
+
+ // This is the format designated by the OMTP spec.
+ public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
+
+ /** OMTP protocol versions. */
+ public static final String PROTOCOL_VERSION1_1 = "11";
+ public static final String PROTOCOL_VERSION1_2 = "12";
+ public static final String PROTOCOL_VERSION1_3 = "13";
+
+ ///////////////////////// Client/Mobile originated SMS //////////////////////
+
+ /** Mobile Originated requests */
+ public static final String ACTIVATE_REQUEST = "Activate";
+ public static final String DEACTIVATE_REQUEST = "Deactivate";
+ public static final String STATUS_REQUEST = "Status";
+
+ /** fields that can be present in a Mobile Originated OMTP SMS */
+ public static final String CLIENT_TYPE = "ct";
+ public static final String APPLICATION_PORT = "pt";
+ public static final String PROTOCOL_VERSION = "pv";
+
+
+ //////////////////////////////// Sync SMS fields ////////////////////////////
+
+ /**
+ * Sync SMS fields.
+ * <p>
+ * Each string constant is the field's key in the SMS body which is used by the parser to
+ * identify the field's value, if present, in the SMS body.
+ */
+
+ /**
+ * The event that triggered this SYNC SMS.
+ * See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES}
+ */
+ public static final String SYNC_TRIGGER_EVENT = "ev";
+ public static final String MESSAGE_UID = "id";
+ public static final String MESSAGE_LENGTH = "l";
+ public static final String NUM_MESSAGE_COUNT = "c";
+ /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
+ public static final String CONTENT_TYPE = "t";
+ public static final String SENDER = "s";
+ public static final String TIME = "dt";
+
+ /**
+ * SYNC message trigger events.
+ * <p>
+ * These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
+ */
+ public static final String NEW_MESSAGE = "NM";
+ public static final String MAILBOX_UPDATE = "MBU";
+ public static final String GREETINGS_UPDATE = "GU";
+
+ public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
+ NEW_MESSAGE,
+ MAILBOX_UPDATE,
+ GREETINGS_UPDATE
+ };
+
+ /**
+ * Content types supported by OMTP VVM.
+ * <p>
+ * These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
+ */
+ public static final String VOICE = "v";
+ public static final String VIDEO = "o";
+ public static final String FAX = "f";
+ /** Voice message deposited by an external application */
+ public static final String INFOTAINMENT = "i";
+ /** Empty Call Capture - i.e. voicemail with no voice message. */
+ public static final String ECC = "e";
+
+ public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
+
+ ////////////////////////////// Status SMS fields ////////////////////////////
+
+ /**
+ * Status SMS fields.
+ * <p>
+ * Each string constant is the field's key in the SMS body which is used by the parser to
+ * identify the field's value, if present, in the SMS body.
+ */
+ /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
+ public static final String PROVISIONING_STATUS = "st";
+ /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
+ public static final String RETURN_CODE = "rc";
+ /** URL to send users to for activation VVM */
+ public static final String SUBSCRIPTION_URL = "rs";
+ /** IMAP4/SMTP server IP address or fully qualified domain name */
+ public static final String SERVER_ADDRESS = "srv";
+ /** Phone number to access voicemails through Telephony User Interface */
+ public static final String TUI_ACCESS_NUMBER = "tui";
+ public static final String TUI_PASSWORD_LENGTH = "pw_len";
+ /** Number to send client origination SMS */
+ public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
+ public static final String IMAP_PORT = "ipt";
+ public static final String IMAP_USER_NAME = "u";
+ public static final String IMAP_PASSWORD = "pw";
+ public static final String SMTP_PORT = "spt";
+ public static final String SMTP_USER_NAME = "smtp_u";
+ public static final String SMTP_PASSWORD = "smtp_pw";
+
+ /**
+ * User provisioning status values.
+ * <p>
+ * Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
+ */
+ public static final String SUBSCRIBER_NEW = "N";
+ public static final String SUBSCRIBER_READY = "R";
+ public static final String SUBSCRIBER_PROVISIONED = "P";
+ public static final String SUBSCRIBER_UNKNOWN = "U";
+ public static final String SUBSCRIBER_BLOCKED = "B";
+
+ public static final String[] PROVISIONING_STATUS_VALUES = {
+ SUBSCRIBER_NEW,
+ SUBSCRIBER_READY,
+ SUBSCRIBER_PROVISIONED,
+ SUBSCRIBER_UNKNOWN,
+ SUBSCRIBER_BLOCKED
+ };
+
+ /**
+ * The return code included in a status message.
+ * <p>
+ * These are the possible values of {@link OmtpConstants#RETURN_CODE}.
+ */
+ public static final String SUCCESS = "0";
+ public static final String SYSTEM_ERROR = "1";
+ public static final String SUBSCRIBER_ERROR = "2";
+ public static final String MAILBOX_UNKNOWN = "3";
+ public static final String VVM_NOT_ACTIVATED = "4";
+ public static final String VVM_NOT_PROVISIONED = "5";
+ public static final String VVM_CLIENT_UKNOWN = "6";
+ public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
+
+ public static final String[] RETURN_CODE_VALUES = {
+ SUCCESS,
+ SYSTEM_ERROR,
+ SUBSCRIBER_ERROR,
+ MAILBOX_UNKNOWN,
+ VVM_NOT_ACTIVATED,
+ VVM_NOT_PROVISIONED,
+ VVM_CLIENT_UKNOWN,
+ VVM_MAILBOX_NOT_INITIALIZED,
+ };
+
+ /**
+ * A map of all the field keys to the possible values they can have.
+ */
+ public static final Map<String, String[]> possibleValuesMap = new HashMap<String, String[]>() {{
+ put(SYNC_TRIGGER_EVENT, SYNC_TRIGGER_EVENT_VALUES);
+ put(CONTENT_TYPE, CONTENT_TYPE_VALUES);
+ put(PROVISIONING_STATUS, PROVISIONING_STATUS_VALUES);
+ put(RETURN_CODE, RETURN_CODE_VALUES);
+ }};
+
+ /**
+ * IMAP command extensions
+ */
+
+ /**
+ * OMTP spec v1.3 2.3.1 Change password request syntax
+ *
+ * This changes the PIN to access the Telephone User Interface, the traditional voicemail
+ * system.
+ */
+ public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+
+ /**
+ * OMTP spec v1.3 2.4.1 Change languate request syntax
+ *
+ * This changes the language in the Telephone User Interface.
+ */
+ public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s";
+
+ /**
+ * OMTP spec v1.3 2.5.1 Close NUT Request syntax
+ *
+ * This disables the new user tutorial, the message played to new users calling in the Telephone
+ * User Interface.
+ */
+ public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT";
+
+ /**
+ * Possible NO responses for CHANGE_TUI_PWD
+ */
+
+ public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short";
+ public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long";
+ public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak";
+ public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch";
+ public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER =
+ "password contains invalid characters";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {CHANGE_PIN_SUCCESS, CHANGE_PIN_TOO_SHORT, CHANGE_PIN_TOO_LONG,
+ CHANGE_PIN_TOO_WEAK, CHANGE_PIN_MISMATCH, CHANGE_PIN_INVALID_CHARACTER,
+ CHANGE_PIN_SYSTEM_ERROR})
+
+ public @interface ChangePinResult {
+
+ }
+
+ public static final int CHANGE_PIN_SUCCESS = 0;
+ public static final int CHANGE_PIN_TOO_SHORT = 1;
+ public static final int CHANGE_PIN_TOO_LONG = 2;
+ public static final int CHANGE_PIN_TOO_WEAK = 3;
+ public static final int CHANGE_PIN_MISMATCH = 4;
+ public static final int CHANGE_PIN_INVALID_CHARACTER = 5;
+ public static final int CHANGE_PIN_SYSTEM_ERROR = 6;
+
+ /** Indicates the client is Google visual voicemail version 1.0. */
+ public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10";
+}
diff --git a/java/com/android/voicemailomtp/OmtpEvents.java b/java/com/android/voicemailomtp/OmtpEvents.java
new file mode 100644
index 000000000..d5c2a8b03
--- /dev/null
+++ b/java/com/android/voicemailomtp/OmtpEvents.java
@@ -0,0 +1,156 @@
+/*
+ * 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.voicemailomtp;
+
+import android.support.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Events internal to the OMTP client. These should be translated into {@link
+ * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status
+ * table.
+ */
+public enum OmtpEvents {
+
+ // Configuration State
+ CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true),
+
+ CONFIG_PIN_SET(Type.CONFIGURATION, true),
+ // The voicemail PIN is replaced with a generated PIN, user should change it.
+ CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true),
+ CONFIG_ACTIVATING(Type.CONFIGURATION, true),
+ // There are already activation records, this is only a book-keeping activation.
+ CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true),
+ CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION),
+ CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION),
+
+ // Data channel State
+
+ // A new sync has started, old errors in data channel should be cleared.
+ DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true),
+ // Successfully downloaded/uploaded data from the server, which means the data channel is clear.
+ DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true),
+ // The port provided in the STATUS SMS is invalid.
+ DATA_INVALID_PORT(Type.DATA_CHANNEL),
+ // No connection to the internet, and the carrier requires cellular data
+ DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL),
+ // No connection to the internet.
+ DATA_NO_CONNECTION(Type.DATA_CHANNEL),
+ // Address lookup for the server hostname failed. DNS error?
+ DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL),
+ // All destination address that resolves to the server hostname are rejected or timed out
+ DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL),
+ // Failed to establish SSL with the server, either with a direct SSL connection or by
+ // STARTTLS command
+ DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL),
+ // Identity of the server cannot be verified.
+ DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL),
+ // The server rejected our username/password
+ DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL),
+
+ DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL),
+ DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL),
+ DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL),
+ DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL),
+ DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL),
+ DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL),
+ DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL),
+
+ // A command to the server didn't result with an "OK" or continuation request
+ DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL),
+ // The server did not greet us with a "OK", possibly not a IMAP server.
+ DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL),
+ // An IOException occurred while trying to open an ImapConnection
+ // TODO: reduce scope
+ DATA_IOE_ON_OPEN(Type.DATA_CHANNEL),
+ // The SELECT command on a mailbox is rejected
+ DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL),
+ // An IOException has occurred
+ // TODO: reduce scope
+ DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL),
+ // An SslException has occurred while opening an ImapConnection
+ // TODO: reduce scope
+ DATA_SSL_EXCEPTION(Type.DATA_CHANNEL),
+
+ // Notification Channel
+
+ // Cell signal restored, can received VVM SMSs
+ NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true),
+ // Cell signal lost, cannot received VVM SMSs
+ NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false),
+
+
+ // Other
+ OTHER_SOURCE_REMOVED(Type.OTHER, false),
+
+ // VVM3
+ VVM3_NEW_USER_SETUP_FAILED,
+ // Table 4. client internal error handling
+ VVM3_VMG_DNS_FAILURE,
+ VVM3_SPG_DNS_FAILURE,
+ VVM3_VMG_CONNECTION_FAILED,
+ VVM3_SPG_CONNECTION_FAILED,
+ VVM3_VMG_TIMEOUT,
+ VVM3_STATUS_SMS_TIMEOUT,
+
+ VVM3_SUBSCRIBER_PROVISIONED,
+ VVM3_SUBSCRIBER_BLOCKED,
+ VVM3_SUBSCRIBER_UNKNOWN;
+
+ public static class Type {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER})
+ public @interface Values {
+
+ }
+
+ public static final int CONFIGURATION = 1;
+ public static final int DATA_CHANNEL = 2;
+ public static final int NOTIFICATION_CHANNEL = 3;
+ public static final int OTHER = 4;
+ }
+
+ private final int mType;
+ private final boolean mIsSuccess;
+
+ OmtpEvents(int type, boolean isSuccess) {
+ mType = type;
+ mIsSuccess = isSuccess;
+ }
+
+ OmtpEvents(int type) {
+ mType = type;
+ mIsSuccess = false;
+ }
+
+ OmtpEvents() {
+ mType = Type.OTHER;
+ mIsSuccess = false;
+ }
+
+ @Type.Values
+ public int getType() {
+ return mType;
+ }
+
+ public boolean isSuccess() {
+ return mIsSuccess;
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/OmtpService.java b/java/com/android/voicemailomtp/OmtpService.java
new file mode 100644
index 000000000..261a7cb32
--- /dev/null
+++ b/java/com/android/voicemailomtp/OmtpService.java
@@ -0,0 +1,65 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSms;
+
+import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
+
+public class OmtpService extends VisualVoicemailService {
+
+ private static String TAG = "VvmOmtpService";
+
+ public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received";
+
+ public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms";
+
+ @Override
+ public void onCellServiceConnected(VisualVoicemailTask task,
+ final PhoneAccountHandle phoneAccountHandle) {
+ VvmLog.i(TAG, "onCellServiceConnected");
+ ActivationTask
+ .start(OmtpService.this, phoneAccountHandle, null);
+ task.finish();
+ }
+
+ @Override
+ public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) {
+ VvmLog.i(TAG, "onSmsReceived");
+ Intent intent = new Intent(ACTION_SMS_RECEIVED);
+ intent.setPackage(getPackageName());
+ intent.putExtra(EXTRA_VOICEMAIL_SMS, sms);
+ sendBroadcast(intent);
+ task.finish();
+ }
+
+ @Override
+ public void onSimRemoved(final VisualVoicemailTask task,
+ final PhoneAccountHandle phoneAccountHandle) {
+ VvmLog.i(TAG, "onSimRemoved");
+ OmtpVvmSourceManager.getInstance(OmtpService.this).removeSource(phoneAccountHandle);
+ task.finish();
+ }
+
+ @Override
+ public void onStopped(VisualVoicemailTask task) {
+ VvmLog.i(TAG, "onStopped");
+ }
+}
diff --git a/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java
new file mode 100644
index 000000000..b3e72d215
--- /dev/null
+++ b/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java
@@ -0,0 +1,423 @@
+/*
+ * 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.voicemailomtp;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.os.PersistableBundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.telephony.VisualVoicemailService;
+import android.telephony.VisualVoicemailSmsFilterSettings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
+import com.android.voicemailomtp.protocol.VisualVoicemailProtocolFactory;
+import com.android.voicemailomtp.sms.StatusMessage;
+
+import java.util.Arrays;
+import java.util.Set;
+
+/**
+ * Manages carrier dependent visual voicemail configuration values. The primary source is the value
+ * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config
+ * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will
+ * be used (in res/xml/vvm_config.xml)
+ *
+ * Hidden configs are new configs that are planned for future APIs, or miscellaneous settings that
+ * may clutter CarrierConfigManager too much.
+ *
+ * The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()}
+ */
+public class OmtpVvmCarrierConfigHelper {
+
+ private static final String TAG = "OmtpVvmCarrierCfgHlpr";
+
+ static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING;
+ static final String KEY_VVM_DESTINATION_NUMBER_STRING =
+ CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING;
+ static final String KEY_VVM_PORT_NUMBER_INT =
+ CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT;
+ static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING =
+ CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING;
+ static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY =
+ "carrier_vvm_package_name_string_array";
+ static final String KEY_VVM_PREFETCH_BOOL =
+ CarrierConfigManager.KEY_VVM_PREFETCH_BOOL;
+ static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL =
+ CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL;
+
+ /**
+ * @see #getSslPort()
+ */
+ static final String KEY_VVM_SSL_PORT_NUMBER_INT =
+ "vvm_ssl_port_number_int";
+
+ /**
+ * @see #isLegacyModeEnabled()
+ */
+ static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL =
+ "vvm_legacy_mode_enabled_bool";
+
+ /**
+ * Ban a capability reported by the server from being used. The array of string should be a
+ * subset of the capabilities returned IMAP CAPABILITY command.
+ *
+ * @see #getDisabledCapabilities()
+ */
+ static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY =
+ "vvm_disabled_capabilities_string_array";
+ static final String KEY_VVM_CLIENT_PREFIX_STRING =
+ "vvm_client_prefix_string";
+
+ private final Context mContext;
+ private final PersistableBundle mCarrierConfig;
+ private final String mVvmType;
+ private final VisualVoicemailProtocol mProtocol;
+ private final PersistableBundle mTelephonyConfig;
+
+ private PhoneAccountHandle mPhoneAccountHandle;
+
+ public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) {
+ mContext = context;
+ mPhoneAccountHandle = handle;
+ mCarrierConfig = getCarrierConfig();
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ mTelephonyConfig = new TelephonyVvmConfigManager(context.getResources())
+ .getConfig(telephonyManager.createForPhoneAccountHandle(mPhoneAccountHandle)
+ .getSimOperator());
+
+ mVvmType = getVvmType();
+ mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+
+ }
+
+ @VisibleForTesting
+ OmtpVvmCarrierConfigHelper(Context context, PersistableBundle carrierConfig,
+ PersistableBundle telephonyConfig) {
+ mContext = context;
+ mCarrierConfig = carrierConfig;
+ mTelephonyConfig = telephonyConfig;
+ mVvmType = getVvmType();
+ mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ @Nullable
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ /**
+ * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a
+ * known protocol.
+ */
+ public boolean isValid() {
+ return mProtocol != null;
+ }
+
+ @Nullable
+ public String getVvmType() {
+ return (String) getValue(KEY_VVM_TYPE_STRING);
+ }
+
+ @Nullable
+ public VisualVoicemailProtocol getProtocol() {
+ return mProtocol;
+ }
+
+ /**
+ * @returns arbitrary String stored in the config file. Used for protocol specific values.
+ */
+ @Nullable
+ public String getString(String key) {
+ return (String) getValue(key);
+ }
+
+ @Nullable
+ public Set<String> getCarrierVvmPackageNames() {
+ Set<String> names = getCarrierVvmPackageNames(mCarrierConfig);
+ if (names != null) {
+ return names;
+ }
+ return getCarrierVvmPackageNames(mTelephonyConfig);
+ }
+
+ private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ Set<String> names = new ArraySet<>();
+ if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) {
+ names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING));
+ }
+ if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) {
+ names.addAll(Arrays.asList(
+ bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)));
+ }
+ if (names.isEmpty()) {
+ return null;
+ }
+ return names;
+ }
+
+ /**
+ * For checking upon sim insertion whether visual voicemail should be enabled. This method does
+ * so by checking if the carrier's voicemail app is installed.
+ */
+ public boolean isEnabledByDefault() {
+ if (!isValid()) {
+ return false;
+ }
+
+ Set<String> carrierPackages = getCarrierVvmPackageNames();
+ if (carrierPackages == null) {
+ return true;
+ }
+ for (String packageName : carrierPackages) {
+ try {
+ mContext.getPackageManager().getPackageInfo(packageName, 0);
+ return false;
+ } catch (NameNotFoundException e) {
+ // Do nothing.
+ }
+ }
+ return true;
+ }
+
+ public boolean isCellularDataRequired() {
+ return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false);
+ }
+
+ public boolean isPrefetchEnabled() {
+ return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true);
+ }
+
+
+ public int getApplicationPort() {
+ return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0);
+ }
+
+ @Nullable
+ public String getDestinationNumber() {
+ return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING);
+ }
+
+ /**
+ * Hidden config.
+ *
+ * @return Port to start a SSL IMAP connection directly.
+ *
+ * TODO: make config public and add to CarrierConfigManager
+ */
+ public int getSslPort() {
+ return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0);
+ }
+
+ /**
+ * Hidden Config.
+ *
+ * <p>Sometimes the server states it supports a certain feature but we found they have bug on
+ * the server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability
+ * but using it to login will cause subsequent response to be erroneous.
+ *
+ * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined
+ * to have issues and should not be used.
+ */
+ @Nullable
+ public Set<String> getDisabledCapabilities() {
+ Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig);
+ if (disabledCapabilities != null) {
+ return disabledCapabilities;
+ }
+ return getDisabledCapabilities(mTelephonyConfig);
+ }
+
+ @Nullable
+ private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) {
+ return null;
+ }
+ ArraySet<String> result = new ArraySet<String>();
+ result.addAll(
+ Arrays.asList(bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)));
+ return result;
+ }
+
+ public String getClientPrefix() {
+ String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING);
+ if (prefix != null) {
+ return prefix;
+ }
+ return "//VVM";
+ }
+
+ /**
+ * Should legacy mode be used when the OMTP VVM client is disabled?
+ *
+ * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on
+ * the client side all network operations are disabled. SMSs are still monitored so a new
+ * message SYNC SMS will be translated to show a message waiting indicator, like traditional
+ * voicemails.
+ *
+ * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to
+ * function without the data cost.
+ */
+ public boolean isLegacyModeEnabled() {
+ return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false);
+ }
+
+ public void startActivation() {
+ PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ // This should never happen
+ // Error logged in getPhoneAccountHandle().
+ return;
+ }
+
+ if (mVvmType == null || mVvmType.isEmpty()) {
+ // The VVM type is invalid; we should never have gotten here in the first place since
+ // this is loaded initially in the constructor, and callers should check isValid()
+ // before trying to start activation anyways.
+ VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " +
+ phoneAccountHandle);
+ return;
+ }
+
+ if (mProtocol != null) {
+ ActivationTask.start(mContext, mPhoneAccountHandle, null);
+ }
+ }
+
+ public void activateSmsFilter() {
+ VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(),
+ new VisualVoicemailSmsFilterSettings.Builder()
+ .setClientPrefix(getClientPrefix())
+ .build());
+ }
+
+ public void startDeactivation() {
+ if (!isLegacyModeEnabled()) {
+ // SMS should still be filtered in legacy mode
+ VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null);
+ }
+ if (mProtocol != null) {
+ mProtocol.startDeactivation(this);
+ }
+ }
+
+ public boolean supportsProvisioning() {
+ if (mProtocol != null) {
+ return mProtocol.supportsProvisioning();
+ }
+ return false;
+ }
+
+ public void startProvisioning(ActivationTask task, PhoneAccountHandle phone,
+ VoicemailStatus.Editor status, StatusMessage message, Bundle data) {
+ if (mProtocol != null) {
+ mProtocol.startProvisioning(task, phone, this, status, message, data);
+ }
+ }
+
+ public void requestStatus(@Nullable PendingIntent sentIntent) {
+ if (mProtocol != null) {
+ mProtocol.requestStatus(this, sentIntent);
+ }
+ }
+
+ public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+ VvmLog.i(TAG, "OmtpEvent:" + event);
+ if (mProtocol != null) {
+ mProtocol.handleEvent(mContext, this, status, event);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper [");
+ builder.append("phoneAccountHandle: ").append(mPhoneAccountHandle)
+ .append(", carrierConfig: ").append(mCarrierConfig != null)
+ .append(", telephonyConfig: ").append(mTelephonyConfig != null)
+ .append(", type: ").append(getVvmType())
+ .append(", destinationNumber: ").append(getDestinationNumber())
+ .append(", applicationPort: ").append(getApplicationPort())
+ .append(", sslPort: ").append(getSslPort())
+ .append(", isEnabledByDefault: ").append(isEnabledByDefault())
+ .append(", isCellularDataRequired: ").append(isCellularDataRequired())
+ .append(", isPrefetchEnabled: ").append(isPrefetchEnabled())
+ .append(", isLegacyModeEnabled: ").append(isLegacyModeEnabled())
+ .append("]");
+ return builder.toString();
+ }
+
+ @Nullable
+ private PersistableBundle getCarrierConfig() {
+
+ CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
+ mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ if (carrierConfigManager == null) {
+ VvmLog.w(TAG, "No carrier config service found.");
+ return null;
+ }
+
+ PersistableBundle config = TelephonyManagerStub
+ .getCarrirConfigForPhoneAccountHandle(getContext(), mPhoneAccountHandle);
+
+ if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) {
+ return null;
+ }
+ return config;
+ }
+
+ @Nullable
+ private Object getValue(String key) {
+ return getValue(key, null);
+ }
+
+ @Nullable
+ private Object getValue(String key, Object defaultValue) {
+ Object result;
+ if (mCarrierConfig != null) {
+ result = mCarrierConfig.get(key);
+ if (result != null) {
+ return result;
+ }
+ }
+ if (mTelephonyConfig != null) {
+ result = mTelephonyConfig.get(key);
+ if (result != null) {
+ return result;
+ }
+ }
+ return defaultValue;
+ }
+
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/SubscriptionInfoHelper.java b/java/com/android/voicemailomtp/SubscriptionInfoHelper.java
new file mode 100644
index 000000000..b916247ad
--- /dev/null
+++ b/java/com/android/voicemailomtp/SubscriptionInfoHelper.java
@@ -0,0 +1,75 @@
+/**
+ * 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.voicemailomtp;
+
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.text.TextUtils;
+
+/**
+ * Helper for manipulating intents or components with subscription-related information.
+ *
+ * In settings, subscription ids and labels are passed along to indicate that settings
+ * are being changed for particular subscriptions. This helper provides functions for
+ * helping extract this info and perform common operations using this info.
+ */
+public class SubscriptionInfoHelper {
+ public static final int NO_SUB_ID = -1;
+
+ // Extra on intent containing the id of a subscription.
+ public static final String SUB_ID_EXTRA =
+ "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId";
+ // Extra on intent containing the label of a subscription.
+ private static final String SUB_LABEL_EXTRA =
+ "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel";
+
+ private static Context mContext;
+
+ private static int mSubId = NO_SUB_ID;
+ private static String mSubLabel;
+
+ /**
+ * Instantiates the helper, by extracting the subscription id and label from the intent.
+ */
+ public SubscriptionInfoHelper(Context context, Intent intent) {
+ mContext = context;
+ mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID);
+ mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA);
+ }
+
+ /**
+ * Sets the action bar title to the string specified by the given resource id, formatting
+ * it with the subscription label. This assumes the resource string is formattable with a
+ * string-type specifier.
+ *
+ * If the subscription label does not exists, leave the existing title.
+ */
+ public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) {
+ if (actionBar == null || TextUtils.isEmpty(mSubLabel)) {
+ return;
+ }
+
+ String title = String.format(res.getString(resId), mSubLabel);
+ actionBar.setTitle(title);
+ }
+
+ public int getSubId() {
+ return mSubId;
+ }
+}
diff --git a/java/com/android/voicemailomtp/TelephonyManagerStub.java b/java/com/android/voicemailomtp/TelephonyManagerStub.java
new file mode 100644
index 000000000..e2e5dacdb
--- /dev/null
+++ b/java/com/android/voicemailomtp/TelephonyManagerStub.java
@@ -0,0 +1,80 @@
+/*
+ * 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.voicemailomtp;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.os.PersistableBundle;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.CarrierConfigManager;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import java.lang.reflect.Method;
+
+/**
+ * Temporary stub for public APIs that should be added into telephony manager.
+ *
+ * <p>TODO(b/32637799) remove this.
+ */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class TelephonyManagerStub {
+
+ private static final String TAG = "TelephonyManagerStub";
+
+ public static void showVoicemailNotification(int voicemailCount) {
+
+ }
+
+ /**
+ * Dismisses the message waiting (voicemail) indicator.
+ *
+ * @param subId the subscription id we should dismiss the notification for.
+ */
+ public static void clearMwiIndicator(int subId) {
+
+ }
+
+ public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId,
+ boolean enabled) {
+
+ }
+
+ public static int getSubIdForPhoneAccount(Context context, PhoneAccount phoneAccount) {
+ // Hidden
+ TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
+ try {
+ Method method = TelephonyManager.class
+ .getMethod("getSubIdForPhoneAccount", PhoneAccount.class);
+ return (int) method.invoke(telephonyManager, phoneAccount);
+ } catch (Exception e) {
+ VvmLog.e(TAG, "reflection call to getSubIdForPhoneAccount failed:", e);
+ }
+ return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
+ }
+
+ public static String getNetworkSpecifierForPhoneAccountHandle(Context context,
+ PhoneAccountHandle phoneAccountHandle) {
+ return String.valueOf(SubscriptionManager.getDefaultDataSubscriptionId());
+ }
+
+ public static PersistableBundle getCarrirConfigForPhoneAccountHandle(Context context,
+ PhoneAccountHandle phoneAccountHandle) {
+ return context.getSystemService(CarrierConfigManager.class).getConfig();
+ }
+}
diff --git a/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java b/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java
new file mode 100644
index 000000000..ab13d36ad
--- /dev/null
+++ b/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java
@@ -0,0 +1,154 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.res.Resources;
+import android.os.PersistableBundle;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import com.android.voicemailomtp.utils.XmlUtils;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.Map.Entry;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Load and caches telephony vvm config from res/xml/vvm_config.xml
+ */
+public class TelephonyVvmConfigManager {
+
+ private static final String TAG = "TelephonyVvmCfgMgr";
+
+ private static final boolean USE_DEBUG_CONFIG = false;
+
+ private static final String TAG_PERSISTABLEMAP = "pbundle_as_map";
+
+ static final String KEY_MCCMNC = "mccmnc";
+
+ private static Map<String, PersistableBundle> sCachedConfigs;
+
+ private final Map<String, PersistableBundle> mConfigs;
+
+ public TelephonyVvmConfigManager(Resources resources) {
+ if (sCachedConfigs == null) {
+ sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config));
+ }
+ mConfigs = sCachedConfigs;
+ }
+
+ @VisibleForTesting
+ TelephonyVvmConfigManager(XmlPullParser parser) {
+ mConfigs = loadConfigs(parser);
+ }
+
+ @Nullable
+ public PersistableBundle getConfig(String mccMnc) {
+ if (USE_DEBUG_CONFIG) {
+ return mConfigs.get("TEST");
+ }
+ return mConfigs.get(mccMnc);
+ }
+
+ private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) {
+ Map<String, PersistableBundle> configs = new ArrayMap<>();
+ try {
+ ArrayList list = readBundleList(parser);
+ for (Object object : list) {
+ if (!(object instanceof PersistableBundle)) {
+ throw new IllegalArgumentException("PersistableBundle expected, got " + object);
+ }
+ PersistableBundle bundle = (PersistableBundle) object;
+ String[] mccMncs = bundle.getStringArray(KEY_MCCMNC);
+ if (mccMncs == null) {
+ throw new IllegalArgumentException("MCCMNC is null");
+ }
+ for (String mccMnc : mccMncs) {
+ configs.put(mccMnc, bundle);
+ }
+ }
+ } catch (IOException | XmlPullParserException e) {
+ throw new RuntimeException(e);
+ }
+ return configs;
+ }
+
+ @Nullable
+ public static ArrayList readBundleList(XmlPullParser in) throws IOException,
+ XmlPullParserException {
+ final int outerDepth = in.getDepth();
+ int event;
+ while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
+ (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+ if (event == XmlPullParser.START_TAG) {
+ final String startTag = in.getName();
+ final String[] tagName = new String[1];
+ in.next();
+ return XmlUtils.readThisListXml(in, startTag, tagName,
+ new MyReadMapCallback(), false);
+ }
+ }
+ return null;
+ }
+
+ public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException,
+ XmlPullParserException {
+ final int outerDepth = in.getDepth();
+ final String startTag = in.getName();
+ final String[] tagName = new String[1];
+ int event;
+ while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
+ (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) {
+ if (event == XmlPullParser.START_TAG) {
+ ArrayMap<String, ?> map =
+ XmlUtils.readThisArrayMapXml(in, startTag, tagName,
+ new MyReadMapCallback());
+ PersistableBundle result = new PersistableBundle();
+ for (Entry<String, ?> entry : map.entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof Integer) {
+ result.putInt(entry.getKey(), (int) value);
+ } else if (value instanceof Boolean) {
+ result.putBoolean(entry.getKey(), (boolean) value);
+ } else if (value instanceof String) {
+ result.putString(entry.getKey(), (String) value);
+ } else if (value instanceof String[]) {
+ result.putStringArray(entry.getKey(), (String[]) value);
+ } else if (value instanceof PersistableBundle) {
+ result.putPersistableBundle(entry.getKey(), (PersistableBundle) value);
+ }
+ }
+ return result;
+ }
+ }
+ return PersistableBundle.EMPTY;
+ }
+
+ static class MyReadMapCallback implements XmlUtils.ReadMapCallback {
+
+ @Override
+ public Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+ throws XmlPullParserException, IOException {
+ if (TAG_PERSISTABLEMAP.equals(tag)) {
+ return restoreFromXml(in);
+ }
+ throw new XmlPullParserException("Unknown tag=" + tag);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/VisualVoicemailPreferences.java b/java/com/android/voicemailomtp/VisualVoicemailPreferences.java
new file mode 100644
index 000000000..5bc2c6951
--- /dev/null
+++ b/java/com/android/voicemailomtp/VisualVoicemailPreferences.java
@@ -0,0 +1,143 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import java.util.Set;
+
+/**
+ * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail
+ * source is tied 1:1 to a phone account, the phone account handle is used in the key for each
+ * voicemail source and the associated data.
+ */
+public class VisualVoicemailPreferences {
+
+ private static final String VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX =
+ "visual_voicemail_";
+
+ private final SharedPreferences mPreferences;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) {
+ mPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ mPhoneAccountHandle = phoneAccountHandle;
+ }
+
+ public class Editor {
+
+ private final SharedPreferences.Editor mEditor;
+
+ private Editor() {
+ mEditor = mPreferences.edit();
+ }
+
+ public void apply() {
+ mEditor.apply();
+ }
+
+ public Editor putBoolean(String key, boolean value) {
+ mEditor.putBoolean(getKey(key), value);
+ return this;
+ }
+
+ @NeededForTesting
+ public Editor putFloat(String key, float value) {
+ mEditor.putFloat(getKey(key), value);
+ return this;
+ }
+
+ public Editor putInt(String key, int value) {
+ mEditor.putInt(getKey(key), value);
+ return this;
+ }
+
+ @NeededForTesting
+ public Editor putLong(String key, long value) {
+ mEditor.putLong(getKey(key), value);
+ return this;
+ }
+
+ public Editor putString(String key, String value) {
+ mEditor.putString(getKey(key), value);
+ return this;
+ }
+
+ @NeededForTesting
+ public Editor putStringSet(String key, Set<String> value) {
+ mEditor.putStringSet(getKey(key), value);
+ return this;
+ }
+ }
+
+ public Editor edit() {
+ return new Editor();
+ }
+
+ public boolean getBoolean(String key, boolean defValue) {
+ return getValue(key, defValue);
+ }
+
+ @NeededForTesting
+ public float getFloat(String key, float defValue) {
+ return getValue(key, defValue);
+ }
+
+ public int getInt(String key, int defValue) {
+ return getValue(key, defValue);
+ }
+
+ @NeededForTesting
+ public long getLong(String key, long defValue) {
+ return getValue(key, defValue);
+ }
+
+ public String getString(String key, String defValue) {
+ return getValue(key, defValue);
+ }
+
+ @Nullable
+ public String getString(String key) {
+ return getValue(key, null);
+ }
+
+ @NeededForTesting
+ public Set<String> getStringSet(String key, Set<String> defValue) {
+ return getValue(key, defValue);
+ }
+
+ public boolean contains(String key) {
+ return mPreferences.contains(getKey(key));
+ }
+
+ private <T> T getValue(String key, T defValue) {
+ if (!contains(key)) {
+ return defValue;
+ }
+ Object object = mPreferences.getAll().get(getKey(key));
+ if (object == null) {
+ return defValue;
+ }
+ return (T) object;
+ }
+
+ private String getKey(String key) {
+ return VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX + key + "_" + mPhoneAccountHandle.getId();
+ }
+}
diff --git a/java/com/android/voicemailomtp/Voicemail.java b/java/com/android/voicemailomtp/Voicemail.java
new file mode 100644
index 000000000..9d8395142
--- /dev/null
+++ b/java/com/android/voicemailomtp/Voicemail.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.voicemailomtp;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+/**
+ * Represents a single voicemail stored in the voicemail content provider.
+ */
+public class Voicemail implements Parcelable {
+
+ private final Long mTimestamp;
+ private final String mNumber;
+ private final PhoneAccountHandle mPhoneAccount;
+ private final Long mId;
+ private final Long mDuration;
+ private final String mSource;
+ private final String mProviderData;
+ private final Uri mUri;
+ private final Boolean mIsRead;
+ private final Boolean mHasContent;
+ private final String mTranscription;
+
+ private Voicemail(Long timestamp, String number, PhoneAccountHandle phoneAccountHandle, Long id,
+ Long duration, String source, String providerData, Uri uri, Boolean isRead,
+ Boolean hasContent, String transcription) {
+ mTimestamp = timestamp;
+ mNumber = number;
+ mPhoneAccount = phoneAccountHandle;
+ mId = id;
+ mDuration = duration;
+ mSource = source;
+ mProviderData = providerData;
+ mUri = uri;
+ mIsRead = isRead;
+ mHasContent = hasContent;
+ mTranscription = transcription;
+ }
+
+ /**
+ * Create a {@link Builder} for a new {@link Voicemail} to be inserted. <p> The number and the
+ * timestamp are mandatory for insertion.
+ */
+ public static Builder createForInsertion(long timestamp, String number) {
+ return new Builder().setNumber(number).setTimestamp(timestamp);
+ }
+
+ /**
+ * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted). <p> The id and
+ * source data fields are mandatory for update - id is necessary for updating the database and
+ * source data is necessary for updating the server.
+ */
+ public static Builder createForUpdate(long id, String sourceData) {
+ return new Builder().setId(id).setSourceData(sourceData);
+ }
+
+ /**
+ * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link
+ * #createForInsertion(long, String)} method. <p> This class is <b>not thread safe</b>
+ */
+ public static class Builder {
+
+ private Long mBuilderTimestamp;
+ private String mBuilderNumber;
+ private PhoneAccountHandle mBuilderPhoneAccount;
+ private Long mBuilderId;
+ private Long mBuilderDuration;
+ private String mBuilderSourcePackage;
+ private String mBuilderSourceData;
+ private Uri mBuilderUri;
+ private Boolean mBuilderIsRead;
+ private boolean mBuilderHasContent;
+ private String mBuilderTranscription;
+
+ /**
+ * You should use the correct factory method to construct a builder.
+ */
+ private Builder() {
+ }
+
+ public Builder setNumber(String number) {
+ mBuilderNumber = number;
+ return this;
+ }
+
+ public Builder setTimestamp(long timestamp) {
+ mBuilderTimestamp = timestamp;
+ return this;
+ }
+
+ public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) {
+ mBuilderPhoneAccount = phoneAccount;
+ return this;
+ }
+
+ public Builder setId(long id) {
+ mBuilderId = id;
+ return this;
+ }
+
+ public Builder setDuration(long duration) {
+ mBuilderDuration = duration;
+ return this;
+ }
+
+ public Builder setSourcePackage(String sourcePackage) {
+ mBuilderSourcePackage = sourcePackage;
+ return this;
+ }
+
+ public Builder setSourceData(String sourceData) {
+ mBuilderSourceData = sourceData;
+ return this;
+ }
+
+ public Builder setUri(Uri uri) {
+ mBuilderUri = uri;
+ return this;
+ }
+
+ public Builder setIsRead(boolean isRead) {
+ mBuilderIsRead = isRead;
+ return this;
+ }
+
+ public Builder setHasContent(boolean hasContent) {
+ mBuilderHasContent = hasContent;
+ return this;
+ }
+
+ public Builder setTranscription(String transcription) {
+ mBuilderTranscription = transcription;
+ return this;
+ }
+
+ public Voicemail build() {
+ mBuilderId = mBuilderId == null ? -1 : mBuilderId;
+ mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp;
+ mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration;
+ mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead;
+ return new Voicemail(mBuilderTimestamp, mBuilderNumber, mBuilderPhoneAccount,
+ mBuilderId, mBuilderDuration, mBuilderSourcePackage, mBuilderSourceData,
+ mBuilderUri, mBuilderIsRead, mBuilderHasContent, mBuilderTranscription);
+ }
+ }
+
+ /**
+ * The identifier of the voicemail in the content provider. <p> This may be missing in the case
+ * of a new {@link Voicemail} that we plan to insert into the content provider, since until it
+ * has been inserted we don't know what id it should have. If none is specified, we return -1.
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * The number of the person leaving the voicemail, empty string if unknown, null if not set.
+ */
+ public String getNumber() {
+ return mNumber;
+ }
+
+ /**
+ * The phone account associated with the voicemail, null if not set.
+ */
+ public PhoneAccountHandle getPhoneAccount() {
+ return mPhoneAccount;
+ }
+
+ /**
+ * The timestamp the voicemail was received, in millis since the epoch, zero if not set.
+ */
+ public long getTimestampMillis() {
+ return mTimestamp;
+ }
+
+ /**
+ * Gets the duration of the voicemail in millis, or zero if the field is not set.
+ */
+ public long getDuration() {
+ return mDuration;
+ }
+
+ /**
+ * Returns the package name of the source that added this voicemail, or null if this field is
+ * not set.
+ */
+ public String getSourcePackage() {
+ return mSource;
+ }
+
+ /**
+ * Returns the application-specific data type stored with the voicemail, or null if this field
+ * is not set. <p> Source data is typically used as an identifier to uniquely identify the
+ * voicemail against the voicemail server. This is likely to be something like the IMAP UID, or
+ * some other server-generated identifying string.
+ */
+ public String getSourceData() {
+ return mProviderData;
+ }
+
+ /**
+ * Gets the Uri that can be used to refer to this voicemail, and to make it play. <p> Returns
+ * null if we don't know the Uri.
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Tells us if the voicemail message has been marked as read. <p> Always returns false if this
+ * field has not been set, i.e. if hasRead() returns false.
+ */
+ public boolean isRead() {
+ return mIsRead;
+ }
+
+ /**
+ * Tells us if there is content stored at the Uri.
+ */
+ public boolean hasContent() {
+ return mHasContent;
+ }
+
+ /**
+ * Returns the text transcription of this voicemail, or null if this field is not set.
+ */
+ public String getTranscription() {
+ return mTranscription;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(mTimestamp);
+ writeCharSequence(dest, mNumber);
+ if (mPhoneAccount == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mPhoneAccount.writeToParcel(dest, flags);
+ }
+ dest.writeLong(mId);
+ dest.writeLong(mDuration);
+ writeCharSequence(dest, mSource);
+ writeCharSequence(dest, mProviderData);
+ if (mUri == null) {
+ dest.writeInt(0);
+ } else {
+ dest.writeInt(1);
+ mUri.writeToParcel(dest, flags);
+ }
+ if (mIsRead) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ }
+ if (mHasContent) {
+ dest.writeInt(1);
+ } else {
+ dest.writeInt(0);
+ }
+ writeCharSequence(dest, mTranscription);
+ }
+
+ public static final Creator<Voicemail> CREATOR
+ = new Creator<Voicemail>() {
+ @Override
+ public Voicemail createFromParcel(Parcel in) {
+ return new Voicemail(in);
+ }
+
+ @Override
+ public Voicemail[] newArray(int size) {
+ return new Voicemail[size];
+ }
+ };
+
+ private Voicemail(Parcel in) {
+ mTimestamp = in.readLong();
+ mNumber = (String) readCharSequence(in);
+ if (in.readInt() > 0) {
+ mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in);
+ } else {
+ mPhoneAccount = null;
+ }
+ mId = in.readLong();
+ mDuration = in.readLong();
+ mSource = (String) readCharSequence(in);
+ mProviderData = (String) readCharSequence(in);
+ if (in.readInt() > 0) {
+ mUri = Uri.CREATOR.createFromParcel(in);
+ } else {
+ mUri = null;
+ }
+ mIsRead = in.readInt() > 0 ? true : false;
+ mHasContent = in.readInt() > 0 ? true : false;
+ mTranscription = (String) readCharSequence(in);
+ }
+
+ private static CharSequence readCharSequence(Parcel in) {
+ return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+ }
+
+ public static void writeCharSequence(Parcel dest, CharSequence val) {
+ TextUtils.writeToParcel(val, dest, 0);
+ }
+}
diff --git a/java/com/android/voicemailomtp/VoicemailStatus.java b/java/com/android/voicemailomtp/VoicemailStatus.java
new file mode 100644
index 000000000..63007932e
--- /dev/null
+++ b/java/com/android/voicemailomtp/VoicemailStatus.java
@@ -0,0 +1,158 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+
+public class VoicemailStatus {
+
+ private static final String TAG = "VvmStatus";
+
+ public static class Editor {
+
+ private final Context mContext;
+ @Nullable
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ private ContentValues mValues = new ContentValues();
+
+ private Editor(Context context, PhoneAccountHandle phoneAccountHandle) {
+ mContext = context;
+ mPhoneAccountHandle = phoneAccountHandle;
+ if (mPhoneAccountHandle == null) {
+ VvmLog.w(TAG, "VoicemailStatus.Editor created with null phone account, status will"
+ + " not be written");
+ }
+ }
+
+ @Nullable
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+
+ public Editor setType(String type) {
+ mValues.put(Status.SOURCE_TYPE, type);
+ return this;
+ }
+
+ public Editor setConfigurationState(int configurationState) {
+ mValues.put(Status.CONFIGURATION_STATE, configurationState);
+ return this;
+ }
+
+ public Editor setDataChannelState(int dataChannelState) {
+ mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState);
+ return this;
+ }
+
+ public Editor setNotificationChannelState(int notificationChannelState) {
+ mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState);
+ return this;
+ }
+
+ public Editor setQuota(int occupied, int total) {
+ if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE
+ && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) {
+ return this;
+ }
+
+ mValues.put(Status.QUOTA_OCCUPIED, occupied);
+ mValues.put(Status.QUOTA_TOTAL, total);
+ return this;
+ }
+
+ /**
+ * Apply the changes to the {@link VoicemailStatus} {@link #Editor}.
+ *
+ * @return {@code true} if the changes were successfully applied, {@code false} otherwise.
+ */
+ public boolean apply() {
+ if (mPhoneAccountHandle == null) {
+ return false;
+ }
+ mValues.put(Status.PHONE_ACCOUNT_COMPONENT_NAME,
+ mPhoneAccountHandle.getComponentName().flattenToString());
+ mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId());
+ ContentResolver contentResolver = mContext.getContentResolver();
+ Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+ try {
+ contentResolver.insert(statusUri, mValues);
+ } catch (IllegalArgumentException iae) {
+ VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae);
+ mValues.clear();
+ return false;
+ }
+ mValues.clear();
+ return true;
+ }
+
+ public ContentValues getValues() {
+ return mValues;
+ }
+ }
+
+ /**
+ * A voicemail status editor that the decision of whether to actually write to the database can
+ * be deferred. This object will be passed around as a usual {@link Editor}, but {@link
+ * #apply()} doesn't do anything. If later the creator of this object decides any status changes
+ * written to it should be committed, {@link #deferredApply()} should be called.
+ */
+ public static class DeferredEditor extends Editor {
+
+ private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) {
+ super(context, phoneAccountHandle);
+ }
+
+ @Override
+ public boolean apply() {
+ // Do nothing
+ return true;
+ }
+
+ public void deferredApply() {
+ super.apply();
+ }
+ }
+
+ public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return new Editor(context, phoneAccountHandle);
+ }
+
+ /**
+ * Reset the status to the "disabled" state, which the UI should not show anything for this
+ * phoneAccountHandle.
+ */
+ public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) {
+ edit(context, phoneAccountHandle)
+ .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED)
+ .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION)
+ .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION)
+ .apply();
+ }
+
+ public static DeferredEditor deferredEdit(Context context,
+ PhoneAccountHandle phoneAccountHandle) {
+ return new DeferredEditor(context, phoneAccountHandle);
+ }
+}
diff --git a/java/com/android/voicemailomtp/VvmLog.java b/java/com/android/voicemailomtp/VvmLog.java
new file mode 100644
index 000000000..2add66a53
--- /dev/null
+++ b/java/com/android/voicemailomtp/VvmLog.java
@@ -0,0 +1,179 @@
+/*
+ * 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.voicemailomtp;
+
+import android.util.Log;
+import com.android.voicemailomtp.utils.IndentingPrintWriter;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayDeque;
+import java.util.Calendar;
+import java.util.Deque;
+import java.util.Iterator;
+
+/**
+ * Helper methods for adding to OMTP visual voicemail local logs.
+ */
+public class VvmLog {
+
+ private static final int MAX_OMTP_VVM_LOGS = 100;
+
+ private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS);
+
+ public static void log(String tag, String log) {
+ sLocalLog.log(tag + ": " + log);
+ }
+
+ public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) {
+ IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, " ");
+ indentingPrintWriter.increaseIndent();
+ sLocalLog.dump(fd, indentingPrintWriter, args);
+ indentingPrintWriter.decreaseIndent();
+ }
+
+ public static int e(String tag, String log) {
+ log(tag, log);
+ return Log.e(tag, log);
+ }
+
+ public static int e(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ return Log.e(tag, log, e);
+ }
+
+ public static int w(String tag, String log) {
+ log(tag, log);
+ return Log.w(tag, log);
+ }
+
+ public static int w(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ return Log.w(tag, log, e);
+ }
+
+ public static int i(String tag, String log) {
+ log(tag, log);
+ return Log.i(tag, log);
+ }
+
+ public static int i(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ return Log.i(tag, log, e);
+ }
+
+ public static int d(String tag, String log) {
+ log(tag, log);
+ return Log.d(tag, log);
+ }
+
+ public static int d(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ return Log.d(tag, log, e);
+ }
+
+ public static int v(String tag, String log) {
+ log(tag, log);
+ return Log.v(tag, log);
+ }
+
+ public static int v(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ return Log.v(tag, log, e);
+ }
+
+ public static int wtf(String tag, String log) {
+ log(tag, log);
+ return Log.wtf(tag, log);
+ }
+
+ public static int wtf(String tag, String log, Throwable e) {
+ log(tag, log + " " + e);
+ return Log.wtf(tag, log, e);
+ }
+
+ /**
+ * Redact personally identifiable information for production users. If we are running in verbose
+ * mode, return the original string, otherwise return a SHA-1 hash of the input string.
+ */
+ public static String pii(Object pii) {
+ if (pii == null) {
+ return String.valueOf(pii);
+ }
+ return "[PII]";
+ }
+
+ public static class LocalLog {
+
+ private final Deque<String> mLog;
+ private final int mMaxLines;
+
+ public LocalLog(int maxLines) {
+ mMaxLines = Math.max(0, maxLines);
+ mLog = new ArrayDeque<>(mMaxLines);
+ }
+
+ public void log(String msg) {
+ if (mMaxLines <= 0) {
+ return;
+ }
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(System.currentTimeMillis());
+ append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg));
+ }
+
+ private synchronized void append(String logLine) {
+ while (mLog.size() >= mMaxLines) {
+ mLog.remove();
+ }
+ mLog.add(logLine);
+ }
+
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ Iterator<String> itr = mLog.iterator();
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+
+ public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ Iterator<String> itr = mLog.descendingIterator();
+ while (itr.hasNext()) {
+ pw.println(itr.next());
+ }
+ }
+
+ public static class ReadOnlyLocalLog {
+
+ private final LocalLog mLog;
+
+ ReadOnlyLocalLog(LocalLog log) {
+ mLog = log;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.dump(fd, pw, args);
+ }
+
+ public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mLog.reverseDump(fd, pw, args);
+ }
+ }
+
+ public ReadOnlyLocalLog readOnlyLocalLog() {
+ return new ReadOnlyLocalLog(this);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java b/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java
new file mode 100644
index 000000000..7d9eee9f8
--- /dev/null
+++ b/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java
@@ -0,0 +1,70 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
+
+import java.util.Set;
+
+/**
+ * When a new package is installed, check if it matches any of the vvm carrier apps of the currently
+ * enabled dialer vvm sources.
+ */
+public class VvmPackageInstallReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "VvmPkgInstallReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getData() == null) {
+ return;
+ }
+
+ String packageName = intent.getData().getSchemeSpecificPart();
+ if (packageName == null) {
+ return;
+ }
+
+ OmtpVvmSourceManager vvmSourceManager = OmtpVvmSourceManager.getInstance(context);
+ Set<PhoneAccountHandle> phoneAccounts = vvmSourceManager.getOmtpVvmSources();
+ for (PhoneAccountHandle phoneAccount : phoneAccounts) {
+ if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) {
+ // Skip the check if this voicemail source's setting is overridden by the user.
+ continue;
+ }
+
+ OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper(
+ context, phoneAccount);
+ if (carrierConfigHelper.getCarrierVvmPackageNames() == null) {
+ continue;
+ }
+ if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) {
+ // Force deactivate the client. The user can re-enable it in the settings.
+ // There are no need to update the settings for deactivation. At this point, if the
+ // default value is used it should be false because a carrier package is present.
+ VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client");
+ OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
+ carrierConfigHelper.startDeactivation();
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/VvmPhoneStateListener.java b/java/com/android/voicemailomtp/VvmPhoneStateListener.java
new file mode 100644
index 000000000..1a3013d1f
--- /dev/null
+++ b/java/com/android/voicemailomtp/VvmPhoneStateListener.java
@@ -0,0 +1,103 @@
+/*
+ * 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.voicemailomtp;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+
+import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
+import com.android.voicemailomtp.sync.OmtpVvmSyncService;
+import com.android.voicemailomtp.sync.SyncTask;
+import com.android.voicemailomtp.sync.VoicemailStatusQueryHelper;
+
+/**
+ * Check if service is lost and indicate this in the voicemail status.
+ */
+public class VvmPhoneStateListener extends PhoneStateListener {
+
+ private static final String TAG = "VvmPhoneStateListener";
+
+ private PhoneAccountHandle mPhoneAccount;
+ private Context mContext;
+ private int mPreviousState = -1;
+
+ public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) {
+ // TODO: b/32637799 too much trouble to call super constructor through reflection,
+ // just use non-phoneAccountHandle version for now.
+ super();
+ mContext = context;
+ mPhoneAccount = accountHandle;
+ }
+
+ @Override
+ public void onServiceStateChanged(ServiceState serviceState) {
+ if (mPhoneAccount == null) {
+ VvmLog.e(TAG, "onServiceStateChanged on phoneAccount " + mPhoneAccount
+ + " with invalid phoneAccountHandle, ignoring");
+ return;
+ }
+
+ int state = serviceState.getState();
+ if (state == mPreviousState || (state != ServiceState.STATE_IN_SERVICE
+ && mPreviousState != ServiceState.STATE_IN_SERVICE)) {
+ // Only interested in state changes or transitioning into or out of "in service".
+ // Otherwise just quit.
+ mPreviousState = state;
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount);
+
+ if (state == ServiceState.STATE_IN_SERVICE) {
+ VoicemailStatusQueryHelper voicemailStatusQueryHelper =
+ new VoicemailStatusQueryHelper(mContext);
+ if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) {
+ if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) {
+ VvmLog
+ .v(TAG, "Notifications channel is active for " + mPhoneAccount);
+ helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount),
+ OmtpEvents.NOTIFICATION_IN_SERVICE);
+ }
+ }
+
+ if (OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) {
+ VvmLog
+ .v(TAG, "Signal returned: requesting resync for " + mPhoneAccount);
+ // If the source is already registered, run a full sync in case something was missed
+ // while signal was down.
+ SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC);
+ } else {
+ VvmLog.v(TAG,
+ "Signal returned: reattempting activation for " + mPhoneAccount);
+ // Otherwise initiate an activation because this means that an OMTP source was
+ // recognized but either the activation text was not successfully sent or a response
+ // was not received.
+ helper.startActivation();
+ }
+ } else {
+ VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount);
+
+ if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) {
+ return;
+ }
+ helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount),
+ OmtpEvents.NOTIFICATION_SERVICE_LOST);
+ }
+ mPreviousState = state;
+ }
+}
diff --git a/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java
new file mode 100644
index 000000000..85fea80d7
--- /dev/null
+++ b/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java
@@ -0,0 +1,219 @@
+/*
+ * 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.voicemailomtp.fetch;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.os.BuildCompat;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.imap.ImapHelper;
+import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
+import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
+import com.android.voicemailomtp.sync.VvmNetworkRequestCallback;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class FetchVoicemailReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "FetchVoicemailReceiver";
+
+ final static String[] PROJECTION = new String[]{
+ Voicemails.SOURCE_DATA, // 0
+ Voicemails.PHONE_ACCOUNT_ID, // 1
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2
+ };
+
+ public static final int SOURCE_DATA = 0;
+ public static final int PHONE_ACCOUNT_ID = 1;
+ public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2;
+
+ // Number of retries
+ private static final int NETWORK_RETRY_COUNT = 3;
+
+ private ContentResolver mContentResolver;
+ private Uri mUri;
+ private VvmNetworkRequestCallback mNetworkCallback;
+ private Context mContext;
+ private String mUid;
+ private PhoneAccountHandle mPhoneAccount;
+ private int mRetryCount = NETWORK_RETRY_COUNT;
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) {
+ VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received");
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mUri = intent.getData();
+
+ if (mUri == null) {
+ VvmLog.w(TAG,
+ VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data");
+ return;
+ }
+
+ if (!context.getPackageName().equals(
+ mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) {
+ // Ignore if the fetch request is for a voicemail not from this package.
+ VvmLog.e(TAG,
+ "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName());
+ return;
+ }
+
+ Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null);
+ if (cursor == null) {
+ VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null");
+ return;
+ }
+ try {
+ if (cursor.moveToFirst()) {
+ mUid = cursor.getString(SOURCE_DATA);
+ String accountId = cursor.getString(PHONE_ACCOUNT_ID);
+ if (TextUtils.isEmpty(accountId)) {
+ TelephonyManager telephonyManager = (TelephonyManager)
+ context.getSystemService(Context.TELEPHONY_SERVICE);
+ accountId = telephonyManager.getSimSerialNumber();
+
+ if (TextUtils.isEmpty(accountId)) {
+ VvmLog.e(TAG, "Account null and no default sim found.");
+ return;
+ }
+ }
+
+ mPhoneAccount = new PhoneAccountHandle(
+ ComponentName.unflattenFromString(
+ cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)),
+ cursor.getString(PHONE_ACCOUNT_ID));
+ if (!OmtpVvmSourceManager.getInstance(context)
+ .isVvmSourceRegistered(mPhoneAccount)) {
+ mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount);
+ if (mPhoneAccount == null) {
+ VvmLog.w(TAG, "Account not registered - cannot retrieve message.");
+ return;
+ }
+ VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle");
+ }
+ VvmLog.i(TAG, "Requesting network to fetch voicemail");
+ mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context,
+ mPhoneAccount);
+ mNetworkCallback.requestNetwork();
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ }
+
+ /**
+ * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat.
+ * This method attempts to search the account from the old database in registered sources using
+ * the old format. There's a chance of M phone account collisions on multi-SIM devices, but
+ * visual voicemail is not supported on M multi-SIM.
+ */
+ @Nullable
+ private static PhoneAccountHandle getAccountFromMarshmallowAccount(Context context,
+ PhoneAccountHandle oldAccount) {
+ if (!BuildCompat.isAtLeastN()) {
+ return null;
+ }
+ for (PhoneAccountHandle handle : OmtpVvmSourceManager.getInstance(context)
+ .getOmtpVvmSources()) {
+ if (getIccSerialNumberFromFullIccSerialNumber(handle.getId())
+ .equals(oldAccount.getId())) {
+ return handle;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after.
+ * getIccSerialNumber() stops at the first hex char.
+ */
+ @NonNull
+ private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) {
+ for(int i =0;i<id.length();i++){
+ if(!Character.isDigit(id.charAt(i))){
+ return id.substring(0,i);
+ }
+ }
+ return id;
+ }
+
+ private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+ public fetchVoicemailNetworkRequestCallback(Context context,
+ PhoneAccountHandle phoneAccount) {
+ super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount));
+ }
+
+ @Override
+ public void onAvailable(final Network network) {
+ super.onAvailable(network);
+ fetchVoicemail(network, getVoicemailStatusEditor());
+ }
+ }
+
+ private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) {
+ Executor executor = Executors.newCachedThreadPool();
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ while (mRetryCount > 0) {
+ VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount);
+ try (ImapHelper imapHelper = new ImapHelper(mContext, mPhoneAccount,
+ network, status)) {
+ boolean success = imapHelper.fetchVoicemailPayload(
+ new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount),
+ mUid);
+ if (!success && mRetryCount > 0) {
+ VvmLog.i(TAG, "fetch voicemail failed, retrying");
+ mRetryCount--;
+ } else {
+ return;
+ }
+ } catch (InitializingException e) {
+ VvmLog.w(TAG, "Can't retrieve Imap credentials ", e);
+ return;
+ }
+ }
+ } finally {
+ if (mNetworkCallback != null) {
+ mNetworkCallback.releaseNetwork();
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java
new file mode 100644
index 000000000..7479c4c4e
--- /dev/null
+++ b/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java
@@ -0,0 +1,101 @@
+/*
+ * 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.voicemailomtp.fetch;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import com.android.voicemailomtp.R;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.imap.VoicemailPayload;
+import java.io.IOException;
+import java.io.OutputStream;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * Callback for when a voicemail payload is fetched. It copies the returned stream to the data
+ * file corresponding to the voicemail.
+ */
+public class VoicemailFetchedCallback {
+ private static final String TAG = "VoicemailFetchedCallback";
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final Uri mUri;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ public VoicemailFetchedCallback(Context context, Uri uri,
+ PhoneAccountHandle phoneAccountHandle) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mUri = uri;
+ mPhoneAccountHandle = phoneAccountHandle;
+ }
+
+ /**
+ * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit
+ * of the voicemail to "1".
+ *
+ * @param voicemailPayload The object containing the content data for the voicemail
+ */
+ public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) {
+ if (voicemailPayload == null) {
+ VvmLog.i(TAG, "Payload not found, message has unsupported format");
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.TRANSCRIPTION,
+ mContext.getString(R.string.vvm_unsupported_message_format,
+ mContext.getSystemService(TelecomManager.class)
+ .getVoiceMailNumber(mPhoneAccountHandle)));
+ updateVoicemail(values);
+ return;
+ }
+
+ VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri));
+ OutputStream outputStream = null;
+
+ try {
+ outputStream = mContentResolver.openOutputStream(mUri);
+ byte[] inputBytes = voicemailPayload.getBytes();
+ if (inputBytes != null) {
+ outputStream.write(inputBytes);
+ }
+ } catch (IOException e) {
+ VvmLog.w(TAG, String.format("File not found for %s", mUri));
+ return;
+ } finally {
+ IOUtils.closeQuietly(outputStream);
+ }
+
+ // Update mime_type & has_content after we are done with file update.
+ ContentValues values = new ContentValues();
+ values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType());
+ values.put(Voicemails.HAS_CONTENT, true);
+ updateVoicemail(values);
+ }
+
+ private void updateVoicemail(ContentValues values) {
+ int updatedCount = mContentResolver.update(mUri, values, null, null);
+ if (updatedCount != 1) {
+ VvmLog
+ .e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/imap/ImapHelper.java b/java/com/android/voicemailomtp/imap/ImapHelper.java
new file mode 100644
index 000000000..b2a40fb64
--- /dev/null
+++ b/java/com/android/voicemailomtp/imap/ImapHelper.java
@@ -0,0 +1,711 @@
+/*
+ * 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.voicemailomtp.imap;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkInfo;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.util.Base64;
+
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpConstants.ChangePinResult;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VisualVoicemailPreferences;
+import com.android.voicemailomtp.Voicemail;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.fetch.VoicemailFetchedCallback;
+import com.android.voicemailomtp.mail.Address;
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.BodyPart;
+import com.android.voicemailomtp.mail.FetchProfile;
+import com.android.voicemailomtp.mail.Flag;
+import com.android.voicemailomtp.mail.Message;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.Multipart;
+import com.android.voicemailomtp.mail.TempDirectory;
+import com.android.voicemailomtp.mail.internet.MimeMessage;
+import com.android.voicemailomtp.mail.store.ImapConnection;
+import com.android.voicemailomtp.mail.store.ImapFolder;
+import com.android.voicemailomtp.mail.store.ImapStore;
+import com.android.voicemailomtp.mail.store.imap.ImapConstants;
+import com.android.voicemailomtp.mail.store.imap.ImapResponse;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+import com.android.voicemailomtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback;
+
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import org.apache.commons.io.IOUtils;
+
+/**
+ * A helper interface to abstract commands sent across IMAP interface for a given account.
+ */
+public class ImapHelper implements Closeable {
+
+ private static final String TAG = "ImapHelper";
+
+ private ImapFolder mFolder;
+ private ImapStore mImapStore;
+
+ private final Context mContext;
+ private final PhoneAccountHandle mPhoneAccount;
+ private final Network mNetwork;
+ private final VoicemailStatus.Editor mStatus;
+
+ VisualVoicemailPreferences mPrefs;
+ private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_";
+ private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_";
+
+ private int mQuotaOccupied;
+ private int mQuotaTotal;
+
+ private final OmtpVvmCarrierConfigHelper mConfig;
+
+ public class InitializingException extends Exception {
+
+ public InitializingException(String message) {
+ super(message);
+ }
+ }
+
+ public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network,
+ VoicemailStatus.Editor status)
+ throws InitializingException {
+ this(context,
+ new OmtpVvmCarrierConfigHelper(
+ context,
+ phoneAccount),
+ phoneAccount,
+ network,
+ status);
+ }
+
+ public ImapHelper(Context context, OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle phoneAccount, Network network, VoicemailStatus.Editor status)
+ throws InitializingException {
+ mContext = context;
+ mPhoneAccount = phoneAccount;
+ mNetwork = network;
+ mStatus = status;
+ mConfig = config;
+ mPrefs = new VisualVoicemailPreferences(context,
+ phoneAccount);
+
+ try {
+ TempDirectory.setTempDirectory(context);
+
+ String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null);
+ String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null);
+ String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null);
+ int port = Integer.parseInt(
+ mPrefs.getString(OmtpConstants.IMAP_PORT, null));
+ int auth = ImapStore.FLAG_NONE;
+
+ int sslPort = mConfig.getSslPort();
+ if (sslPort != 0) {
+ port = sslPort;
+ auth = ImapStore.FLAG_SSL;
+ }
+
+ mImapStore = new ImapStore(
+ context, this, username, password, port, serverName, auth, network);
+ } catch (NumberFormatException e) {
+ handleEvent(OmtpEvents.DATA_INVALID_PORT);
+ LogUtils.w(TAG, "Could not parse port number");
+ throw new InitializingException("cannot initialize ImapHelper:" + e.toString());
+ }
+
+ mQuotaOccupied = mPrefs
+ .getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+ mQuotaTotal = mPrefs
+ .getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE);
+ }
+
+ @Override
+ public void close() {
+ mImapStore.closeConnection();
+ }
+
+ public boolean isRoaming() {
+ ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork);
+ if (info == null) {
+ return false;
+ }
+ return info.isRoaming();
+ }
+
+ public OmtpVvmCarrierConfigHelper getConfig() {
+ return mConfig;
+ }
+
+ public ImapConnection connect() {
+ return mImapStore.getConnection();
+ }
+
+ /**
+ * The caller thread will block until the method returns.
+ */
+ public boolean markMessagesAsRead(List<Voicemail> voicemails) {
+ return setFlags(voicemails, Flag.SEEN);
+ }
+
+ /**
+ * The caller thread will block until the method returns.
+ */
+ public boolean markMessagesAsDeleted(List<Voicemail> voicemails) {
+ return setFlags(voicemails, Flag.DELETED);
+ }
+
+ public void handleEvent(OmtpEvents event) {
+ mConfig.handleEvent(mStatus, event);
+ }
+
+ /**
+ * Set flags on the server for a given set of voicemails.
+ *
+ * @param voicemails The voicemails to set flags for.
+ * @param flags The flags to set on the voicemails.
+ * @return {@code true} if the operation completes successfully, {@code false} otherwise.
+ */
+ private boolean setFlags(List<Voicemail> voicemails, String... flags) {
+ if (voicemails.size() == 0) {
+ return false;
+ }
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder != null) {
+ mFolder.setFlags(convertToImapMessages(voicemails), flags, true);
+ return true;
+ }
+ return false;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging exception");
+ return false;
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ /**
+ * Fetch a list of voicemails from the server.
+ *
+ * @return A list of voicemail objects containing data about voicemails stored on the server.
+ */
+ public List<Voicemail> fetchAllVoicemails() {
+ List<Voicemail> result = new ArrayList<Voicemail>();
+ Message[] messages;
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return null;
+ }
+
+ // This method retrieves lightweight messages containing only the uid of the message.
+ messages = mFolder.getMessages(null);
+
+ for (Message message : messages) {
+ // Get the voicemail details (message structure).
+ MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+ if (messageStructureWrapper != null) {
+ result.add(getVoicemailFromMessageStructure(messageStructureWrapper));
+ }
+ }
+ return result;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ return null;
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ /**
+ * Extract voicemail details from the message structure. Also fetch transcription if a
+ * transcription exists.
+ */
+ private Voicemail getVoicemailFromMessageStructure(
+ MessageStructureWrapper messageStructureWrapper) throws MessagingException {
+ Message messageDetails = messageStructureWrapper.messageStructure;
+
+ TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+ if (messageStructureWrapper.transcriptionBodyPart != null) {
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+ mFolder.fetch(new Message[]{messageDetails}, fetchProfile, listener);
+ }
+
+ // Found an audio attachment, this is a valid voicemail.
+ long time = messageDetails.getSentDate().getTime();
+ String number = getNumber(messageDetails.getFrom());
+ boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN);
+ return Voicemail.createForInsertion(time, number)
+ .setPhoneAccount(mPhoneAccount)
+ .setSourcePackage(mContext.getPackageName())
+ .setSourceData(messageDetails.getUid())
+ .setIsRead(isRead)
+ .setTranscription(listener.getVoicemailTranscription())
+ .build();
+ }
+
+ /**
+ * The "from" field of a visual voicemail IMAP message is the number of the caller who left the
+ * message. Extract this number from the list of "from" addresses.
+ *
+ * @param fromAddresses A list of addresses that comprise the "from" line.
+ * @return The number of the voicemail sender.
+ */
+ private String getNumber(Address[] fromAddresses) {
+ if (fromAddresses != null && fromAddresses.length > 0) {
+ if (fromAddresses.length != 1) {
+ LogUtils.w(TAG, "More than one from addresses found. Using the first one.");
+ }
+ String sender = fromAddresses[0].getAddress();
+ int atPos = sender.indexOf('@');
+ if (atPos != -1) {
+ // Strip domain part of the address.
+ sender = sender.substring(0, atPos);
+ }
+ return sender;
+ }
+ return null;
+ }
+
+ /**
+ * Fetches the structure of the given message and returns a wrapper containing the message
+ * structure and the transcription structure (if applicable).
+ *
+ * @throws MessagingException if fetching the structure of the message fails
+ */
+ private MessageStructureWrapper fetchMessageStructure(Message message)
+ throws MessagingException {
+ LogUtils.d(TAG, "Fetching message structure for " + message.getUid());
+
+ MessageStructureFetchedListener listener = new MessageStructureFetchedListener();
+
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE,
+ FetchProfile.Item.STRUCTURE));
+
+ // The IMAP folder fetch method will call "messageRetrieved" on the listener when the
+ // message is successfully retrieved.
+ mFolder.fetch(new Message[]{message}, fetchProfile, listener);
+ return listener.getMessageStructure();
+ }
+
+ public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) {
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return false;
+ }
+ Message message = mFolder.getMessage(uid);
+ if (message == null) {
+ return false;
+ }
+ VoicemailPayload voicemailPayload = fetchVoicemailPayload(message);
+ callback.setVoicemailContent(voicemailPayload);
+ return true;
+ } catch (MessagingException e) {
+ } finally {
+ closeImapFolder();
+ }
+ return false;
+ }
+
+ /**
+ * Fetches the body of the given message and returns the parsed voicemail payload.
+ *
+ * @throws MessagingException if fetching the body of the message fails
+ */
+ private VoicemailPayload fetchVoicemailPayload(Message message)
+ throws MessagingException {
+ LogUtils.d(TAG, "Fetching message body for " + message.getUid());
+
+ MessageBodyFetchedListener listener = new MessageBodyFetchedListener();
+
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.add(FetchProfile.Item.BODY);
+
+ mFolder.fetch(new Message[]{message}, fetchProfile, listener);
+ return listener.getVoicemailPayload();
+ }
+
+ public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) {
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return false;
+ }
+
+ Message message = mFolder.getMessage(uid);
+ if (message == null) {
+ return false;
+ }
+
+ MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message);
+ if (messageStructureWrapper != null) {
+ TranscriptionFetchedListener listener = new TranscriptionFetchedListener();
+ if (messageStructureWrapper.transcriptionBodyPart != null) {
+ FetchProfile fetchProfile = new FetchProfile();
+ fetchProfile.add(messageStructureWrapper.transcriptionBodyPart);
+
+ // This method is called synchronously so the transcription will be populated
+ // in the listener once the next method is called.
+ mFolder.fetch(new Message[]{message}, fetchProfile, listener);
+ callback.setVoicemailTranscription(listener.getVoicemailTranscription());
+ }
+ }
+ return true;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ return false;
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+
+ @ChangePinResult
+ public int changePin(String oldPin, String newPin)
+ throws MessagingException {
+ ImapConnection connection = mImapStore.getConnection();
+ try {
+ String command = getConfig().getProtocol()
+ .getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT);
+ connection.sendCommand(
+ String.format(Locale.US, command, newPin, oldPin), true);
+ return getChangePinResultFromImapResponse(connection.readResponse());
+ } catch (IOException ioe) {
+ VvmLog.e(TAG, "changePin: ", ioe);
+ return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+ } finally {
+ connection.destroyResponses();
+ }
+ }
+
+ public void changeVoicemailTuiLanguage(String languageCode)
+ throws MessagingException {
+ ImapConnection connection = mImapStore.getConnection();
+ try {
+ String command = getConfig().getProtocol()
+ .getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT);
+ connection.sendCommand(
+ String.format(Locale.US, command, languageCode), true);
+ } catch (IOException ioe) {
+ LogUtils.e(TAG, ioe.toString());
+ } finally {
+ connection.destroyResponses();
+ }
+ }
+
+ public void closeNewUserTutorial() throws MessagingException {
+ ImapConnection connection = mImapStore.getConnection();
+ try {
+ String command = getConfig().getProtocol()
+ .getCommand(OmtpConstants.IMAP_CLOSE_NUT);
+ connection.executeSimpleCommand(command, false);
+ } catch (IOException ioe) {
+ throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString());
+ } finally {
+ connection.destroyResponses();
+ }
+ }
+
+ @ChangePinResult
+ private static int getChangePinResultFromImapResponse(ImapResponse response)
+ throws MessagingException {
+ if (!response.isTagged()) {
+ throw new MessagingException(MessagingException.SERVER_ERROR,
+ "tagged response expected");
+ }
+ if (!response.isOk()) {
+ String message = response.getStringOrEmpty(1).getString();
+ LogUtils.d(TAG, "change PIN failed: " + message);
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_TOO_SHORT;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_TOO_LONG;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_TOO_WEAK;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_MISMATCH;
+ }
+ if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) {
+ return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER;
+ }
+ return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR;
+ }
+ LogUtils.d(TAG, "change PIN succeeded");
+ return OmtpConstants.CHANGE_PIN_SUCCESS;
+ }
+
+ public void updateQuota() {
+ try {
+ mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE);
+ if (mFolder == null) {
+ // This means we were unable to successfully open the folder.
+ return;
+ }
+ updateQuota(mFolder);
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ } finally {
+ closeImapFolder();
+ }
+ }
+
+ private void updateQuota(ImapFolder folder) throws MessagingException {
+ setQuota(folder.getQuota());
+ }
+
+ private void setQuota(ImapFolder.Quota quota) {
+ if (quota == null) {
+ return;
+ }
+ if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) {
+ VvmLog.v(TAG, "Quota hasn't changed");
+ return;
+ }
+ mQuotaOccupied = quota.occupied;
+ mQuotaTotal = quota.total;
+ VoicemailStatus.edit(mContext, mPhoneAccount)
+ .setQuota(mQuotaOccupied, mQuotaTotal)
+ .apply();
+ mPrefs.edit()
+ .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied)
+ .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal)
+ .apply();
+ VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal);
+ }
+
+ /**
+ * A wrapper to hold a message with its header details and the structure for transcriptions (so
+ * they can be fetched in the future).
+ */
+ public class MessageStructureWrapper {
+
+ public Message messageStructure;
+ public BodyPart transcriptionBodyPart;
+
+ public MessageStructureWrapper() {
+ }
+ }
+
+ /**
+ * Listener for the message structure being fetched.
+ */
+ private final class MessageStructureFetchedListener
+ implements ImapFolder.MessageRetrievalListener {
+
+ private MessageStructureWrapper mMessageStructure;
+
+ public MessageStructureFetchedListener() {
+ }
+
+ public MessageStructureWrapper getMessageStructure() {
+ return mMessageStructure;
+ }
+
+ @Override
+ public void messageRetrieved(Message message) {
+ LogUtils.d(TAG, "Fetched message structure for " + message.getUid());
+ LogUtils.d(TAG, "Message retrieved: " + message);
+ try {
+ mMessageStructure = getMessageOrNull(message);
+ if (mMessageStructure == null) {
+ LogUtils.d(TAG, "This voicemail does not have an attachment...");
+ return;
+ }
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ closeImapFolder();
+ }
+ }
+
+ /**
+ * Check if this IMAP message is a valid voicemail and whether it contains a transcription.
+ *
+ * @param message The IMAP message.
+ * @return The MessageStructureWrapper object corresponding to an IMAP message and
+ * transcription.
+ */
+ private MessageStructureWrapper getMessageOrNull(Message message)
+ throws MessagingException {
+ if (!message.getMimeType().startsWith("multipart/")) {
+ LogUtils.w(TAG, "Ignored non multi-part message");
+ return null;
+ }
+
+ MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper();
+
+ Multipart multipart = (Multipart) message.getBody();
+ for (int i = 0; i < multipart.getCount(); ++i) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+ LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType);
+
+ if (bodyPartMimeType.startsWith("audio/")) {
+ messageStructureWrapper.messageStructure = message;
+ } else if (bodyPartMimeType.startsWith("text/")) {
+ messageStructureWrapper.transcriptionBodyPart = bodyPart;
+ } else {
+ VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType);
+ }
+ }
+
+ if (messageStructureWrapper.messageStructure != null) {
+ return messageStructureWrapper;
+ }
+
+ // No attachment found, this is not a voicemail.
+ return null;
+ }
+ }
+
+ /**
+ * Listener for the message body being fetched.
+ */
+ private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener {
+
+ private VoicemailPayload mVoicemailPayload;
+
+ /**
+ * Returns the fetch voicemail payload.
+ */
+ public VoicemailPayload getVoicemailPayload() {
+ return mVoicemailPayload;
+ }
+
+ @Override
+ public void messageRetrieved(Message message) {
+ LogUtils.d(TAG, "Fetched message body for " + message.getUid());
+ LogUtils.d(TAG, "Message retrieved: " + message);
+ try {
+ mVoicemailPayload = getVoicemailPayloadFromMessage(message);
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, "Messaging Exception:", e);
+ } catch (IOException e) {
+ LogUtils.e(TAG, "IO Exception:", e);
+ }
+ }
+
+ private VoicemailPayload getVoicemailPayloadFromMessage(Message message)
+ throws MessagingException, IOException {
+ Multipart multipart = (Multipart) message.getBody();
+ List<String> mimeTypes = new ArrayList<>();
+ for (int i = 0; i < multipart.getCount(); ++i) {
+ BodyPart bodyPart = multipart.getBodyPart(i);
+ String bodyPartMimeType = bodyPart.getMimeType().toLowerCase();
+ mimeTypes.add(bodyPartMimeType);
+ if (bodyPartMimeType.startsWith("audio/")) {
+ byte[] bytes = getDataFromBody(bodyPart.getBody());
+ LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length));
+ return new VoicemailPayload(bodyPartMimeType, bytes);
+ }
+ }
+ LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes);
+ return null;
+ }
+ }
+
+ /**
+ * Listener for the transcription being fetched.
+ */
+ private final class TranscriptionFetchedListener implements
+ ImapFolder.MessageRetrievalListener {
+
+ private String mVoicemailTranscription;
+
+ /**
+ * Returns the fetched voicemail transcription.
+ */
+ public String getVoicemailTranscription() {
+ return mVoicemailTranscription;
+ }
+
+ @Override
+ public void messageRetrieved(Message message) {
+ LogUtils.d(TAG, "Fetched transcription for " + message.getUid());
+ try {
+ mVoicemailTranscription = new String(getDataFromBody(message.getBody()));
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, "Messaging Exception:", e);
+ } catch (IOException e) {
+ LogUtils.e(TAG, "IO Exception:", e);
+ }
+ }
+ }
+
+ private ImapFolder openImapFolder(String modeReadWrite) {
+ try {
+ if (mImapStore == null) {
+ return null;
+ }
+ ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX);
+ folder.open(modeReadWrite);
+ return folder;
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ }
+ return null;
+ }
+
+ private Message[] convertToImapMessages(List<Voicemail> voicemails) {
+ Message[] messages = new Message[voicemails.size()];
+ for (int i = 0; i < voicemails.size(); ++i) {
+ messages[i] = new MimeMessage();
+ messages[i].setUid(voicemails.get(i).getSourceData());
+ }
+ return messages;
+ }
+
+ private void closeImapFolder() {
+ if (mFolder != null) {
+ mFolder.close(true);
+ }
+ }
+
+ private byte[] getDataFromBody(Body body) throws IOException, MessagingException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ BufferedOutputStream bufferedOut = new BufferedOutputStream(out);
+ try {
+ body.writeTo(bufferedOut);
+ return Base64.decode(out.toByteArray(), Base64.DEFAULT);
+ } finally {
+ IOUtils.closeQuietly(bufferedOut);
+ IOUtils.closeQuietly(out);
+ }
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/imap/VoicemailPayload.java b/java/com/android/voicemailomtp/imap/VoicemailPayload.java
new file mode 100644
index 000000000..04c69dea5
--- /dev/null
+++ b/java/com/android/voicemailomtp/imap/VoicemailPayload.java
@@ -0,0 +1,38 @@
+/*
+ * 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.voicemailomtp.imap;
+
+/**
+ * The payload for a voicemail, usually audio data.
+ */
+public class VoicemailPayload {
+ private final String mMimeType;
+ private final byte[] mBytes;
+
+ public VoicemailPayload(String mimeType, byte[] bytes) {
+ mMimeType = mimeType;
+ mBytes = bytes;
+ }
+
+ public byte[] getBytes() {
+ return mBytes;
+ }
+
+ public String getMimeType() {
+ return mMimeType;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Address.java b/java/com/android/voicemailomtp/mail/Address.java
new file mode 100644
index 000000000..ed3f44c03
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Address.java
@@ -0,0 +1,541 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.VisibleForTesting;
+import android.text.Html;
+import android.text.TextUtils;
+import android.text.util.Rfc822Token;
+import android.text.util.Rfc822Tokenizer;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+import java.util.ArrayList;
+import java.util.regex.Pattern;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+/**
+ * This class represent email address.
+ *
+ * RFC822 email address may have following format.
+ * "name" <address> (comment)
+ * "name" <address>
+ * name <address>
+ * address
+ * Name and comment part should be MIME/base64 encoded in header if necessary.
+ *
+ */
+public class Address implements Parcelable {
+ public static final String ADDRESS_DELIMETER = ",";
+ /**
+ * Address part, in the form local_part@domain_part. No surrounding angle brackets.
+ */
+ private String mAddress;
+
+ /**
+ * Name part. No surrounding double quote, and no MIME/base64 encoding.
+ * This must be null if Address has no name part.
+ */
+ private String mPersonal;
+
+ /**
+ * When personal is set, it will return the first token of the personal
+ * string. Otherwise, it will return the e-mail address up to the '@' sign.
+ */
+ private String mSimplifiedName;
+
+ // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$'
+ private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$");
+ // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$'
+ private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$");
+ // Regex that matches escaped character '\\([\\"])'
+ private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])");
+
+ // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved.
+ // TODO: Fix this to better constrain comments.
+ /** Regex for the local part of an email address. */
+ private static final String LOCAL_PART = "[^@]+";
+ /** Regex for each part of the domain part, i.e. the thing between the dots. */
+ private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+";
+ /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */
+ private static final String DOMAIN_PART =
+ "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART;
+
+ /** Pattern to check if an email address is valid. */
+ private static final Pattern EMAIL_ADDRESS =
+ Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z");
+
+ private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0];
+
+ // delimiters are chars that do not appear in an email address, used by fromHeader
+ private static final char LIST_DELIMITER_EMAIL = '\1';
+ private static final char LIST_DELIMITER_PERSONAL = '\2';
+
+ private static final String LOG_TAG = "Email Address";
+
+ @VisibleForTesting
+ public Address(String address) {
+ setAddress(address);
+ }
+
+ public Address(String address, String personal) {
+ setPersonal(personal);
+ setAddress(address);
+ }
+
+ /**
+ * Returns a simplified string for this e-mail address.
+ * When a name is known, it will return the first token of that name. Otherwise, it will
+ * return the e-mail address up to the '@' sign.
+ */
+ public String getSimplifiedName() {
+ if (mSimplifiedName == null) {
+ if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) {
+ int atSign = mAddress.indexOf('@');
+ mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : "";
+ } else if (!TextUtils.isEmpty(mPersonal)) {
+
+ // TODO: use Contacts' NameSplitter for more reliable first-name extraction
+
+ int end = mPersonal.indexOf(' ');
+ while (end > 0 && mPersonal.charAt(end - 1) == ',') {
+ end--;
+ }
+ mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end);
+
+ } else {
+ LogUtils.w(LOG_TAG, "Unable to get a simplified name");
+ mSimplifiedName = "";
+ }
+ }
+ return mSimplifiedName;
+ }
+
+ public static synchronized Address getEmailAddress(String rawAddress) {
+ if (TextUtils.isEmpty(rawAddress)) {
+ return null;
+ }
+ String name, address;
+ final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress);
+ if (tokens.length > 0) {
+ final String tokenizedName = tokens[0].getName();
+ name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString()
+ : "";
+ address = Html.fromHtml(tokens[0].getAddress()).toString();
+ } else {
+ name = "";
+ address = rawAddress == null ?
+ "" : Html.fromHtml(rawAddress).toString();
+ }
+ return new Address(address, name);
+ }
+
+ public String getAddress() {
+ return mAddress;
+ }
+
+ public void setAddress(String address) {
+ mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1");
+ }
+
+ /**
+ * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding.
+ *
+ * @return Name part of email address. Returns null if it is omitted.
+ */
+ public String getPersonal() {
+ return mPersonal;
+ }
+
+ /**
+ * Set personal part from UTF-16 string. Optional surrounding double quote will be removed.
+ * It will be also unquoted and MIME/base64 decoded.
+ *
+ * @param personal name part of email address as UTF-16 string. Null is acceptable.
+ */
+ public void setPersonal(String personal) {
+ mPersonal = decodeAddressPersonal(personal);
+ }
+
+ /**
+ * Decodes name from UTF-16 string. Optional surrounding double quote will be removed.
+ * It will be also unquoted and MIME/base64 decoded.
+ *
+ * @param personal name part of email address as UTF-16 string. Null is acceptable.
+ */
+ public static String decodeAddressPersonal(String personal) {
+ if (personal != null) {
+ personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1");
+ personal = UNQUOTE.matcher(personal).replaceAll("$1");
+ personal = DecoderUtil.decodeEncodedWords(personal);
+ if (personal.length() == 0) {
+ personal = null;
+ }
+ }
+ return personal;
+ }
+
+ /**
+ * This method is used to check that all the addresses that the user
+ * entered in a list (e.g. To:) are valid, so that none is dropped.
+ */
+ @VisibleForTesting
+ public static boolean isAllValid(String addressList) {
+ // This code mimics the parse() method below.
+ // I don't know how to better avoid the code-duplication.
+ if (addressList != null && addressList.length() > 0) {
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address) && !isValidAddress(address)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Parse a comma-delimited list of addresses in RFC822 format and return an
+ * array of Address objects.
+ *
+ * @param addressList Address list in comma-delimited string.
+ * @return An array of 0 or more Addresses.
+ */
+ public static Address[] parse(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList);
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ for (int i = 0, length = tokens.length; i < length; ++i) {
+ Rfc822Token token = tokens[i];
+ String address = token.getAddress();
+ if (!TextUtils.isEmpty(address)) {
+ if (isValidAddress(address)) {
+ String name = token.getName();
+ if (TextUtils.isEmpty(name)) {
+ name = null;
+ }
+ addresses.add(new Address(address, name));
+ }
+ }
+ }
+ return addresses.toArray(new Address[addresses.size()]);
+ }
+
+ /**
+ * Checks whether a string email address is valid.
+ * E.g. name@domain.com is valid.
+ */
+ @VisibleForTesting
+ static boolean isValidAddress(final String address) {
+ return EMAIL_ADDRESS.matcher(address).find();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof Address) {
+ // It seems that the spec says that the "user" part is case-sensitive,
+ // while the domain part in case-insesitive.
+ // So foo@yahoo.com and Foo@yahoo.com are different.
+ // This may seem non-intuitive from the user POV, so we
+ // may re-consider it if it creates UI trouble.
+ // A problem case is "replyAll" sending to both
+ // a@b.c and to A@b.c, which turn out to be the same on the server.
+ // Leave unchanged for now (i.e. case-sensitive).
+ return getAddress().equals(((Address) o).getAddress());
+ }
+ return super.equals(o);
+ }
+
+ @Override
+ public int hashCode() {
+ return getAddress().hashCode();
+ }
+
+ /**
+ * Get human readable address string.
+ * Do not use this for email header.
+ *
+ * @return Human readable address string. Not quoted and not encoded.
+ */
+ @Override
+ public String toString() {
+ if (mPersonal != null && !mPersonal.equals(mAddress)) {
+ if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
+ return ensureQuotedString(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mPersonal + " <" + mAddress + ">";
+ }
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Ensures that the given string starts and ends with the double quote character. The string is
+ * not modified in any way except to add the double quote character to start and end if it's not
+ * already there.
+ *
+ * sample -> "sample"
+ * "sample" -> "sample"
+ * ""sample"" -> "sample"
+ * "sample"" -> "sample"
+ * sa"mp"le -> "sa"mp"le"
+ * "sa"mp"le" -> "sa"mp"le"
+ * (empty string) -> ""
+ * " -> ""
+ */
+ private static String ensureQuotedString(String s) {
+ if (s == null) {
+ return null;
+ }
+ if (!s.matches("^\".*\"$")) {
+ return "\"" + s + "\"";
+ } else {
+ return s;
+ }
+ }
+
+ /**
+ * Get human readable comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return Human readable comma-delimited address string.
+ */
+ @VisibleForTesting
+ public static String toString(Address[] addresses) {
+ return toString(addresses, ADDRESS_DELIMETER);
+ }
+
+ /**
+ * Get human readable address strings joined with the specified separator.
+ *
+ * @param addresses Address array
+ * @param separator Separator
+ * @return Human readable comma-delimited address string.
+ */
+ public static String toString(Address[] addresses, String separator) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toString();
+ }
+ StringBuilder sb = new StringBuilder(addresses[0].toString());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(separator);
+ // TODO: investigate why this .trim() is needed.
+ sb.append(addresses[i].toString().trim());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get RFC822/MIME compatible address string.
+ *
+ * @return RFC822/MIME compatible address string.
+ * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary.
+ */
+ public String toHeader() {
+ if (mPersonal != null) {
+ return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">";
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Get RFC822/MIME compatible comma-delimited address string.
+ *
+ * @param addresses Address array
+ * @return RFC822/MIME compatible comma-delimited address string.
+ * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary.
+ */
+ public static String toHeader(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toHeader();
+ }
+ StringBuilder sb = new StringBuilder(addresses[0].toHeader());
+ for (int i = 1; i < addresses.length; i++) {
+ // We need space character to be able to fold line.
+ sb.append(", ");
+ sb.append(addresses[i].toHeader());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get Human friendly address string.
+ *
+ * @return the personal part of this Address, or the address part if the
+ * personal part is not available
+ */
+ @VisibleForTesting
+ public String toFriendly() {
+ if (mPersonal != null && mPersonal.length() > 0) {
+ return mPersonal;
+ } else {
+ return mAddress;
+ }
+ }
+
+ /**
+ * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for
+ * details on the per-address conversion).
+ *
+ * @param addresses Array of Address[] values
+ * @return A comma-delimited string listing all of the addresses supplied. Null if source
+ * was null or empty.
+ */
+ @VisibleForTesting
+ public static String toFriendly(Address[] addresses) {
+ if (addresses == null || addresses.length == 0) {
+ return null;
+ }
+ if (addresses.length == 1) {
+ return addresses[0].toFriendly();
+ }
+ StringBuilder sb = new StringBuilder(addresses[0].toFriendly());
+ for (int i = 1; i < addresses.length; i++) {
+ sb.append(", ");
+ sb.append(addresses[i].toFriendly());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)).
+ */
+ @VisibleForTesting
+ public static String fromHeaderToString(String addressList) {
+ return toString(fromHeader(addressList));
+ }
+
+ /**
+ * Returns exactly the same result as Address.toHeader(Address.parse(addressList)).
+ */
+ @VisibleForTesting
+ public static String parseToHeader(String addressList) {
+ return Address.toHeader(Address.parse(addressList));
+ }
+
+ /**
+ * Returns null if the addressList has 0 addresses, otherwise returns the first address.
+ * The same as Address.fromHeader(addressList)[0] for non-empty list.
+ * This is an utility method that offers some performance optimization opportunities.
+ */
+ @VisibleForTesting
+ public static Address firstAddress(String addressList) {
+ Address[] array = fromHeader(addressList);
+ return array.length > 0 ? array[0] : null;
+ }
+
+ /**
+ * This method exists to convert an address list formatted in a deprecated legacy format to the
+ * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy
+ * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format.
+ *
+ * This implementation is brute-force, and could be replaced with a more efficient version
+ * if desired.
+ */
+ public static String reformatToHeader(String addressList) {
+ return toHeader(fromHeader(addressList));
+ }
+
+ /**
+ * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format
+ * @return array of addresses parsed from <code>addressList</code>
+ */
+ @VisibleForTesting
+ public static Address[] fromHeader(String addressList) {
+ if (addressList == null || addressList.length() == 0) {
+ return EMPTY_ADDRESS_ARRAY;
+ }
+ // IF we're CSV, just parse
+ if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) &&
+ (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) {
+ return Address.parse(addressList);
+ }
+ // Otherwise, do backward-compatible unpack
+ ArrayList<Address> addresses = new ArrayList<Address>();
+ int length = addressList.length();
+ int pairStartIndex = 0;
+ int pairEndIndex;
+
+ /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL
+ is used, not for every email address; i.e. not for every iteration of the while().
+ This reduces the theoretical complexity from quadratic to linear,
+ and provides some speed-up in practice by removing redundant scans of the string.
+ */
+ int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL);
+
+ while (pairStartIndex < length) {
+ pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex);
+ if (pairEndIndex == -1) {
+ pairEndIndex = length;
+ }
+ Address address;
+ if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair,
+ // so don't use personal, and don't update addressEndIndex
+ address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null);
+ } else {
+ address = new Address(addressList.substring(pairStartIndex, addressEndIndex),
+ addressList.substring(addressEndIndex + 1, pairEndIndex));
+ // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL
+ addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1);
+ }
+ addresses.add(address);
+ pairStartIndex = pairEndIndex + 1;
+ }
+ return addresses.toArray(new Address[addresses.size()]);
+ }
+
+ public static final Creator<Address> CREATOR = new Creator<Address>() {
+ @Override
+ public Address createFromParcel(Parcel parcel) {
+ return new Address(parcel);
+ }
+
+ @Override
+ public Address[] newArray(int size) {
+ return new Address[size];
+ }
+ };
+
+ public Address(Parcel in) {
+ setPersonal(in.readString());
+ setAddress(in.readString());
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mPersonal);
+ out.writeString(mAddress);
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java b/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java
new file mode 100644
index 000000000..995d5d348
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java
@@ -0,0 +1,33 @@
+/*
+ * 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.voicemailomtp.mail;
+
+public class AuthenticationFailedException extends MessagingException {
+ public static final long serialVersionUID = -1;
+
+ public AuthenticationFailedException(String message) {
+ super(MessagingException.AUTHENTICATION_FAILED, message);
+ }
+
+ public AuthenticationFailedException(int exceptionType, String message) {
+ super(exceptionType, message);
+ }
+
+ public AuthenticationFailedException(String message, Throwable throwable) {
+ super(MessagingException.AUTHENTICATION_FAILED, message, throwable);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Base64Body.java b/java/com/android/voicemailomtp/mail/Base64Body.java
new file mode 100644
index 000000000..6e1deff44
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Base64Body.java
@@ -0,0 +1,62 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public class Base64Body implements Body {
+ private final InputStream mSource;
+ // Because we consume the input stream, we can only write out once
+ private boolean mAlreadyWritten;
+
+ public Base64Body(InputStream source) {
+ mSource = source;
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return mSource;
+ }
+
+ /**
+ * This method consumes the input stream, so can only be called once
+ * @param out Stream to write to
+ * @throws IllegalStateException If called more than once
+ * @throws IOException
+ * @throws MessagingException
+ */
+ @Override
+ public void writeTo(OutputStream out)
+ throws IllegalStateException, IOException, MessagingException {
+ if (mAlreadyWritten) {
+ throw new IllegalStateException("Base64Body can only be written once");
+ }
+ mAlreadyWritten = true;
+ try {
+ final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT);
+ IOUtils.copyLarge(mSource, b64out);
+ } finally {
+ mSource.close();
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/Body.java b/java/com/android/voicemailomtp/mail/Body.java
new file mode 100644
index 000000000..393e1823c
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Body.java
@@ -0,0 +1,25 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public interface Body {
+ public InputStream getInputStream() throws MessagingException;
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/java/com/android/voicemailomtp/mail/BodyPart.java b/java/com/android/voicemailomtp/mail/BodyPart.java
new file mode 100644
index 000000000..62390a43e
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/BodyPart.java
@@ -0,0 +1,24 @@
+/*
+ * 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.voicemailomtp.mail;
+
+public abstract class BodyPart implements Part {
+ protected Multipart mParent;
+
+ public Multipart getParent() {
+ return mParent;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/CertificateValidationException.java b/java/com/android/voicemailomtp/mail/CertificateValidationException.java
new file mode 100644
index 000000000..8ebe5480b
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/CertificateValidationException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.voicemailomtp.mail;
+
+public class CertificateValidationException extends MessagingException {
+ public static final long serialVersionUID = -1;
+
+ public CertificateValidationException(String message) {
+ super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message);
+ }
+
+ public CertificateValidationException(String message, Throwable throwable) {
+ super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/FetchProfile.java b/java/com/android/voicemailomtp/mail/FetchProfile.java
new file mode 100644
index 000000000..d050692cc
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/FetchProfile.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.voicemailomtp.mail;
+
+import java.util.ArrayList;
+
+/**
+ * <pre>
+ * A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
+ * FetchProfile can contain the following objects:
+ * FetchProfile.Item: Described below.
+ * Message: Indicates that the body of the entire message should be fetched.
+ * Synonymous with FetchProfile.Item.BODY.
+ * Part: Indicates that the given Part should be fetched. The provider
+ * is expected have previously created the given BodyPart and stored
+ * any information it needs to download the content.
+ * </pre>
+ */
+public class FetchProfile extends ArrayList<Fetchable> {
+ /**
+ * Default items available for pre-fetching. It should be expected that any
+ * item fetched by using these items could potentially include all of the
+ * previous items.
+ */
+ public enum Item implements Fetchable {
+ /**
+ * Download the flags of the message.
+ */
+ FLAGS,
+
+ /**
+ * Download the envelope of the message. This should include at minimum
+ * the size and the following headers: date, subject, from, content-type, to, cc
+ */
+ ENVELOPE,
+
+ /**
+ * Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
+ * and may map to other providers.
+ * The provider should, if possible, fill in a properly formatted MIME structure in
+ * the message without actually downloading any message data. If the provider is not
+ * capable of this operation it should specifically set the body of the message to null
+ * so that upper levels can detect that a full body download is needed.
+ */
+ STRUCTURE,
+
+ /**
+ * A sane portion of the entire message, cut off at a provider determined limit.
+ * This should generally be around 50kB.
+ */
+ BODY_SANE,
+
+ /**
+ * The entire message.
+ */
+ BODY,
+ }
+
+ /**
+ * @return the first {@link Part} in this collection, or null if it doesn't contain
+ * {@link Part}.
+ */
+ public Part getFirstPart() {
+ for (Fetchable o : this) {
+ if (o instanceof Part) {
+ return (Part) o;
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/Fetchable.java b/java/com/android/voicemailomtp/mail/Fetchable.java
new file mode 100644
index 000000000..1d8d0005b
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Fetchable.java
@@ -0,0 +1,23 @@
+/*
+ * 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.voicemailomtp.mail;
+
+/**
+ * Interface for classes that can be added to {@link FetchProfile}.
+ * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}.
+ */
+public interface Fetchable {
+}
diff --git a/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java b/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java
new file mode 100644
index 000000000..65655efd5
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java
@@ -0,0 +1,79 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that stops allowing reads after the given length has been read. This
+ * is used to allow a client to read directly from an underlying protocol stream without reading
+ * past where the protocol handler intended the client to read.
+ */
+public class FixedLengthInputStream extends InputStream {
+ private final InputStream mIn;
+ private final int mLength;
+ private int mCount;
+
+ public FixedLengthInputStream(InputStream in, int length) {
+ this.mIn = in;
+ this.mLength = length;
+ }
+
+ @Override
+ public int available() throws IOException {
+ return mLength - mCount;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (mCount < mLength) {
+ mCount++;
+ return mIn.read();
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ if (mCount < mLength) {
+ int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
+ if (d == -1) {
+ return -1;
+ } else {
+ mCount += d;
+ return d;
+ }
+ } else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Flag.java b/java/com/android/voicemailomtp/mail/Flag.java
new file mode 100644
index 000000000..a9f927099
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Flag.java
@@ -0,0 +1,29 @@
+/*
+ * 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.voicemailomtp.mail;
+
+/**
+ * Flags that can be applied to Messages.
+ */
+public class Flag {
+ // If adding new flags: ALL FLAGS MUST BE UPPER CASE.
+ public static final String DELETED = "deleted";
+ public static final String SEEN = "seen";
+ public static final String ANSWERED = "answered";
+ public static final String FLAGGED = "flagged";
+ public static final String DRAFT = "draft";
+ public static final String RECENT = "recent";
+}
diff --git a/java/com/android/voicemailomtp/mail/MailTransport.java b/java/com/android/voicemailomtp/mail/MailTransport.java
new file mode 100644
index 000000000..3bf851fd8
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/MailTransport.java
@@ -0,0 +1,344 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import android.content.Context;
+import android.net.Network;
+import android.support.annotation.VisibleForTesting;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.imap.ImapHelper;
+import com.android.voicemailomtp.mail.store.ImapStore;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Make connection and perform operations on mail server by reading and writing lines.
+ */
+public class MailTransport {
+ private static final String TAG = "MailTransport";
+
+ // TODO protected eventually
+ /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000;
+ /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000;
+
+ private static final HostnameVerifier HOSTNAME_VERIFIER =
+ HttpsURLConnection.getDefaultHostnameVerifier();
+
+ private final Context mContext;
+ private final ImapHelper mImapHelper;
+ private final Network mNetwork;
+ private final String mHost;
+ private final int mPort;
+ private Socket mSocket;
+ private BufferedInputStream mIn;
+ private BufferedOutputStream mOut;
+ private final int mFlags;
+ private SocketCreator mSocketCreator;
+ private InetSocketAddress mAddress;
+
+ public MailTransport(Context context, ImapHelper imapHelper, Network network, String address,
+ int port, int flags) {
+ mContext = context;
+ mImapHelper = imapHelper;
+ mNetwork = network;
+ mHost = address;
+ mPort = port;
+ mFlags = flags;
+ }
+
+ /**
+ * Returns a new transport, using the current transport as a model. The new transport is
+ * configured identically, but not opened or connected in any way.
+ */
+ @Override
+ public MailTransport clone() {
+ return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags);
+ }
+
+ public boolean canTrySslSecurity() {
+ return (mFlags & ImapStore.FLAG_SSL) != 0;
+ }
+
+ public boolean canTrustAllCertificates() {
+ return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0;
+ }
+
+ /**
+ * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt
+ * an SSL connection if indicated.
+ */
+ public void open() throws MessagingException {
+ LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort));
+
+ List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>();
+
+ if (mNetwork == null) {
+ socketAddresses.add(new InetSocketAddress(mHost, mPort));
+ } else {
+ try {
+ InetAddress[] inetAddresses = mNetwork.getAllByName(mHost);
+ if (inetAddresses.length == 0) {
+ throw new MessagingException(MessagingException.IOERROR,
+ "Host name " + mHost + "cannot be resolved on designated network");
+ }
+ for (int i = 0; i < inetAddresses.length; i++) {
+ socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort));
+ }
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK);
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+
+ boolean success = false;
+ while (socketAddresses.size() > 0) {
+ mSocket = createSocket();
+ try {
+ mAddress = socketAddresses.remove(0);
+ mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT);
+
+ if (canTrySslSecurity()) {
+ /*
+ SSLSocket cannot be created with a connection timeout, so instead of doing a
+ direct SSL connection, we connect with a normal connection and upgrade it into
+ SSL
+ */
+ reopenTls();
+ } else {
+ mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+ mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+ }
+ success = true;
+ return;
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ if (socketAddresses.size() == 0) {
+ // Only throw an error when there are no more sockets to try.
+ mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED);
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ } finally {
+ if (!success) {
+ try {
+ mSocket.close();
+ mSocket = null;
+ } catch (IOException ioe) {
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+
+ }
+ }
+ }
+ }
+
+ // For testing. We need something that can replace the behavior of "new Socket()"
+ @VisibleForTesting
+ interface SocketCreator {
+
+ Socket createSocket() throws MessagingException;
+ }
+
+ @VisibleForTesting
+ void setSocketCreator(SocketCreator creator) {
+ mSocketCreator = creator;
+ }
+
+ protected Socket createSocket() throws MessagingException {
+ if (mSocketCreator != null) {
+ return mSocketCreator.createSocket();
+ }
+
+ if (mNetwork == null) {
+ LogUtils.v(TAG, "createSocket: network not specified");
+ return new Socket();
+ }
+
+ try {
+ LogUtils.v(TAG, "createSocket: network specified");
+ return mNetwork.getSocketFactory().createSocket();
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+
+ /**
+ * Attempts to reopen a normal connection into a TLS connection.
+ */
+ public void reopenTls() throws MessagingException {
+ try {
+ LogUtils.d(TAG, "open: converting to TLS socket");
+ mSocket = HttpsURLConnection.getDefaultSSLSocketFactory()
+ .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true);
+ // After the socket connects to an SSL server, confirm that the hostname is as
+ // expected
+ if (!canTrustAllCertificates()) {
+ verifyHostname(mSocket, mHost);
+ }
+ mSocket.setSoTimeout(SOCKET_READ_TIMEOUT);
+ mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
+ mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
+
+ } catch (SSLException e) {
+ LogUtils.d(TAG, e.toString());
+ throw new CertificateValidationException(e.getMessage(), e);
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, ioe.toString());
+ throw new MessagingException(MessagingException.IOERROR, ioe.toString());
+ }
+ }
+
+ /**
+ * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this
+ * service but is not in the public API.
+ *
+ * Verify the hostname of the certificate used by the other end of a
+ * connected socket. It is harmless to call this method redundantly if the hostname has already
+ * been verified.
+ *
+ * <p>Wildcard certificates are allowed to verify any matching hostname,
+ * so "foo.bar.example.com" is verified if the peer has a certificate
+ * for "*.example.com".
+ *
+ * @param socket An SSL socket which has been connected to a server
+ * @param hostname The expected hostname of the remote server
+ * @throws IOException if something goes wrong handshaking with the server
+ * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+ */
+ private void verifyHostname(Socket socket, String hostname) throws IOException {
+ // The code at the start of OpenSSLSocketImpl.startHandshake()
+ // ensures that the call is idempotent, so we can safely call it.
+ SSLSocket ssl = (SSLSocket) socket;
+ ssl.startHandshake();
+
+ SSLSession session = ssl.getSession();
+ if (session == null) {
+ mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION);
+ throw new SSLException("Cannot verify SSL socket without session");
+ }
+ // TODO: Instead of reporting the name of the server we think we're connecting to,
+ // we should be reporting the bad name in the certificate. Unfortunately this is buried
+ // in the verifier code and is not available in the verifier API, and extracting the
+ // CN & alts is beyond the scope of this patch.
+ if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
+ mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME);
+ throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: "
+ + session.getPeerPrincipal());
+ }
+ }
+
+ public boolean isOpen() {
+ return (mIn != null && mOut != null &&
+ mSocket != null && mSocket.isConnected() && !mSocket.isClosed());
+ }
+
+ /**
+ * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe.
+ */
+ public void close() {
+ try {
+ mIn.close();
+ } catch (Exception e) {
+ // May fail if the connection is already closed.
+ }
+ try {
+ mOut.close();
+ } catch (Exception e) {
+ // May fail if the connection is already closed.
+ }
+ try {
+ mSocket.close();
+ } catch (Exception e) {
+ // May fail if the connection is already closed.
+ }
+ mIn = null;
+ mOut = null;
+ mSocket = null;
+ }
+
+ public String getHost() {
+ return mHost;
+ }
+
+ public InputStream getInputStream() {
+ return mIn;
+ }
+
+ public OutputStream getOutputStream() {
+ return mOut;
+ }
+
+ /**
+ * Writes a single line to the server using \r\n termination.
+ */
+ public void writeLine(String s, String sensitiveReplacement) throws IOException {
+ if (sensitiveReplacement != null) {
+ LogUtils.d(TAG, ">>> " + sensitiveReplacement);
+ } else {
+ LogUtils.d(TAG, ">>> " + s);
+ }
+
+ OutputStream out = getOutputStream();
+ out.write(s.getBytes());
+ out.write('\r');
+ out.write('\n');
+ out.flush();
+ }
+
+ /**
+ * Reads a single line from the server, using either \r\n or \n as the delimiter. The
+ * delimiter char(s) are not included in the result.
+ */
+ public String readLine(boolean loggable) throws IOException {
+ StringBuffer sb = new StringBuffer();
+ InputStream in = getInputStream();
+ int d;
+ while ((d = in.read()) != -1) {
+ if (((char)d) == '\r') {
+ continue;
+ } else if (((char)d) == '\n') {
+ break;
+ } else {
+ sb.append((char)d);
+ }
+ }
+ if (d == -1) {
+ LogUtils.d(TAG, "End of stream reached while trying to read line.");
+ }
+ String ret = sb.toString();
+ if (loggable) {
+ LogUtils.d(TAG, "<<< " + ret);
+ }
+ return ret;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/MeetingInfo.java b/java/com/android/voicemailomtp/mail/MeetingInfo.java
new file mode 100644
index 000000000..0505bbf2c
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/MeetingInfo.java
@@ -0,0 +1,29 @@
+/*
+ * 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.voicemailomtp.mail;
+
+public class MeetingInfo {
+ // Predefined tags; others can be added
+ public static final String MEETING_DTSTAMP = "DTSTAMP";
+ public static final String MEETING_UID = "UID";
+ public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL";
+ public static final String MEETING_DTSTART = "DTSTART";
+ public static final String MEETING_DTEND = "DTEND";
+ public static final String MEETING_TITLE = "TITLE";
+ public static final String MEETING_LOCATION = "LOC";
+ public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE";
+ public static final String MEETING_ALL_DAY = "ALLDAY";
+}
diff --git a/java/com/android/voicemailomtp/mail/Message.java b/java/com/android/voicemailomtp/mail/Message.java
new file mode 100644
index 000000000..41555690f
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Message.java
@@ -0,0 +1,144 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import android.support.annotation.VisibleForTesting;
+import java.util.Date;
+import java.util.HashSet;
+
+public abstract class Message implements Part, Body {
+ public static final Message[] EMPTY_ARRAY = new Message[0];
+
+ public static final String RECIPIENT_TYPE_TO = "to";
+ public static final String RECIPIENT_TYPE_CC = "cc";
+ public static final String RECIPIENT_TYPE_BCC = "bcc";
+ public enum RecipientType {
+ TO, CC, BCC,
+ }
+
+ protected String mUid;
+
+ private HashSet<String> mFlags = null;
+
+ protected Date mInternalDate;
+
+ public String getUid() {
+ return mUid;
+ }
+
+ public void setUid(String uid) {
+ this.mUid = uid;
+ }
+
+ public abstract String getSubject() throws MessagingException;
+
+ public abstract void setSubject(String subject) throws MessagingException;
+
+ public Date getInternalDate() {
+ return mInternalDate;
+ }
+
+ public void setInternalDate(Date internalDate) {
+ this.mInternalDate = internalDate;
+ }
+
+ public abstract Date getReceivedDate() throws MessagingException;
+
+ public abstract Date getSentDate() throws MessagingException;
+
+ public abstract void setSentDate(Date sentDate) throws MessagingException;
+
+ public abstract Address[] getRecipients(String type) throws MessagingException;
+
+ public abstract void setRecipients(String type, Address[] addresses)
+ throws MessagingException;
+
+ public void setRecipient(String type, Address address) throws MessagingException {
+ setRecipients(type, new Address[] {
+ address
+ });
+ }
+
+ public abstract Address[] getFrom() throws MessagingException;
+
+ public abstract void setFrom(Address from) throws MessagingException;
+
+ public abstract Address[] getReplyTo() throws MessagingException;
+
+ public abstract void setReplyTo(Address[] from) throws MessagingException;
+
+ // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID");
+ public abstract void setMessageId(String messageId) throws MessagingException;
+ public abstract String getMessageId() throws MessagingException;
+
+ @Override
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getContentType().startsWith(mimeType);
+ }
+
+ private HashSet<String> getFlagSet() {
+ if (mFlags == null) {
+ mFlags = new HashSet<String>();
+ }
+ return mFlags;
+ }
+
+ /*
+ * TODO Refactor Flags at some point to be able to store user defined flags.
+ */
+ public String[] getFlags() {
+ return getFlagSet().toArray(new String[] {});
+ }
+
+ /**
+ * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses.
+ * Only used for testing.
+ */
+ @VisibleForTesting
+ private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException {
+ if (set) {
+ getFlagSet().add(flag);
+ } else {
+ getFlagSet().remove(flag);
+ }
+ }
+
+ public void setFlag(String flag, boolean set) throws MessagingException {
+ setFlagDirectlyForTest(flag, set);
+ }
+
+ /**
+ * This method calls setFlag(String, boolean)
+ * @param flags
+ * @param set
+ */
+ public void setFlags(String[] flags, boolean set) throws MessagingException {
+ for (String flag : flags) {
+ setFlag(flag, set);
+ }
+ }
+
+ public boolean isSet(String flag) {
+ return getFlagSet().contains(flag);
+ }
+
+ public abstract void saveChanges() throws MessagingException;
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + ':' + mUid;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/MessageDateComparator.java b/java/com/android/voicemailomtp/mail/MessageDateComparator.java
new file mode 100644
index 000000000..37071034a
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/MessageDateComparator.java
@@ -0,0 +1,34 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import java.util.Comparator;
+
+public class MessageDateComparator implements Comparator<Message> {
+ @Override
+ public int compare(Message o1, Message o2) {
+ try {
+ if (o1.getSentDate() == null) {
+ return 1;
+ } else if (o2.getSentDate() == null) {
+ return -1;
+ } else
+ return o2.getSentDate().compareTo(o1.getSentDate());
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/MessagingException.java b/java/com/android/voicemailomtp/mail/MessagingException.java
new file mode 100644
index 000000000..28550527f
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/MessagingException.java
@@ -0,0 +1,139 @@
+/*
+ * 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.voicemailomtp.mail;
+
+/**
+ * This exception is used for most types of failures that occur during server interactions.
+ *
+ * Data passed through this exception should be considered non-localized. Any strings should
+ * either be internal-only (for debugging) or server-generated.
+ *
+ * TO DO: Does it make sense to further collapse AuthenticationFailedException and
+ * CertificateValidationException and any others into this?
+ */
+public class MessagingException extends Exception {
+ public static final long serialVersionUID = -1;
+
+ public static final int NO_ERROR = -1;
+ /** Any exception that does not specify a specific issue */
+ public static final int UNSPECIFIED_EXCEPTION = 0;
+ /** Connection or IO errors */
+ public static final int IOERROR = 1;
+ /** The configuration requested TLS but the server did not support it. */
+ public static final int TLS_REQUIRED = 2;
+ /** Authentication is required but the server did not support it. */
+ public static final int AUTH_REQUIRED = 3;
+ /** General security failures */
+ public static final int GENERAL_SECURITY = 4;
+ /** Authentication failed */
+ public static final int AUTHENTICATION_FAILED = 5;
+ /** Attempt to create duplicate account */
+ public static final int DUPLICATE_ACCOUNT = 6;
+ /** Required security policies reported - advisory only */
+ public static final int SECURITY_POLICIES_REQUIRED = 7;
+ /** Required security policies not supported */
+ public static final int SECURITY_POLICIES_UNSUPPORTED = 8;
+ /** The protocol (or protocol version) isn't supported */
+ public static final int PROTOCOL_VERSION_UNSUPPORTED = 9;
+ /** The server's SSL certificate couldn't be validated */
+ public static final int CERTIFICATE_VALIDATION_ERROR = 10;
+ /** Authentication failed during autodiscover */
+ public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11;
+ /** Autodiscover completed with a result (non-error) */
+ public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12;
+ /** Ambiguous failure; server error or bad credentials */
+ public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13;
+ /** The server refused access */
+ public static final int ACCESS_DENIED = 14;
+ /** The server refused access */
+ public static final int ATTACHMENT_NOT_FOUND = 15;
+ /** A client SSL certificate is required for connections to the server */
+ public static final int CLIENT_CERTIFICATE_REQUIRED = 16;
+ /** The client SSL certificate specified is invalid */
+ public static final int CLIENT_CERTIFICATE_ERROR = 17;
+ /** The server indicates it does not support OAuth authentication */
+ public static final int OAUTH_NOT_SUPPORTED = 18;
+ /** The server indicates it experienced an internal error */
+ public static final int SERVER_ERROR = 19;
+
+ protected int mExceptionType;
+ // Exception type-specific data
+ protected Object mExceptionData;
+
+ public MessagingException(String message, Throwable throwable) {
+ this(UNSPECIFIED_EXCEPTION, message, throwable);
+ }
+
+ public MessagingException(int exceptionType, String message, Throwable throwable) {
+ super(message, throwable);
+ mExceptionType = exceptionType;
+ mExceptionData = null;
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a null message.
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType) {
+ this(exceptionType, null, null);
+ }
+
+ /**
+ * Constructs a MessagingException with a message.
+ * @param message the message for this exception
+ */
+ public MessagingException(String message) {
+ this(UNSPECIFIED_EXCEPTION, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType and a message.
+ * @param exceptionType The exception type to set for this exception.
+ */
+ public MessagingException(int exceptionType, String message) {
+ this(exceptionType, message, null);
+ }
+
+ /**
+ * Constructs a MessagingException with an exceptionType, a message, and data
+ * @param exceptionType The exception type to set for this exception.
+ * @param message the message for the exception (or null)
+ * @param data exception-type specific data for the exception (or null)
+ */
+ public MessagingException(int exceptionType, String message, Object data) {
+ super(message);
+ mExceptionType = exceptionType;
+ mExceptionData = data;
+ }
+
+ /**
+ * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set.
+ *
+ * @return Returns the exception type.
+ */
+ public int getExceptionType() {
+ return mExceptionType;
+ }
+ /**
+ * Return the exception data. Will be null if not explicitly set.
+ *
+ * @return Returns the exception data.
+ */
+ public Object getExceptionData() {
+ return mExceptionData;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/Multipart.java b/java/com/android/voicemailomtp/mail/Multipart.java
new file mode 100644
index 000000000..b45ebab3d
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Multipart.java
@@ -0,0 +1,62 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import java.util.ArrayList;
+
+public abstract class Multipart implements Body {
+ protected Part mParent;
+
+ protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
+
+ protected String mContentType;
+
+ public void addBodyPart(BodyPart part) throws MessagingException {
+ mParts.add(part);
+ }
+
+ public void addBodyPart(BodyPart part, int index) throws MessagingException {
+ mParts.add(index, part);
+ }
+
+ public BodyPart getBodyPart(int index) throws MessagingException {
+ return mParts.get(index);
+ }
+
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public int getCount() throws MessagingException {
+ return mParts.size();
+ }
+
+ public boolean removeBodyPart(BodyPart part) throws MessagingException {
+ return mParts.remove(part);
+ }
+
+ public void removeBodyPart(int index) throws MessagingException {
+ mParts.remove(index);
+ }
+
+ public Part getParent() throws MessagingException {
+ return mParent;
+ }
+
+ public void setParent(Part parent) throws MessagingException {
+ this.mParent = parent;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/PackedString.java b/java/com/android/voicemailomtp/mail/PackedString.java
new file mode 100644
index 000000000..585759611
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/PackedString.java
@@ -0,0 +1,175 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A utility class for creating and modifying Strings that are tagged and packed together.
+ *
+ * Uses non-printable (control chars) for internal delimiters; Intended for regular displayable
+ * strings only, so please use base64 or other encoding if you need to hide any binary data here.
+ *
+ * Binary compatible with Address.pack() format, which should migrate to use this code.
+ */
+public class PackedString {
+
+ /**
+ * Packing format is:
+ * element : [ value ] or [ value TAG-DELIMITER tag ]
+ * packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]*
+ */
+ private static final char DELIMITER_ELEMENT = '\1';
+ private static final char DELIMITER_TAG = '\2';
+
+ private String mString;
+ private HashMap<String, String> mExploded;
+ private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>();
+
+ /**
+ * Create a packed string using an already-packed string (e.g. from database)
+ * @param string packed string
+ */
+ public PackedString(String string) {
+ mString = string;
+ mExploded = null;
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return mExploded.get(tag);
+ }
+
+ /**
+ * Return a map of all of the values referred to by a given tag. This is a shallow
+ * copy, don't edit the values.
+ * @return a map of the values in the packed string
+ */
+ public Map<String, String> unpack() {
+ if (mExploded == null) {
+ mExploded = explode(mString);
+ }
+ return new HashMap<String,String>(mExploded);
+ }
+
+ /**
+ * Read out all values into a map.
+ */
+ private static HashMap<String, String> explode(String packed) {
+ if (packed == null || packed.length() == 0) {
+ return EMPTY_MAP;
+ }
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ int length = packed.length();
+ int elementStartIndex = 0;
+ int elementEndIndex = 0;
+ int tagEndIndex = packed.indexOf(DELIMITER_TAG);
+
+ while (elementStartIndex < length) {
+ elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex);
+ if (elementEndIndex == -1) {
+ elementEndIndex = length;
+ }
+ String tag;
+ String value;
+ if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) {
+ // in this case the DELIMITER_PERSONAL is in a future pair (or not found)
+ // so synthesize a positional tag for the value, and don't update tagEndIndex
+ value = packed.substring(elementStartIndex, elementEndIndex);
+ tag = Integer.toString(map.size());
+ } else {
+ value = packed.substring(elementStartIndex, tagEndIndex);
+ tag = packed.substring(tagEndIndex + 1, elementEndIndex);
+ // scan forward for next tag, if any
+ tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1);
+ }
+ map.put(tag, value);
+ elementStartIndex = elementEndIndex + 1;
+ }
+
+ return map;
+ }
+
+ /**
+ * Builder class for creating PackedString values. Can also be used for editing existing
+ * PackedString representations.
+ */
+ static public class Builder {
+ HashMap<String, String> mMap;
+
+ /**
+ * Create a builder that's empty (for filling)
+ */
+ public Builder() {
+ mMap = new HashMap<String, String>();
+ }
+
+ /**
+ * Create a builder using the values of an existing PackedString (for editing).
+ */
+ public Builder(String packed) {
+ mMap = explode(packed);
+ }
+
+ /**
+ * Add a tagged value
+ * @param tag identifier of string of interest
+ * @param value the value to record in this position. null to delete entry.
+ */
+ public void put(String tag, String value) {
+ if (value == null) {
+ mMap.remove(tag);
+ } else {
+ mMap.put(tag, value);
+ }
+ }
+
+ /**
+ * Get the value referred to by a given tag. If the tag does not exist, return null.
+ * @param tag identifier of string of interest
+ * @return returns value, or null if no string is found
+ */
+ public String get(String tag) {
+ return mMap.get(tag);
+ }
+
+ /**
+ * Pack the values and return a single, encoded string
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ for (Map.Entry<String,String> entry : mMap.entrySet()) {
+ if (sb.length() > 0) {
+ sb.append(DELIMITER_ELEMENT);
+ }
+ sb.append(entry.getValue());
+ sb.append(DELIMITER_TAG);
+ sb.append(entry.getKey());
+ }
+ return sb.toString();
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/Part.java b/java/com/android/voicemailomtp/mail/Part.java
new file mode 100644
index 000000000..51f8a4c38
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/Part.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.voicemailomtp.mail;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface Part extends Fetchable {
+ public void addHeader(String name, String value) throws MessagingException;
+
+ public void removeHeader(String name) throws MessagingException;
+
+ public void setHeader(String name, String value) throws MessagingException;
+
+ public Body getBody() throws MessagingException;
+
+ public String getContentType() throws MessagingException;
+
+ public String getDisposition() throws MessagingException;
+
+ public String getContentId() throws MessagingException;
+
+ public String[] getHeader(String name) throws MessagingException;
+
+ public void setExtendedHeader(String name, String value) throws MessagingException;
+
+ public String getExtendedHeader(String name) throws MessagingException;
+
+ public int getSize() throws MessagingException;
+
+ public boolean isMimeType(String mimeType) throws MessagingException;
+
+ public String getMimeType() throws MessagingException;
+
+ public void setBody(Body body) throws MessagingException;
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException;
+}
diff --git a/java/com/android/voicemailomtp/mail/PeekableInputStream.java b/java/com/android/voicemailomtp/mail/PeekableInputStream.java
new file mode 100644
index 000000000..c1181d189
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/PeekableInputStream.java
@@ -0,0 +1,80 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * A filtering InputStream that allows single byte "peeks" without consuming the byte. The
+ * client of this stream can call peek() to see the next available byte in the stream
+ * and a subsequent read will still return the peeked byte.
+ */
+public class PeekableInputStream extends InputStream {
+ private final InputStream mIn;
+ private boolean mPeeked;
+ private int mPeekedByte;
+
+ public PeekableInputStream(InputStream in) {
+ this.mIn = in;
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (!mPeeked) {
+ return mIn.read();
+ } else {
+ mPeeked = false;
+ return mPeekedByte;
+ }
+ }
+
+ public int peek() throws IOException {
+ if (!mPeeked) {
+ mPeekedByte = read();
+ mPeeked = true;
+ }
+ return mPeekedByte;
+ }
+
+ @Override
+ public int read(byte[] b, int offset, int length) throws IOException {
+ if (!mPeeked) {
+ return mIn.read(b, offset, length);
+ } else {
+ b[0] = (byte)mPeekedByte;
+ mPeeked = false;
+ int r = mIn.read(b, offset + 1, length - 1);
+ if (r == -1) {
+ return 1;
+ } else {
+ return r + 1;
+ }
+ }
+ }
+
+ @Override
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
+ mIn.toString(), mPeeked, mPeekedByte);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/TempDirectory.java b/java/com/android/voicemailomtp/mail/TempDirectory.java
new file mode 100644
index 000000000..dfae36026
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/TempDirectory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.voicemailomtp.mail;
+
+import android.content.Context;
+
+import java.io.File;
+
+/**
+ * TempDirectory caches the directory used for caching file. It is set up during application
+ * initialization.
+ */
+public class TempDirectory {
+ private static File sTempDirectory = null;
+
+ public static void setTempDirectory(Context context) {
+ sTempDirectory = context.getCacheDir();
+ }
+
+ public static File getTempDirectory() {
+ if (sTempDirectory == null) {
+ throw new RuntimeException(
+ "TempDirectory not set. " +
+ "If in a unit test, call Email.setTempDirectory(context) in setUp().");
+ }
+ return sTempDirectory;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java
new file mode 100644
index 000000000..52c43de16
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java
@@ -0,0 +1,91 @@
+/*
+ * 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.voicemailomtp.mail.internet;
+
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.TempDirectory;
+
+import org.apache.commons.io.IOUtils;
+
+import android.util.Base64;
+import android.util.Base64OutputStream;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
+ * the user to write to the temp file. After the write the body is available via getInputStream
+ * and writeTo one time. After writeTo is called, or the InputStream returned from
+ * getInputStream is closed the file is deleted and the Body should be considered disposed of.
+ */
+public class BinaryTempFileBody implements Body {
+ private File mFile;
+
+ /**
+ * An alternate way to put data into a BinaryTempFileBody is to simply supply an already-
+ * created file. Note that this file will be deleted after it is read.
+ * @param filePath The file containing the data to be stored on disk temporarily
+ */
+ public void setFile(String filePath) {
+ mFile = new File(filePath);
+ }
+
+ public OutputStream getOutputStream() throws IOException {
+ mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory());
+ mFile.deleteOnExit();
+ return new FileOutputStream(mFile);
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
+ }
+ catch (IOException ioe) {
+ throw new MessagingException("Unable to open body", ioe);
+ }
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ InputStream in = getInputStream();
+ Base64OutputStream base64Out = new Base64OutputStream(
+ out, Base64.CRLF | Base64.NO_CLOSE);
+ IOUtils.copy(in, base64Out);
+ base64Out.close();
+ mFile.delete();
+ in.close();
+ }
+
+ class BinaryTempFileBodyInputStream extends FilterInputStream {
+ public BinaryTempFileBodyInputStream(InputStream in) {
+ super(in);
+ }
+
+ @Override
+ public void close() throws IOException {
+ super.close();
+ mFile.delete();
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java b/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java
new file mode 100644
index 000000000..8a9c45cf9
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java
@@ -0,0 +1,207 @@
+/*
+ * 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.voicemailomtp.mail.internet;
+
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.BodyPart;
+import com.android.voicemailomtp.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.regex.Pattern;
+
+/**
+ * TODO this is a close approximation of Message, need to update along with
+ * Message.
+ */
+public class MimeBodyPart extends BodyPart {
+ protected MimeHeader mHeader = new MimeHeader();
+ protected MimeHeader mExtendedHeader;
+ protected Body mBody;
+ protected int mSize;
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeBodyPart() throws MessagingException {
+ this(null);
+ }
+
+ public MimeBodyPart(Body body) throws MessagingException {
+ this(body, null);
+ }
+
+ public MimeBodyPart(Body body, String mimeType) throws MessagingException {
+ if (mimeType != null) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
+ }
+ setBody(body);
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return mHeader.getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ mHeader.addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ mHeader.setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return mHeader.getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ mHeader.removeHeader(name);
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof com.android.voicemailomtp.mail.Multipart) {
+ com.android.voicemailomtp.mail.Multipart multipart =
+ ((com.android.voicemailomtp.mail.Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ }
+ else if (body instanceof TextBody) {
+ String contentType = String.format("%s;\n charset=utf-8", getMimeType());
+ String name = MimeUtility.getHeaderParameter(getContentType(), "name");
+ if (name != null) {
+ contentType += String.format(";\n name=\"%s\"", name);
+ }
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ @Override
+ public String getDisposition() throws MessagingException {
+ String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ if (contentDisposition == null) {
+ return null;
+ } else {
+ return contentDisposition;
+ }
+ }
+
+ @Override
+ public String getContentId() throws MessagingException {
+ String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ @Override
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ @Override
+ public boolean isMimeType(String mimeType) throws MessagingException {
+ return getMimeType().equals(mimeType);
+ }
+
+ public void setSize(int size) {
+ this.mSize = size;
+ }
+
+ @Override
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ @Override
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ @Override
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Write the MimeMessage out in MIME format.
+ */
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ mHeader.writeTo(out);
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeHeader.java b/java/com/android/voicemailomtp/mail/internet/MimeHeader.java
new file mode 100644
index 000000000..4b0aea749
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/MimeHeader.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.voicemailomtp.mail.internet;
+
+import com.android.voicemailomtp.mail.MessagingException;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+
+public class MimeHeader {
+ /**
+ * Application specific header that contains Store specific information about an attachment.
+ * In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
+ * retrieve the attachment at will from the server.
+ * The info is recorded from this header on LocalStore.appendMessage and is put back
+ * into the MIME data by LocalStore.fetch.
+ */
+ public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
+
+ public static final String HEADER_CONTENT_TYPE = "Content-Type";
+ public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+ public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
+ public static final String HEADER_CONTENT_ID = "Content-ID";
+
+ /**
+ * Fields that should be omitted when writing the header using writeTo()
+ */
+ private static final String[] WRITE_OMIT_FIELDS = {
+// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
+// HEADER_ANDROID_ATTACHMENT_ID,
+ HEADER_ANDROID_ATTACHMENT_STORE_DATA
+ };
+
+ protected final ArrayList<Field> mFields = new ArrayList<Field>();
+
+ public void clear() {
+ mFields.clear();
+ }
+
+ public String getFirstHeader(String name) throws MessagingException {
+ String[] header = getHeader(name);
+ if (header == null) {
+ return null;
+ }
+ return header[0];
+ }
+
+ public void addHeader(String name, String value) throws MessagingException {
+ mFields.add(new Field(name, value));
+ }
+
+ public void setHeader(String name, String value) throws MessagingException {
+ if (name == null || value == null) {
+ return;
+ }
+ removeHeader(name);
+ addHeader(name, value);
+ }
+
+ public String[] getHeader(String name) throws MessagingException {
+ ArrayList<String> values = new ArrayList<String>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ values.add(field.value);
+ }
+ }
+ if (values.size() == 0) {
+ return null;
+ }
+ return values.toArray(new String[] {});
+ }
+
+ public void removeHeader(String name) throws MessagingException {
+ ArrayList<Field> removeFields = new ArrayList<Field>();
+ for (Field field : mFields) {
+ if (field.name.equalsIgnoreCase(name)) {
+ removeFields.add(field);
+ }
+ }
+ mFields.removeAll(removeFields);
+ }
+
+ /**
+ * Write header into String
+ *
+ * @return CR-NL separated header string except the headers in writeOmitFields
+ * null if header is empty
+ */
+ public String writeToString() {
+ if (mFields.size() == 0) {
+ return null;
+ }
+ StringBuilder builder = new StringBuilder();
+ for (Field field : mFields) {
+ if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ builder.append(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ return builder.toString();
+ }
+
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ for (Field field : mFields) {
+ if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) {
+ writer.write(field.name + ": " + field.value + "\r\n");
+ }
+ }
+ writer.flush();
+ }
+
+ private static class Field {
+ final String name;
+ final String value;
+
+ public Field(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ @Override
+ public String toString() {
+ return name + "=" + value;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return (mFields == null) ? null : mFields.toString();
+ }
+
+ public final static boolean arrayContains(Object[] a, Object o) {
+ int index = arrayIndex(a, o);
+ return (index >= 0);
+ }
+
+ public final static int arrayIndex(Object[] a, Object o) {
+ for (int i = 0, count = a.length; i < count; i++) {
+ if (a[i].equals(o)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMessage.java b/java/com/android/voicemailomtp/mail/internet/MimeMessage.java
new file mode 100644
index 000000000..a11cd6d83
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/MimeMessage.java
@@ -0,0 +1,675 @@
+/*
+ * 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.voicemailomtp.mail.internet;
+
+import com.android.voicemailomtp.mail.Address;
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.BodyPart;
+import com.android.voicemailomtp.mail.Message;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.Multipart;
+import com.android.voicemailomtp.mail.Part;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+
+import org.apache.james.mime4j.BodyDescriptor;
+import org.apache.james.mime4j.ContentHandler;
+import org.apache.james.mime4j.EOLConvertingInputStream;
+import org.apache.james.mime4j.MimeStreamParser;
+import org.apache.james.mime4j.field.DateTimeField;
+import org.apache.james.mime4j.field.Field;
+
+import android.text.TextUtils;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Stack;
+import java.util.regex.Pattern;
+
+/**
+ * An implementation of Message that stores all of its metadata in RFC 822 and
+ * RFC 2045 style headers.
+ *
+ * NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed.
+ * It would be better to simply do it explicitly on local creation of new outgoing messages.
+ */
+public class MimeMessage extends Message {
+ private MimeHeader mHeader;
+ private MimeHeader mExtendedHeader;
+
+ // NOTE: The fields here are transcribed out of headers, and values stored here will supersede
+ // the values found in the headers. Use caution to prevent any out-of-phase errors. In
+ // particular, any adds/changes/deletes here must be echoed by changes in the parse() function.
+ private Address[] mFrom;
+ private Address[] mTo;
+ private Address[] mCc;
+ private Address[] mBcc;
+ private Address[] mReplyTo;
+ private Date mSentDate;
+ private Body mBody;
+ protected int mSize;
+ private boolean mInhibitLocalMessageId = false;
+ private boolean mComplete = true;
+
+ // Shared random source for generating local message-id values
+ private static final java.util.Random sRandom = new java.util.Random();
+
+ // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to
+ // "Jan", not the other localized format like "Ene" (meaning January in locale es).
+ // This conversion is used when generating outgoing MIME messages. Incoming MIME date
+ // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any
+ // localization code.
+ private static final SimpleDateFormat DATE_FORMAT =
+ new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
+
+ // regex that matches content id surrounded by "<>" optionally.
+ private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$");
+ // regex that matches end of line.
+ private static final Pattern END_OF_LINE = Pattern.compile("\r?\n");
+
+ public MimeMessage() {
+ mHeader = null;
+ }
+
+ /**
+ * Generate a local message id. This is only used when none has been assigned, and is
+ * installed lazily. Any remote (typically server-assigned) message id takes precedence.
+ * @return a long, locally-generated message-ID value
+ */
+ private static String generateMessageId() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("<");
+ for (int i = 0; i < 24; i++) {
+ // We'll use a 5-bit range (0..31)
+ final int value = sRandom.nextInt() & 31;
+ final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value);
+ sb.append(c);
+ }
+ sb.append(".");
+ sb.append(Long.toString(System.currentTimeMillis()));
+ sb.append("@email.android.com>");
+ return sb.toString();
+ }
+
+ /**
+ * Parse the given InputStream using Apache Mime4J to build a MimeMessage.
+ *
+ * @param in InputStream providing message content
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public MimeMessage(InputStream in) throws IOException, MessagingException {
+ parse(in);
+ }
+
+ private MimeStreamParser init() {
+ // Before parsing the input stream, clear all local fields that may be superceded by
+ // the new incoming message.
+ getMimeHeaders().clear();
+ mInhibitLocalMessageId = true;
+ mFrom = null;
+ mTo = null;
+ mCc = null;
+ mBcc = null;
+ mReplyTo = null;
+ mSentDate = null;
+ mBody = null;
+
+ final MimeStreamParser parser = new MimeStreamParser();
+ parser.setContentHandler(new MimeMessageBuilder());
+ return parser;
+ }
+
+ protected void parse(InputStream in) throws IOException, MessagingException {
+ final MimeStreamParser parser = init();
+ parser.parse(new EOLConvertingInputStream(in));
+ mComplete = !parser.getPrematureEof();
+ }
+
+ public void parse(InputStream in, EOLConvertingInputStream.Callback callback)
+ throws IOException, MessagingException {
+ final MimeStreamParser parser = init();
+ parser.parse(new EOLConvertingInputStream(in, getSize(), callback));
+ mComplete = !parser.getPrematureEof();
+ }
+
+ /**
+ * Return the internal mHeader value, with very lazy initialization.
+ * The goal is to save memory by not creating the headers until needed.
+ */
+ private MimeHeader getMimeHeaders() {
+ if (mHeader == null) {
+ mHeader = new MimeHeader();
+ }
+ return mHeader;
+ }
+
+ @Override
+ public Date getReceivedDate() throws MessagingException {
+ return null;
+ }
+
+ @Override
+ public Date getSentDate() throws MessagingException {
+ if (mSentDate == null) {
+ try {
+ DateTimeField field = (DateTimeField)Field.parse("Date: "
+ + MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
+ mSentDate = field.getDate();
+ // TODO: We should make it more clear what exceptions can be thrown here,
+ // and whether they reflect a normal or error condition.
+ } catch (Exception e) {
+ LogUtils.v(LogUtils.TAG, "Message missing Date header");
+ }
+ }
+ if (mSentDate == null) {
+ // If we still don't have a date, fall back to "Delivery-date"
+ try {
+ DateTimeField field = (DateTimeField)Field.parse("Date: "
+ + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date")));
+ mSentDate = field.getDate();
+ // TODO: We should make it more clear what exceptions can be thrown here,
+ // and whether they reflect a normal or error condition.
+ } catch (Exception e) {
+ LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header");
+ }
+ }
+ return mSentDate;
+ }
+
+ @Override
+ public void setSentDate(Date sentDate) throws MessagingException {
+ setHeader("Date", DATE_FORMAT.format(sentDate));
+ this.mSentDate = sentDate;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
+ if (contentType == null) {
+ return "text/plain";
+ } else {
+ return contentType;
+ }
+ }
+
+ @Override
+ public String getDisposition() throws MessagingException {
+ return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
+ }
+
+ @Override
+ public String getContentId() throws MessagingException {
+ final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID);
+ if (contentId == null) {
+ return null;
+ } else {
+ // remove optionally surrounding brackets.
+ return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1");
+ }
+ }
+
+ public boolean isComplete() {
+ return mComplete;
+ }
+
+ @Override
+ public String getMimeType() throws MessagingException {
+ return MimeUtility.getHeaderParameter(getContentType(), null);
+ }
+
+ @Override
+ public int getSize() throws MessagingException {
+ return mSize;
+ }
+
+ /**
+ * Returns a list of the given recipient type from this message. If no addresses are
+ * found the method returns an empty array.
+ */
+ @Override
+ public Address[] getRecipients(String type) throws MessagingException {
+ if (type == RECIPIENT_TYPE_TO) {
+ if (mTo == null) {
+ mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
+ }
+ return mTo;
+ } else if (type == RECIPIENT_TYPE_CC) {
+ if (mCc == null) {
+ mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
+ }
+ return mCc;
+ } else if (type == RECIPIENT_TYPE_BCC) {
+ if (mBcc == null) {
+ mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
+ }
+ return mBcc;
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ @Override
+ public void setRecipients(String type, Address[] addresses) throws MessagingException {
+ final int TO_LENGTH = 4; // "To: "
+ final int CC_LENGTH = 4; // "Cc: "
+ final int BCC_LENGTH = 5; // "Bcc: "
+ if (type == RECIPIENT_TYPE_TO) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("To");
+ this.mTo = null;
+ } else {
+ setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH));
+ this.mTo = addresses;
+ }
+ } else if (type == RECIPIENT_TYPE_CC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("CC");
+ this.mCc = null;
+ } else {
+ setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH));
+ this.mCc = addresses;
+ }
+ } else if (type == RECIPIENT_TYPE_BCC) {
+ if (addresses == null || addresses.length == 0) {
+ removeHeader("BCC");
+ this.mBcc = null;
+ } else {
+ setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH));
+ this.mBcc = addresses;
+ }
+ } else {
+ throw new MessagingException("Unrecognized recipient type.");
+ }
+ }
+
+ /**
+ * Returns the unfolded, decoded value of the Subject header.
+ */
+ @Override
+ public String getSubject() throws MessagingException {
+ return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
+ }
+
+ @Override
+ public void setSubject(String subject) throws MessagingException {
+ final int HEADER_NAME_LENGTH = 9; // "Subject: "
+ setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH));
+ }
+
+ @Override
+ public Address[] getFrom() throws MessagingException {
+ if (mFrom == null) {
+ String list = MimeUtility.unfold(getFirstHeader("From"));
+ if (list == null || list.length() == 0) {
+ list = MimeUtility.unfold(getFirstHeader("Sender"));
+ }
+ mFrom = Address.parse(list);
+ }
+ return mFrom;
+ }
+
+ @Override
+ public void setFrom(Address from) throws MessagingException {
+ final int FROM_LENGTH = 6; // "From: "
+ if (from != null) {
+ setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH));
+ this.mFrom = new Address[] {
+ from
+ };
+ } else {
+ this.mFrom = null;
+ }
+ }
+
+ @Override
+ public Address[] getReplyTo() throws MessagingException {
+ if (mReplyTo == null) {
+ mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
+ }
+ return mReplyTo;
+ }
+
+ @Override
+ public void setReplyTo(Address[] replyTo) throws MessagingException {
+ final int REPLY_TO_LENGTH = 10; // "Reply-to: "
+ if (replyTo == null || replyTo.length == 0) {
+ removeHeader("Reply-to");
+ mReplyTo = null;
+ } else {
+ setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH));
+ mReplyTo = replyTo;
+ }
+ }
+
+ /**
+ * Set the mime "Message-ID" header
+ * @param messageId the new Message-ID value
+ * @throws MessagingException
+ */
+ @Override
+ public void setMessageId(String messageId) throws MessagingException {
+ setHeader("Message-ID", messageId);
+ }
+
+ /**
+ * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated
+ * random ID, if the value has not previously been set. Local generation can be inhibited/
+ * overridden by explicitly clearing the headers, removing the message-id header, etc.
+ * @return the Message-ID header string, or null if explicitly has been set to null
+ */
+ @Override
+ public String getMessageId() throws MessagingException {
+ String messageId = getFirstHeader("Message-ID");
+ if (messageId == null && !mInhibitLocalMessageId) {
+ messageId = generateMessageId();
+ setMessageId(messageId);
+ }
+ return messageId;
+ }
+
+ @Override
+ public void saveChanges() throws MessagingException {
+ throw new MessagingException("saveChanges not yet implemented");
+ }
+
+ @Override
+ public Body getBody() throws MessagingException {
+ return mBody;
+ }
+
+ @Override
+ public void setBody(Body body) throws MessagingException {
+ this.mBody = body;
+ if (body instanceof Multipart) {
+ final Multipart multipart = ((Multipart)body);
+ multipart.setParent(this);
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
+ setHeader("MIME-Version", "1.0");
+ }
+ else if (body instanceof TextBody) {
+ setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
+ getMimeType()));
+ setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
+ }
+ }
+
+ protected String getFirstHeader(String name) throws MessagingException {
+ return getMimeHeaders().getFirstHeader(name);
+ }
+
+ @Override
+ public void addHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(String name, String value) throws MessagingException {
+ getMimeHeaders().setHeader(name, value);
+ }
+
+ @Override
+ public String[] getHeader(String name) throws MessagingException {
+ return getMimeHeaders().getHeader(name);
+ }
+
+ @Override
+ public void removeHeader(String name) throws MessagingException {
+ getMimeHeaders().removeHeader(name);
+ if ("Message-ID".equalsIgnoreCase(name)) {
+ mInhibitLocalMessageId = true;
+ }
+ }
+
+ /**
+ * Set extended header
+ *
+ * @param name Extended header name
+ * @param value header value - flattened by removing CR-NL if any
+ * remove header if value is null
+ * @throws MessagingException
+ */
+ @Override
+ public void setExtendedHeader(String name, String value) throws MessagingException {
+ if (value == null) {
+ if (mExtendedHeader != null) {
+ mExtendedHeader.removeHeader(name);
+ }
+ return;
+ }
+ if (mExtendedHeader == null) {
+ mExtendedHeader = new MimeHeader();
+ }
+ mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll(""));
+ }
+
+ /**
+ * Get extended header
+ *
+ * @param name Extended header name
+ * @return header value - null if header does not exist
+ * @throws MessagingException
+ */
+ @Override
+ public String getExtendedHeader(String name) throws MessagingException {
+ if (mExtendedHeader == null) {
+ return null;
+ }
+ return mExtendedHeader.getFirstHeader(name);
+ }
+
+ /**
+ * Set entire extended headers from String
+ *
+ * @param headers Extended header and its value - "CR-NL-separated pairs
+ * if null or empty, remove entire extended headers
+ * @throws MessagingException
+ */
+ public void setExtendedHeaders(String headers) throws MessagingException {
+ if (TextUtils.isEmpty(headers)) {
+ mExtendedHeader = null;
+ } else {
+ mExtendedHeader = new MimeHeader();
+ for (final String header : END_OF_LINE.split(headers)) {
+ final String[] tokens = header.split(":", 2);
+ if (tokens.length != 2) {
+ throw new MessagingException("Illegal extended headers: " + headers);
+ }
+ mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim());
+ }
+ }
+ }
+
+ /**
+ * Get entire extended headers as String
+ *
+ * @return "CR-NL-separated extended headers - null if extended header does not exist
+ */
+ public String getExtendedHeaders() {
+ if (mExtendedHeader != null) {
+ return mExtendedHeader.writeToString();
+ }
+ return null;
+ }
+
+ /**
+ * Write message header and body to output stream
+ *
+ * @param out Output steam to write message header and body.
+ */
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+ // Force creation of local message-id
+ getMessageId();
+ getMimeHeaders().writeTo(out);
+ // mExtendedHeader will not be write out to external output stream,
+ // because it is intended to internal use.
+ writer.write("\r\n");
+ writer.flush();
+ if (mBody != null) {
+ mBody.writeTo(out);
+ }
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ class MimeMessageBuilder implements ContentHandler {
+ private final Stack<Object> stack = new Stack<Object>();
+
+ public MimeMessageBuilder() {
+ }
+
+ private void expect(Class<?> c) {
+ if (!c.isInstance(stack.peek())) {
+ throw new IllegalStateException("Internal stack error: " + "Expected '"
+ + c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
+ }
+ }
+
+ @Override
+ public void startMessage() {
+ if (stack.isEmpty()) {
+ stack.push(MimeMessage.this);
+ } else {
+ expect(Part.class);
+ try {
+ final MimeMessage m = new MimeMessage();
+ ((Part)stack.peek()).setBody(m);
+ stack.push(m);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+ }
+
+ @Override
+ public void endMessage() {
+ expect(MimeMessage.class);
+ stack.pop();
+ }
+
+ @Override
+ public void startHeader() {
+ expect(Part.class);
+ }
+
+ @Override
+ public void field(String fieldData) {
+ expect(Part.class);
+ try {
+ final String[] tokens = fieldData.split(":", 2);
+ ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void endHeader() {
+ expect(Part.class);
+ }
+
+ @Override
+ public void startMultipart(BodyDescriptor bd) {
+ expect(Part.class);
+
+ final Part e = (Part)stack.peek();
+ try {
+ final MimeMultipart multiPart = new MimeMultipart(e.getContentType());
+ e.setBody(multiPart);
+ stack.push(multiPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void body(BodyDescriptor bd, InputStream in) throws IOException {
+ expect(Part.class);
+ final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
+ try {
+ ((Part)stack.peek()).setBody(body);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void endMultipart() {
+ stack.pop();
+ }
+
+ @Override
+ public void startBodyPart() {
+ expect(MimeMultipart.class);
+
+ try {
+ final MimeBodyPart bodyPart = new MimeBodyPart();
+ ((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
+ stack.push(bodyPart);
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void endBodyPart() {
+ expect(BodyPart.class);
+ stack.pop();
+ }
+
+ @Override
+ public void epilogue(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ final StringBuilder sb = new StringBuilder();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ // TODO: why is this commented out?
+ // ((Multipart) stack.peek()).setEpilogue(sb.toString());
+ }
+
+ @Override
+ public void preamble(InputStream is) throws IOException {
+ expect(MimeMultipart.class);
+ final StringBuilder sb = new StringBuilder();
+ int b;
+ while ((b = is.read()) != -1) {
+ sb.append((char)b);
+ }
+ try {
+ ((MimeMultipart)stack.peek()).setPreamble(sb.toString());
+ } catch (MessagingException me) {
+ throw new Error(me);
+ }
+ }
+
+ @Override
+ public void raw(InputStream is) throws IOException {
+ throw new UnsupportedOperationException("Not supported");
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java b/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java
new file mode 100644
index 000000000..111924336
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java
@@ -0,0 +1,112 @@
+/*
+ * 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.voicemailomtp.mail.internet;
+
+import com.android.voicemailomtp.mail.BodyPart;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.Multipart;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+
+public class MimeMultipart extends Multipart {
+ protected String mPreamble;
+
+ protected String mContentType;
+
+ protected String mBoundary;
+
+ protected String mSubType;
+
+ public MimeMultipart() throws MessagingException {
+ mBoundary = generateBoundary();
+ setSubType("mixed");
+ }
+
+ public MimeMultipart(String contentType) throws MessagingException {
+ this.mContentType = contentType;
+ try {
+ mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
+ mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
+ if (mBoundary == null) {
+ throw new MessagingException("MultiPart does not contain boundary: " + contentType);
+ }
+ } catch (Exception e) {
+ throw new MessagingException(
+ "Invalid MultiPart Content-Type; must contain subtype and boundary. ("
+ + contentType + ")", e);
+ }
+ }
+
+ public String generateBoundary() {
+ StringBuffer sb = new StringBuffer();
+ sb.append("----");
+ for (int i = 0; i < 30; i++) {
+ sb.append(Integer.toString((int)(Math.random() * 35), 36));
+ }
+ return sb.toString().toUpperCase();
+ }
+
+ public String getPreamble() throws MessagingException {
+ return mPreamble;
+ }
+
+ public void setPreamble(String preamble) throws MessagingException {
+ this.mPreamble = preamble;
+ }
+
+ @Override
+ public String getContentType() throws MessagingException {
+ return mContentType;
+ }
+
+ public void setSubType(String subType) throws MessagingException {
+ this.mSubType = subType;
+ mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
+
+ if (mPreamble != null) {
+ writer.write(mPreamble + "\r\n");
+ }
+
+ for (int i = 0, count = mParts.size(); i < count; i++) {
+ BodyPart bodyPart = mParts.get(i);
+ writer.write("--" + mBoundary + "\r\n");
+ writer.flush();
+ bodyPart.writeTo(out);
+ writer.write("\r\n");
+ }
+
+ writer.write("--" + mBoundary + "--\r\n");
+ writer.flush();
+ }
+
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ return null;
+ }
+
+ public String getSubTypeForTest() {
+ return mSubType;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/internet/MimeUtility.java b/java/com/android/voicemailomtp/mail/internet/MimeUtility.java
new file mode 100644
index 000000000..4d310b0f5
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/MimeUtility.java
@@ -0,0 +1,416 @@
+/*
+ * 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.voicemailomtp.mail.internet;
+
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Base64DataException;
+import android.util.Base64InputStream;
+
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.BodyPart;
+import com.android.voicemailomtp.mail.Message;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.Multipart;
+import com.android.voicemailomtp.mail.Part;
+import com.android.voicemailomtp.VvmLog;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.james.mime4j.codec.EncoderUtil;
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+import org.apache.james.mime4j.util.CharsetUtil;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MimeUtility {
+ private static final String LOG_TAG = "Email";
+
+ public static final String MIME_TYPE_RFC822 = "message/rfc822";
+ private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n");
+
+ /**
+ * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string
+ * object whenever possible.
+ */
+ public static String unfold(String s) {
+ if (s == null) {
+ return null;
+ }
+ Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s);
+ if (patternMatcher.find()) {
+ patternMatcher.reset();
+ s = patternMatcher.replaceAll("");
+ }
+ return s;
+ }
+
+ public static String decode(String s) {
+ if (s == null) {
+ return null;
+ }
+ return DecoderUtil.decodeEncodedWords(s);
+ }
+
+ public static String unfoldAndDecode(String s) {
+ return decode(unfold(s));
+ }
+
+ // TODO implement proper foldAndEncode
+ // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent
+ // duplication of encoding.
+ public static String foldAndEncode(String s) {
+ return s;
+ }
+
+ /**
+ * INTERIM version of foldAndEncode that will be used only by Subject: headers.
+ * This is safer than implementing foldAndEncode() (see above) and risking unknown damage
+ * to other headers.
+ *
+ * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK.
+ *
+ * @param s original string to encode and fold
+ * @param usedCharacters number of characters already used up by header name
+
+ * @return the String ready to be transmitted
+ */
+ public static String foldAndEncode2(String s, int usedCharacters) {
+ // james.mime4j.codec.EncoderUtil.java
+ // encode: encodeIfNecessary(text, usage, numUsedInHeaderName)
+ // Usage.TEXT_TOKENlooks like the right thing for subjects
+ // use WORD_ENTITY for address/names
+
+ String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN,
+ usedCharacters);
+
+ return fold(encoded, usedCharacters);
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Splits the specified string into a multiple-line representation with
+ * lines no longer than 76 characters (because the line might contain
+ * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC
+ * 2047</a> section 2). If the string contains non-whitespace sequences
+ * longer than 76 characters a line break is inserted at the whitespace
+ * character following the sequence resulting in a line longer than 76
+ * characters.
+ *
+ * @param s
+ * string to split.
+ * @param usedCharacters
+ * number of characters already used up. Usually the number of
+ * characters for header field name plus colon and one space.
+ * @return a multiple-line representation of the given string.
+ */
+ public static String fold(String s, int usedCharacters) {
+ final int maxCharacters = 76;
+
+ final int length = s.length();
+ if (usedCharacters + length <= maxCharacters)
+ return s;
+
+ StringBuilder sb = new StringBuilder();
+
+ int lastLineBreak = -usedCharacters;
+ int wspIdx = indexOfWsp(s, 0);
+ while (true) {
+ if (wspIdx == length) {
+ sb.append(s.substring(Math.max(0, lastLineBreak)));
+ return sb.toString();
+ }
+
+ int nextWspIdx = indexOfWsp(s, wspIdx + 1);
+
+ if (nextWspIdx - lastLineBreak > maxCharacters) {
+ sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx));
+ sb.append("\r\n");
+ lastLineBreak = wspIdx;
+ }
+
+ wspIdx = nextWspIdx;
+ }
+ }
+
+ /**
+ * INTERIM: From newer version of org.apache.james (but we don't want to import
+ * the entire MimeUtil class).
+ *
+ * Search for whitespace.
+ */
+ private static int indexOfWsp(String s, int fromIndex) {
+ final int len = s.length();
+ for (int index = fromIndex; index < len; index++) {
+ char c = s.charAt(index);
+ if (c == ' ' || c == '\t')
+ return index;
+ }
+ return len;
+ }
+
+ /**
+ * Returns the named parameter of a header field. If name is null the first
+ * parameter is returned, or if there are no additional parameters in the
+ * field the entire field is returned. Otherwise the named parameter is
+ * searched for in a case insensitive fashion and returned. If the parameter
+ * cannot be found the method returns null.
+ *
+ * TODO: quite inefficient with the inner trimming & splitting.
+ * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive.
+ * TODO: The doc says that for a null name you get the first param, but you get the header.
+ * Should probably just fix the doc, but if other code assumes that behavior, fix the code.
+ * TODO: Need to decode %-escaped strings, as in: filename="ab%22d".
+ * ('+' -> ' ' conversion too? check RFC)
+ *
+ * @param header
+ * @param name
+ * @return the entire header (if name=null), the found parameter, or null
+ */
+ public static String getHeaderParameter(String header, String name) {
+ if (header == null) {
+ return null;
+ }
+ String[] parts = unfold(header).split(";");
+ if (name == null) {
+ return parts[0].trim();
+ }
+ String lowerCaseName = name.toLowerCase();
+ for (String part : parts) {
+ if (part.trim().toLowerCase().startsWith(lowerCaseName)) {
+ String[] parameterParts = part.split("=", 2);
+ if (parameterParts.length < 2) {
+ return null;
+ }
+ String parameter = parameterParts[1].trim();
+ if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
+ return parameter.substring(1, parameter.length() - 1);
+ } else {
+ return parameter;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Reads the Part's body and returns a String based on any charset conversion that needed
+ * to be done.
+ * @param part The part containing a body
+ * @return a String containing the converted text in the body, or null if there was no text
+ * or an error during conversion.
+ */
+ public static String getTextFromPart(Part part) {
+ try {
+ if (part != null && part.getBody() != null) {
+ InputStream in = part.getBody().getInputStream();
+ String mimeType = part.getMimeType();
+ if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
+ /*
+ * Now we read the part into a buffer for further processing. Because
+ * the stream is now wrapped we'll remove any transfer encoding at this point.
+ */
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ IOUtils.copy(in, out);
+ in.close();
+ in = null; // we want all of our memory back, and close might not release
+
+ /*
+ * We've got a text part, so let's see if it needs to be processed further.
+ */
+ String charset = getHeaderParameter(part.getContentType(), "charset");
+ if (charset != null) {
+ /*
+ * See if there is conversion from the MIME charset to the Java one.
+ */
+ charset = CharsetUtil.toJavaCharset(charset);
+ }
+ /*
+ * No encoding, so use us-ascii, which is the standard.
+ */
+ if (charset == null) {
+ charset = "ASCII";
+ }
+ /*
+ * Convert and return as new String
+ */
+ String result = out.toString(charset);
+ out.close();
+ return result;
+ }
+ }
+
+ }
+ catch (OutOfMemoryError oom) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString());
+ }
+ catch (Exception e) {
+ /*
+ * If we are not able to process the body there's nothing we can do about it. Return
+ * null and let the upper layers handle the missing content.
+ */
+ VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString());
+ }
+ return null;
+ }
+
+ /**
+ * Returns true if the given mimeType matches the matchAgainst specification. The comparison
+ * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst A MIME type to check against. May include wildcards.
+ * @return true if the mimeType matches
+ */
+ public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
+ Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"),
+ Pattern.CASE_INSENSITIVE);
+ return p.matcher(mimeType).matches();
+ }
+
+ /**
+ * Returns true if the given mimeType matches any of the matchAgainst specifications. The
+ * comparison ignores case and the matchAgainst strings may include "*" for a wildcard
+ * (e.g. "image/*").
+ *
+ * @param mimeType A MIME type to check.
+ * @param matchAgainst An array of MIME types to check against. May include wildcards.
+ * @return true if the mimeType matches any of the matchAgainst strings
+ */
+ public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
+ for (String matchType : matchAgainst) {
+ if (mimeTypeMatches(mimeType, matchType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Given an input stream and a transfer encoding, return a wrapped input stream for that
+ * encoding (or the original if none is required)
+ * @param in the input stream
+ * @param contentTransferEncoding the content transfer encoding
+ * @return a properly wrapped stream
+ */
+ public static InputStream getInputStreamForContentTransferEncoding(InputStream in,
+ String contentTransferEncoding) {
+ if (contentTransferEncoding != null) {
+ contentTransferEncoding =
+ MimeUtility.getHeaderParameter(contentTransferEncoding, null);
+ if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new QuotedPrintableInputStream(in);
+ }
+ else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
+ in = new Base64InputStream(in, Base64.DEFAULT);
+ }
+ }
+ return in;
+ }
+
+ /**
+ * Removes any content transfer encoding from the stream and returns a Body.
+ */
+ public static Body decodeBody(InputStream in, String contentTransferEncoding)
+ throws IOException {
+ /*
+ * We'll remove any transfer encoding by wrapping the stream.
+ */
+ in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ IOUtils.copy(in, out);
+ } catch (Base64DataException bde) {
+ // TODO Need to fix this somehow
+ //String warning = "\n\n" + Email.getMessageDecodeErrorString();
+ //out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ /**
+ * Recursively scan a Part (usually a Message) and sort out which of its children will be
+ * "viewable" and which will be attachments.
+ *
+ * @param part The part to be broken down
+ * @param viewables This arraylist will be populated with all parts that appear to be
+ * the "message" (e.g. text/plain & text/html)
+ * @param attachments This arraylist will be populated with all parts that appear to be
+ * attachments (including inlines)
+ * @throws MessagingException
+ */
+ public static void collectParts(Part part, ArrayList<Part> viewables,
+ ArrayList<Part> attachments) throws MessagingException {
+ String disposition = part.getDisposition();
+ String dispositionType = MimeUtility.getHeaderParameter(disposition, null);
+ // If a disposition is not specified, default to "inline"
+ boolean inline =
+ TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType);
+ // The lower-case mime type
+ String mimeType = part.getMimeType().toLowerCase();
+
+ if (part.getBody() instanceof Multipart) {
+ // If the part is Multipart but not alternative it's either mixed or
+ // something we don't know about, which means we treat it as mixed
+ // per the spec. We just process its pieces recursively.
+ MimeMultipart mp = (MimeMultipart)part.getBody();
+ boolean foundHtml = false;
+ if (mp.getSubTypeForTest().equals("alternative")) {
+ for (int i = 0; i < mp.getCount(); i++) {
+ if (mp.getBodyPart(i).isMimeType("text/html")) {
+ foundHtml = true;
+ break;
+ }
+ }
+ }
+ for (int i = 0; i < mp.getCount(); i++) {
+ // See if we have text and html
+ BodyPart bp = mp.getBodyPart(i);
+ // If there's html, don't bother loading text
+ if (foundHtml && bp.isMimeType("text/plain")) {
+ continue;
+ }
+ collectParts(bp, viewables, attachments);
+ }
+ } else if (part.getBody() instanceof Message) {
+ // If the part is an embedded message we just continue to process
+ // it, pulling any viewables or attachments into the running list.
+ Message message = (Message)part.getBody();
+ collectParts(message, viewables, attachments);
+ } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) {
+ // We'll treat text and images as viewables
+ viewables.add(part);
+ } else {
+ // Everything else is an attachment.
+ attachments.add(part);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/internet/TextBody.java b/java/com/android/voicemailomtp/mail/internet/TextBody.java
new file mode 100644
index 000000000..578193eff
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/internet/TextBody.java
@@ -0,0 +1,63 @@
+/*
+ * 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.voicemailomtp.mail.internet;
+
+import android.util.Base64;
+
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.MessagingException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+public class TextBody implements Body {
+ String mBody;
+
+ public TextBody(String body) {
+ this.mBody = body;
+ }
+
+ @Override
+ public void writeTo(OutputStream out) throws IOException, MessagingException {
+ byte[] bytes = mBody.getBytes("UTF-8");
+ out.write(Base64.encode(bytes, Base64.CRLF));
+ }
+
+ /**
+ * Get the text of the body in it's unencoded format.
+ * @return
+ */
+ public String getText() {
+ return mBody;
+ }
+
+ /**
+ * Returns an InputStream that reads this body's text in UTF-8 format.
+ */
+ @Override
+ public InputStream getInputStream() throws MessagingException {
+ try {
+ byte[] b = mBody.getBytes("UTF-8");
+ return new ByteArrayInputStream(b);
+ }
+ catch (UnsupportedEncodingException usee) {
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/store/ImapConnection.java b/java/com/android/voicemailomtp/mail/store/ImapConnection.java
new file mode 100644
index 000000000..61dcf1281
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/ImapConnection.java
@@ -0,0 +1,413 @@
+/*
+ * 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.voicemailomtp.mail.store;
+
+import android.util.ArraySet;
+import android.util.Base64;
+import com.android.voicemailomtp.mail.AuthenticationFailedException;
+import com.android.voicemailomtp.mail.CertificateValidationException;
+import com.android.voicemailomtp.mail.MailTransport;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.store.ImapStore.ImapException;
+import com.android.voicemailomtp.mail.store.imap.DigestMd5Utils;
+import com.android.voicemailomtp.mail.store.imap.ImapConstants;
+import com.android.voicemailomtp.mail.store.imap.ImapResponse;
+import com.android.voicemailomtp.mail.store.imap.ImapResponseParser;
+import com.android.voicemailomtp.mail.store.imap.ImapUtility;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.VvmLog;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import javax.net.ssl.SSLException;
+
+/**
+ * A cacheable class that stores the details for a single IMAP connection.
+ */
+public class ImapConnection {
+ private final String TAG = "ImapConnection";
+
+ private String mLoginPhrase;
+ private ImapStore mImapStore;
+ private MailTransport mTransport;
+ private ImapResponseParser mParser;
+ private Set<String> mCapabilities = new ArraySet<>();
+
+ static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
+
+ /**
+ * Next tag to use. All connections associated to the same ImapStore instance share the same
+ * counter to make tests simpler.
+ * (Some of the tests involve multiple connections but only have a single counter to track the
+ * tag.)
+ */
+ private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
+
+ ImapConnection(ImapStore store) {
+ setStore(store);
+ }
+
+ void setStore(ImapStore store) {
+ // TODO: maybe we should throw an exception if the connection is not closed here,
+ // if it's not currently closed, then we won't reopen it, so if the credentials have
+ // changed, the connection will not be reestablished.
+ mImapStore = store;
+ mLoginPhrase = null;
+ }
+
+ /**
+ * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
+ * username and password.
+ *
+ * @return the login command string to sent to the IMAP server
+ */
+ String getLoginPhrase() {
+ if (mLoginPhrase == null) {
+ if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
+ // build the LOGIN string once (instead of over-and-over again.)
+ // apply the quoting here around the built-up password
+ mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
+ + ImapUtility.imapQuoted(mImapStore.getPassword());
+ }
+ }
+ return mLoginPhrase;
+ }
+
+ public void open() throws IOException, MessagingException {
+ if (mTransport != null && mTransport.isOpen()) {
+ return;
+ }
+
+ try {
+ // copy configuration into a clean transport, if necessary
+ if (mTransport == null) {
+ mTransport = mImapStore.cloneTransport();
+ }
+
+ mTransport.open();
+
+ createParser();
+
+ // The server should greet us with something like
+ // * OK IMAP4rev1 Server
+ // consume the response before doing anything else.
+ ImapResponse response = mParser.readResponse(false);
+ if (!response.isOk()) {
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
+ throw new MessagingException(
+ MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
+ "Invalid server initial response");
+ }
+
+ queryCapability();
+
+ maybeDoStartTls();
+
+ // LOGIN
+ doLogin();
+ } catch (SSLException e) {
+ LogUtils.d(TAG, "SSLException ", e);
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
+ throw new CertificateValidationException(e.getMessage(), e);
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, "IOException", ioe);
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
+ throw ioe;
+ } finally {
+ destroyResponses();
+ }
+ }
+
+ void logout() {
+ try {
+ sendCommand(ImapConstants.LOGOUT, false);
+ if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
+ VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
+ }
+ if (!mParser.readResponse(false).isOk()) {
+ VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
+ }
+ } catch (IOException | MessagingException e) {
+ VvmLog.e(TAG, "Error while logging out:" + e);
+ }
+ }
+
+ /**
+ * Closes the connection and releases all resources. This connection can not be used again
+ * until {@link #setStore(ImapStore)} is called.
+ */
+ void close() {
+ if (mTransport != null) {
+ logout();
+ mTransport.close();
+ mTransport = null;
+ }
+ destroyResponses();
+ mParser = null;
+ mImapStore = null;
+ }
+
+ /**
+ * Attempts to convert the connection into secure connection.
+ */
+ private void maybeDoStartTls() throws IOException, MessagingException {
+ // STARTTLS is required in the OMTP standard but not every implementation support it.
+ // Make sure the server does have this capability
+ if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
+ executeSimpleCommand(ImapConstants.STARTTLS);
+ mTransport.reopenTls();
+ createParser();
+ // The cached capabilities should be refreshed after TLS is established.
+ queryCapability();
+ }
+ }
+
+ /**
+ * Logs into the IMAP server
+ */
+ private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
+ try {
+ if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
+ doDigestMd5Auth();
+ } else {
+ executeSimpleCommand(getLoginPhrase(), true);
+ }
+ } catch (ImapException ie) {
+ LogUtils.d(TAG, "ImapException", ie);
+ String status = ie.getStatus();
+ String statusMessage = ie.getStatusMessage();
+ String alertText = ie.getAlertText();
+
+ if (ImapConstants.NO.equals(status)) {
+ switch (statusMessage) {
+ case ImapConstants.NO_UNKNOWN_USER:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
+ break;
+ case ImapConstants.NO_UNKNOWN_CLIENT:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
+ break;
+ case ImapConstants.NO_INVALID_PASSWORD:
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
+ break;
+ case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
+ break;
+ case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
+ break;
+ case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
+ break;
+ case ImapConstants.NO_USER_IS_BLOCKED:
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
+ break;
+ case ImapConstants.NO_APPLICATION_ERROR:
+ mImapStore.getImapHelper()
+ .handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+ default:
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
+ }
+ throw new AuthenticationFailedException(alertText, ie);
+ }
+
+ mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
+ throw new MessagingException(alertText, ie);
+ }
+ }
+
+ private void doDigestMd5Auth() throws IOException, MessagingException {
+
+ // Initiate the authentication.
+ // The server will issue us a challenge, asking to run MD5 on the nonce with our password
+ // and other data, including the cnonce we randomly generated.
+ //
+ // C: a AUTHENTICATE DIGEST-MD5
+ // S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
+ // algorithm=md5-sess,charset=utf-8
+ List<ImapResponse> responses = executeSimpleCommand(
+ ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
+ String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+
+ Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
+ DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
+
+ String response = data.createResponse();
+ // Respond to the challenge. If the server accepts it, it will reply a response-auth which
+ // is the MD5 of our password and the cnonce we've provided, to prove the server does know
+ // the password.
+ //
+ // C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
+ // nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
+ // digest-uri="imap/elwood.innosoft.com",
+ // response=d388dad90d4bbd760a152321f2143af7,qop=auth
+ // S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
+
+ responses = executeContinuationResponse(encodeBase64(response), true);
+
+ // Verify response-auth.
+ // If failed verifyResponseAuth() will throw a MessagingException, terminating the
+ // connection
+ String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
+ data.verifyResponseAuth(decodedResponseAuth);
+
+ // Send a empty response to indicate we've accepted the response-auth
+ //
+ // C: (empty)
+ // S: a OK User logged in
+ executeContinuationResponse("", false);
+
+ }
+
+ private static String decodeBase64(String string) {
+ return new String(Base64.decode(string, Base64.DEFAULT));
+ }
+
+ private static String encodeBase64(String string) {
+ return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
+ }
+
+ private void queryCapability() throws IOException, MessagingException {
+ List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
+ mCapabilities.clear();
+ Set<String> disabledCapabilities = mImapStore.getImapHelper().getConfig()
+ .getDisabledCapabilities();
+ for (ImapResponse response : responses) {
+ if (response.isTagged()) {
+ continue;
+ }
+ for (int i = 0; i < response.size(); i++) {
+ String capability = response.getStringOrEmpty(i).getString();
+ if (disabledCapabilities != null) {
+ if (!disabledCapabilities.contains(capability)) {
+ mCapabilities.add(capability);
+ }
+ } else {
+ mCapabilities.add(capability);
+ }
+ }
+ }
+
+ LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
+ }
+
+ private boolean hasCapability(String capability) {
+ return mCapabilities.contains(capability);
+ }
+ /**
+ * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
+ * set it to {@link #mParser}.
+ *
+ * If we already have an {@link ImapResponseParser}, we
+ * {@link #destroyResponses()} and throw it away.
+ */
+ private void createParser() {
+ destroyResponses();
+ mParser = new ImapResponseParser(mTransport.getInputStream());
+ }
+
+
+ public void destroyResponses() {
+ if (mParser != null) {
+ mParser.destroyResponses();
+ }
+ }
+
+ public ImapResponse readResponse() throws IOException, MessagingException {
+ return mParser.readResponse(false);
+ }
+
+ public List<ImapResponse> executeSimpleCommand(String command)
+ throws IOException, MessagingException{
+ return executeSimpleCommand(command, false);
+ }
+
+ /**
+ * Send a single command to the server. The command will be preceded by an IMAP command
+ * tag and followed by \r\n (caller need not supply them).
+ * Execute a simple command at the server, a simple command being one that is sent in a single
+ * line of text
+ *
+ * @param command the command to send to the server
+ * @param sensitive whether the command should be redacted in logs (used for login)
+ * @return a list of ImapResponses
+ * @throws IOException
+ * @throws MessagingException
+ */
+ public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
+ throws IOException, MessagingException {
+ // TODO: It may be nice to catch IOExceptions and close the connection here.
+ // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
+ sendCommand(command, sensitive);
+ return getCommandResponses();
+ }
+
+ public String sendCommand(String command, boolean sensitive)
+ throws IOException, MessagingException {
+ open();
+
+ if (mTransport == null) {
+ throw new IOException("Null transport");
+ }
+ String tag = Integer.toString(mNextCommandTag.incrementAndGet());
+ String commandToSend = tag + " " + command;
+ mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command));
+ return tag;
+ }
+
+ List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
+ throws IOException, MessagingException {
+ mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
+ return getCommandResponses();
+ }
+
+ /**
+ * Read and return all of the responses from the most recent command sent to the server
+ *
+ * @return a list of ImapResponses
+ * @throws IOException
+ * @throws MessagingException
+ */
+ List<ImapResponse> getCommandResponses()
+ throws IOException, MessagingException {
+ final List<ImapResponse> responses = new ArrayList<ImapResponse>();
+ ImapResponse response;
+ do {
+ response = mParser.readResponse(false);
+ responses.add(response);
+ } while (!(response.isTagged() || response.isContinuationRequest()));
+
+ if (!(response.isOk() || response.isContinuationRequest())) {
+ final String toString = response.toString();
+ final String status = response.getStatusOrEmpty().getString();
+ final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
+ final String alert = response.getAlertTextOrEmpty().getString();
+ final String responseCode = response.getResponseCodeOrEmpty().getString();
+ destroyResponses();
+ throw new ImapException(toString, status, statusMessage, alert, responseCode);
+ }
+ return responses;
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/store/ImapFolder.java b/java/com/android/voicemailomtp/mail/store/ImapFolder.java
new file mode 100644
index 000000000..eca349876
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/ImapFolder.java
@@ -0,0 +1,784 @@
+/*
+ * 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.voicemailomtp.mail.store;
+
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Base64DataException;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.mail.AuthenticationFailedException;
+import com.android.voicemailomtp.mail.Body;
+import com.android.voicemailomtp.mail.FetchProfile;
+import com.android.voicemailomtp.mail.Flag;
+import com.android.voicemailomtp.mail.Message;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.Part;
+import com.android.voicemailomtp.mail.internet.BinaryTempFileBody;
+import com.android.voicemailomtp.mail.internet.MimeBodyPart;
+import com.android.voicemailomtp.mail.internet.MimeHeader;
+import com.android.voicemailomtp.mail.internet.MimeMultipart;
+import com.android.voicemailomtp.mail.internet.MimeUtility;
+import com.android.voicemailomtp.mail.store.ImapStore.ImapException;
+import com.android.voicemailomtp.mail.store.ImapStore.ImapMessage;
+import com.android.voicemailomtp.mail.store.imap.ImapConstants;
+import com.android.voicemailomtp.mail.store.imap.ImapElement;
+import com.android.voicemailomtp.mail.store.imap.ImapList;
+import com.android.voicemailomtp.mail.store.imap.ImapResponse;
+import com.android.voicemailomtp.mail.store.imap.ImapString;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+import com.android.voicemailomtp.mail.utils.Utility;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+
+public class ImapFolder {
+ private static final String TAG = "ImapFolder";
+ private final static String[] PERMANENT_FLAGS =
+ { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED };
+ private static final int COPY_BUFFER_SIZE = 16*1024;
+
+ private final ImapStore mStore;
+ private final String mName;
+ private int mMessageCount = -1;
+ private ImapConnection mConnection;
+ private String mMode;
+ private boolean mExists;
+ /** A set of hashes that can be used to track dirtiness */
+ Object mHash[];
+
+ public static final String MODE_READ_ONLY = "mode_read_only";
+ public static final String MODE_READ_WRITE = "mode_read_write";
+
+ public ImapFolder(ImapStore store, String name) {
+ mStore = store;
+ mName = name;
+ }
+
+ /**
+ * Callback for each message retrieval.
+ */
+ public interface MessageRetrievalListener {
+ public void messageRetrieved(Message message);
+ }
+
+ private void destroyResponses() {
+ if (mConnection != null) {
+ mConnection.destroyResponses();
+ }
+ }
+
+ public void open(String mode) throws MessagingException {
+ try {
+ if (isOpen()) {
+ throw new AssertionError("Duplicated open on ImapFolder");
+ }
+ synchronized (this) {
+ mConnection = mStore.getConnection();
+ }
+ // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk
+ // $MDNSent)
+ // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft
+ // NonJunk $MDNSent \*)] Flags permitted.
+ // * 23 EXISTS
+ // * 0 RECENT
+ // * OK [UIDVALIDITY 1125022061] UIDs valid
+ // * OK [UIDNEXT 57576] Predicted next UID
+ // 2 OK [READ-WRITE] Select completed.
+ try {
+ doSelect();
+ } catch (IOException ioe) {
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ } catch (AuthenticationFailedException e) {
+ // Don't cache this connection, so we're forced to try connecting/login again
+ mConnection = null;
+ close(false);
+ throw e;
+ } catch (MessagingException e) {
+ mExists = false;
+ close(false);
+ throw e;
+ }
+ }
+
+ public boolean isOpen() {
+ return mExists && mConnection != null;
+ }
+
+ public String getMode() {
+ return mMode;
+ }
+
+ public void close(boolean expunge) {
+ if (expunge) {
+ try {
+ expunge();
+ } catch (MessagingException e) {
+ LogUtils.e(TAG, e, "Messaging Exception");
+ }
+ }
+ mMessageCount = -1;
+ synchronized (this) {
+ mConnection = null;
+ }
+ }
+
+ public int getMessageCount() {
+ return mMessageCount;
+ }
+
+ String[] getSearchUids(List<ImapResponse> responses) {
+ // S: * SEARCH 2 3 6
+ final ArrayList<String> uids = new ArrayList<String>();
+ for (ImapResponse response : responses) {
+ if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
+ continue;
+ }
+ // Found SEARCH response data
+ for (int i = 1; i < response.size(); i++) {
+ ImapString s = response.getStringOrEmpty(i);
+ if (s.isString()) {
+ uids.add(s.getString());
+ }
+ }
+ }
+ return uids.toArray(Utility.EMPTY_STRINGS);
+ }
+
+ @VisibleForTesting
+ String[] searchForUids(String searchCriteria) throws MessagingException {
+ checkOpen();
+ try {
+ try {
+ final String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
+ final String[] result = getSearchUids(mConnection.executeSimpleCommand(command));
+ LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " +
+ result.length);
+ return result;
+ } catch (ImapException me) {
+ LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me);
+ return Utility.EMPTY_STRINGS; // Not found
+ } catch (IOException ioe) {
+ LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe);
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ }
+ } finally {
+ destroyResponses();
+ }
+ }
+
+ @Nullable
+ public Message getMessage(String uid) throws MessagingException {
+ checkOpen();
+
+ final String[] uids = searchForUids(ImapConstants.UID + " " + uid);
+ for (int i = 0; i < uids.length; i++) {
+ if (uids[i].equals(uid)) {
+ return new ImapMessage(uid, this);
+ }
+ }
+ LogUtils.e(TAG, "UID " + uid + " not found on server");
+ return null;
+ }
+
+ @VisibleForTesting
+ protected static boolean isAsciiString(String str) {
+ int len = str.length();
+ for (int i = 0; i < len; i++) {
+ char c = str.charAt(i);
+ if (c >= 128) return false;
+ }
+ return true;
+ }
+
+ public Message[] getMessages(String[] uids) throws MessagingException {
+ if (uids == null) {
+ uids = searchForUids("1:* NOT DELETED");
+ }
+ return getMessagesInternal(uids);
+ }
+
+ public Message[] getMessagesInternal(String[] uids) {
+ final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
+ for (int i = 0; i < uids.length; i++) {
+ final String uid = uids[i];
+ final ImapMessage message = new ImapMessage(uid, this);
+ messages.add(message);
+ }
+ return messages.toArray(Message.EMPTY_ARRAY);
+ }
+
+ public void fetch(Message[] messages, FetchProfile fp,
+ MessageRetrievalListener listener) throws MessagingException {
+ try {
+ fetchInternal(messages, fp, listener);
+ } catch (RuntimeException e) { // Probably a parser error.
+ LogUtils.w(TAG, "Exception detected: " + e.getMessage());
+ throw e;
+ }
+ }
+
+ public void fetchInternal(Message[] messages, FetchProfile fp,
+ MessageRetrievalListener listener) throws MessagingException {
+ if (messages.length == 0) {
+ return;
+ }
+ checkOpen();
+ HashMap<String, Message> messageMap = new HashMap<String, Message>();
+ for (Message m : messages) {
+ messageMap.put(m.getUid(), m);
+ }
+
+ /*
+ * Figure out what command we are going to run:
+ * FLAGS - UID FETCH (FLAGS)
+ * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[
+ * HEADER.FIELDS (date subject from content-type to cc)])
+ * STRUCTURE - UID FETCH (BODYSTRUCTURE)
+ * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned
+ * BODY - UID FETCH (BODY.PEEK[])
+ * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID
+ */
+
+ final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>();
+
+ fetchFields.add(ImapConstants.UID);
+ if (fp.contains(FetchProfile.Item.FLAGS)) {
+ fetchFields.add(ImapConstants.FLAGS);
+ }
+ if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+ fetchFields.add(ImapConstants.INTERNALDATE);
+ fetchFields.add(ImapConstants.RFC822_SIZE);
+ fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS);
+ }
+ if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+ fetchFields.add(ImapConstants.BODYSTRUCTURE);
+ }
+
+ if (fp.contains(FetchProfile.Item.BODY_SANE)) {
+ fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE);
+ }
+ if (fp.contains(FetchProfile.Item.BODY)) {
+ fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK);
+ }
+
+ // TODO Why are we only fetching the first part given?
+ final Part fetchPart = fp.getFirstPart();
+ if (fetchPart != null) {
+ final String[] partIds =
+ fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA);
+ // TODO Why can a single part have more than one Id? And why should we only fetch
+ // the first id if there are more than one?
+ if (partIds != null) {
+ fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE
+ + "[" + partIds[0] + "]");
+ }
+ }
+
+ try {
+ mConnection.sendCommand(String.format(Locale.US,
+ ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages),
+ Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ')
+ ), false);
+ ImapResponse response;
+ do {
+ response = null;
+ try {
+ response = mConnection.readResponse();
+
+ if (!response.isDataResponse(1, ImapConstants.FETCH)) {
+ continue; // Ignore
+ }
+ final ImapList fetchList = response.getListOrEmpty(2);
+ final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID)
+ .getString();
+ if (TextUtils.isEmpty(uid)) continue;
+
+ ImapMessage message = (ImapMessage) messageMap.get(uid);
+ if (message == null) continue;
+
+ if (fp.contains(FetchProfile.Item.FLAGS)) {
+ final ImapList flags =
+ fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS);
+ for (int i = 0, count = flags.size(); i < count; i++) {
+ final ImapString flag = flags.getStringOrEmpty(i);
+ if (flag.is(ImapConstants.FLAG_DELETED)) {
+ message.setFlagInternal(Flag.DELETED, true);
+ } else if (flag.is(ImapConstants.FLAG_ANSWERED)) {
+ message.setFlagInternal(Flag.ANSWERED, true);
+ } else if (flag.is(ImapConstants.FLAG_SEEN)) {
+ message.setFlagInternal(Flag.SEEN, true);
+ } else if (flag.is(ImapConstants.FLAG_FLAGGED)) {
+ message.setFlagInternal(Flag.FLAGGED, true);
+ }
+ }
+ }
+ if (fp.contains(FetchProfile.Item.ENVELOPE)) {
+ final Date internalDate = fetchList.getKeyedStringOrEmpty(
+ ImapConstants.INTERNALDATE).getDateOrNull();
+ final int size = fetchList.getKeyedStringOrEmpty(
+ ImapConstants.RFC822_SIZE).getNumberOrZero();
+ final String header = fetchList.getKeyedStringOrEmpty(
+ ImapConstants.BODY_BRACKET_HEADER, true).getString();
+
+ message.setInternalDate(internalDate);
+ message.setSize(size);
+ message.parse(Utility.streamFromAsciiString(header));
+ }
+ if (fp.contains(FetchProfile.Item.STRUCTURE)) {
+ ImapList bs = fetchList.getKeyedListOrEmpty(
+ ImapConstants.BODYSTRUCTURE);
+ if (!bs.isEmpty()) {
+ try {
+ parseBodyStructure(bs, message, ImapConstants.TEXT);
+ } catch (MessagingException e) {
+ LogUtils.v(TAG, e, "Error handling message");
+ message.setBody(null);
+ }
+ }
+ }
+ if (fp.contains(FetchProfile.Item.BODY)
+ || fp.contains(FetchProfile.Item.BODY_SANE)) {
+ // Body is keyed by "BODY[]...".
+ // Previously used "BODY[..." but this can be confused with "BODY[HEADER..."
+ // TODO Should we accept "RFC822" as well??
+ ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
+ InputStream bodyStream = body.getAsStream();
+ message.parse(bodyStream);
+ }
+ if (fetchPart != null) {
+ InputStream bodyStream =
+ fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream();
+ String encodings[] = fetchPart.getHeader(
+ MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING);
+
+ String contentTransferEncoding = null;
+ if (encodings != null && encodings.length > 0) {
+ contentTransferEncoding = encodings[0];
+ } else {
+ // According to http://tools.ietf.org/html/rfc2045#section-6.1
+ // "7bit" is the default.
+ contentTransferEncoding = "7bit";
+ }
+
+ try {
+ // TODO Don't create 2 temp files.
+ // decodeBody creates BinaryTempFileBody, but we could avoid this
+ // if we implement ImapStringBody.
+ // (We'll need to share a temp file. Protect it with a ref-count.)
+ message.setBody(decodeBody(mStore.getContext(), bodyStream,
+ contentTransferEncoding, fetchPart.getSize(), listener));
+ } catch(Exception e) {
+ // TODO: Figure out what kinds of exceptions might actually be thrown
+ // from here. This blanket catch-all is because we're not sure what to
+ // do if we don't have a contentTransferEncoding, and we don't have
+ // time to figure out what exceptions might be thrown.
+ LogUtils.e(TAG, "Error fetching body %s", e);
+ }
+ }
+
+ if (listener != null) {
+ listener.messageRetrieved(message);
+ }
+ } finally {
+ destroyResponses();
+ }
+ } while (!response.isTagged());
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ }
+ }
+
+ /**
+ * Removes any content transfer encoding from the stream and returns a Body.
+ * This code is taken/condensed from MimeUtility.decodeBody
+ */
+ private static Body decodeBody(Context context,InputStream in, String contentTransferEncoding,
+ int size, MessageRetrievalListener listener) throws IOException {
+ // Get a properly wrapped input stream
+ in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding);
+ BinaryTempFileBody tempBody = new BinaryTempFileBody();
+ OutputStream out = tempBody.getOutputStream();
+ try {
+ byte[] buffer = new byte[COPY_BUFFER_SIZE];
+ int n = 0;
+ int count = 0;
+ while (-1 != (n = in.read(buffer))) {
+ out.write(buffer, 0, n);
+ count += n;
+ }
+ } catch (Base64DataException bde) {
+ String warning = "\n\nThere was an error while decoding the message.";
+ out.write(warning.getBytes());
+ } finally {
+ out.close();
+ }
+ return tempBody;
+ }
+
+ public String[] getPermanentFlags() {
+ return PERMANENT_FLAGS;
+ }
+
+ /**
+ * Handle any untagged responses that the caller doesn't care to handle themselves.
+ * @param responses
+ */
+ private void handleUntaggedResponses(List<ImapResponse> responses) {
+ for (ImapResponse response : responses) {
+ handleUntaggedResponse(response);
+ }
+ }
+
+ /**
+ * Handle an untagged response that the caller doesn't care to handle themselves.
+ * @param response
+ */
+ private void handleUntaggedResponse(ImapResponse response) {
+ if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+ mMessageCount = response.getStringOrEmpty(0).getNumberOrZero();
+ }
+ }
+
+ private static void parseBodyStructure(ImapList bs, Part part, String id)
+ throws MessagingException {
+ if (bs.getElementOrNone(0).isList()) {
+ /*
+ * This is a multipart/*
+ */
+ MimeMultipart mp = new MimeMultipart();
+ for (int i = 0, count = bs.size(); i < count; i++) {
+ ImapElement e = bs.getElementOrNone(i);
+ if (e.isList()) {
+ /*
+ * For each part in the message we're going to add a new BodyPart and parse
+ * into it.
+ */
+ MimeBodyPart bp = new MimeBodyPart();
+ if (id.equals(ImapConstants.TEXT)) {
+ parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1));
+
+ } else {
+ parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1));
+ }
+ mp.addBodyPart(bp);
+
+ } else {
+ if (e.isString()) {
+ mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US));
+ }
+ break; // Ignore the rest of the list.
+ }
+ }
+ part.setBody(mp);
+ } else {
+ /*
+ * This is a body. We need to add as much information as we can find out about
+ * it to the Part.
+ */
+
+ /*
+ body type
+ body subtype
+ body parameter parenthesized list
+ body id
+ body description
+ body encoding
+ body size
+ */
+
+ final ImapString type = bs.getStringOrEmpty(0);
+ final ImapString subType = bs.getStringOrEmpty(1);
+ final String mimeType =
+ (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US);
+
+ final ImapList bodyParams = bs.getListOrEmpty(2);
+ final ImapString cid = bs.getStringOrEmpty(3);
+ final ImapString encoding = bs.getStringOrEmpty(5);
+ final int size = bs.getStringOrEmpty(6).getNumberOrZero();
+
+ if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) {
+ // A body type of type MESSAGE and subtype RFC822
+ // contains, immediately after the basic fields, the
+ // envelope structure, body structure, and size in
+ // text lines of the encapsulated message.
+ // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL,
+ // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL]
+ /*
+ * This will be caught by fetch and handled appropriately.
+ */
+ throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822
+ + " not yet supported.");
+ }
+
+ /*
+ * Set the content type with as much information as we know right now.
+ */
+ final StringBuilder contentType = new StringBuilder(mimeType);
+
+ /*
+ * If there are body params we might be able to get some more information out
+ * of them.
+ */
+ for (int i = 1, count = bodyParams.size(); i < count; i += 2) {
+
+ // TODO We need to convert " into %22, but
+ // because MimeUtility.getHeaderParameter doesn't recognize it,
+ // we can't fix it for now.
+ contentType.append(String.format(";\n %s=\"%s\"",
+ bodyParams.getStringOrEmpty(i - 1).getString(),
+ bodyParams.getStringOrEmpty(i).getString()));
+ }
+
+ part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString());
+
+ // Extension items
+ final ImapList bodyDisposition;
+
+ if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) {
+ // If media-type is TEXT, 9th element might be: [body-fld-lines] := number
+ // So, if it's not a list, use 10th element.
+ // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.)
+ bodyDisposition = bs.getListOrEmpty(9);
+ } else {
+ bodyDisposition = bs.getListOrEmpty(8);
+ }
+
+ final StringBuilder contentDisposition = new StringBuilder();
+
+ if (bodyDisposition.size() > 0) {
+ final String bodyDisposition0Str =
+ bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US);
+ if (!TextUtils.isEmpty(bodyDisposition0Str)) {
+ contentDisposition.append(bodyDisposition0Str);
+ }
+
+ final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1);
+ if (!bodyDispositionParams.isEmpty()) {
+ /*
+ * If there is body disposition information we can pull some more
+ * information about the attachment out.
+ */
+ for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) {
+
+ // TODO We need to convert " into %22. See above.
+ contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"",
+ bodyDispositionParams.getStringOrEmpty(i - 1)
+ .getString().toLowerCase(Locale.US),
+ bodyDispositionParams.getStringOrEmpty(i).getString()));
+ }
+ }
+ }
+
+ if ((size > 0)
+ && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size")
+ == null)) {
+ contentDisposition.append(String.format(Locale.US, ";\n size=%d", size));
+ }
+
+ if (contentDisposition.length() > 0) {
+ /*
+ * Set the content disposition containing at least the size. Attachment
+ * handling code will use this down the road.
+ */
+ part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION,
+ contentDisposition.toString());
+ }
+
+ /*
+ * Set the Content-Transfer-Encoding header. Attachment code will use this
+ * to parse the body.
+ */
+ if (!encoding.isEmpty()) {
+ part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING,
+ encoding.getString());
+ }
+
+ /*
+ * Set the Content-ID header.
+ */
+ if (!cid.isEmpty()) {
+ part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString());
+ }
+
+ if (size > 0) {
+ if (part instanceof ImapMessage) {
+ ((ImapMessage) part).setSize(size);
+ } else if (part instanceof MimeBodyPart) {
+ ((MimeBodyPart) part).setSize(size);
+ } else {
+ throw new MessagingException("Unknown part type " + part.toString());
+ }
+ }
+ part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id);
+ }
+
+ }
+
+ public Message[] expunge() throws MessagingException {
+ checkOpen();
+ try {
+ handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE));
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return null;
+ }
+
+ public void setFlags(Message[] messages, String[] flags, boolean value)
+ throws MessagingException {
+ checkOpen();
+
+ String allFlags = "";
+ if (flags.length > 0) {
+ StringBuilder flagList = new StringBuilder();
+ for (int i = 0, count = flags.length; i < count; i++) {
+ String flag = flags[i];
+ if (flag == Flag.SEEN) {
+ flagList.append(" " + ImapConstants.FLAG_SEEN);
+ } else if (flag == Flag.DELETED) {
+ flagList.append(" " + ImapConstants.FLAG_DELETED);
+ } else if (flag == Flag.FLAGGED) {
+ flagList.append(" " + ImapConstants.FLAG_FLAGGED);
+ } else if (flag == Flag.ANSWERED) {
+ flagList.append(" " + ImapConstants.FLAG_ANSWERED);
+ }
+ }
+ allFlags = flagList.substring(1);
+ }
+ try {
+ mConnection.executeSimpleCommand(String.format(Locale.US,
+ ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)",
+ ImapStore.joinMessageUids(messages),
+ value ? "+" : "-",
+ allFlags));
+
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ }
+
+ /**
+ * Selects the folder for use. Before performing any operations on this folder, it
+ * must be selected.
+ */
+ private void doSelect() throws IOException, MessagingException {
+ final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+ String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName));
+
+ // Assume the folder is opened read-write; unless we are notified otherwise
+ mMode = MODE_READ_WRITE;
+ int messageCount = -1;
+ for (ImapResponse response : responses) {
+ if (response.isDataResponse(1, ImapConstants.EXISTS)) {
+ messageCount = response.getStringOrEmpty(0).getNumberOrZero();
+ } else if (response.isOk()) {
+ final ImapString responseCode = response.getResponseCodeOrEmpty();
+ if (responseCode.is(ImapConstants.READ_ONLY)) {
+ mMode = MODE_READ_ONLY;
+ } else if (responseCode.is(ImapConstants.READ_WRITE)) {
+ mMode = MODE_READ_WRITE;
+ }
+ } else if (response.isTagged()) { // Not OK
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED);
+ throw new MessagingException("Can't open mailbox: "
+ + response.getStatusResponseTextOrEmpty());
+ }
+ }
+ if (messageCount == -1) {
+ throw new MessagingException("Did not find message count during select");
+ }
+ mMessageCount = messageCount;
+ mExists = true;
+ }
+
+ public class Quota {
+
+ public final int occupied;
+ public final int total;
+
+ public Quota(int occupied, int total) {
+ this.occupied = occupied;
+ this.total = total;
+ }
+ }
+
+ public Quota getQuota() throws MessagingException {
+ try {
+ final List<ImapResponse> responses = mConnection.executeSimpleCommand(
+ String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName));
+
+ for (ImapResponse response : responses) {
+ if (!response.isDataResponse(0, ImapConstants.QUOTA)) {
+ continue;
+ }
+ ImapList list = response.getListOrEmpty(2);
+ for (int i = 0; i < list.size(); i += 3) {
+ if (!list.getStringOrEmpty(i).is("voice")) {
+ continue;
+ }
+ return new Quota(
+ list.getStringOrEmpty(i + 1).getNumber(-1),
+ list.getStringOrEmpty(i + 2).getNumber(-1));
+ }
+ }
+ } catch (IOException ioe) {
+ mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE);
+ throw ioExceptionHandler(mConnection, ioe);
+ } finally {
+ destroyResponses();
+ }
+ return null;
+ }
+
+ private void checkOpen() throws MessagingException {
+ if (!isOpen()) {
+ throw new MessagingException("Folder " + mName + " is not open.");
+ }
+ }
+
+ private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
+ LogUtils.d(TAG, "IO Exception detected: ", ioe);
+ connection.close();
+ if (connection == mConnection) {
+ mConnection = null; // To prevent close() from returning the connection to the pool.
+ close(false);
+ }
+ return new MessagingException(MessagingException.IOERROR, "IO Error", ioe);
+ }
+
+ public Message createMessage(String uid) {
+ return new ImapMessage(uid, this);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/ImapStore.java b/java/com/android/voicemailomtp/mail/store/ImapStore.java
new file mode 100644
index 000000000..f3e0c098e
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/ImapStore.java
@@ -0,0 +1,176 @@
+/*
+ * 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.voicemailomtp.mail.store;
+
+import android.content.Context;
+import android.net.Network;
+
+import com.android.voicemailomtp.mail.MailTransport;
+import com.android.voicemailomtp.mail.Message;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.internet.MimeMessage;
+import com.android.voicemailomtp.imap.ImapHelper;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImapStore {
+ /**
+ * A global suggestion to Store implementors on how much of the body
+ * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now.
+ */
+ public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024);
+ private final Context mContext;
+ private final ImapHelper mHelper;
+ private final String mUsername;
+ private final String mPassword;
+ private final MailTransport mTransport;
+ private ImapConnection mConnection;
+
+ public static final int FLAG_NONE = 0x00; // No flags
+ public static final int FLAG_SSL = 0x01; // Use SSL
+ public static final int FLAG_TLS = 0x02; // Use TLS
+ public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication
+ public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates
+ public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication
+
+ /**
+ * Contains all the information necessary to log into an imap server
+ */
+ public ImapStore(Context context, ImapHelper helper, String username, String password, int port,
+ String serverName, int flags, Network network) {
+ mContext = context;
+ mHelper = helper;
+ mUsername = username;
+ mPassword = password;
+ mTransport = new MailTransport(context, this.getImapHelper(),
+ network, serverName, port, flags);
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public ImapHelper getImapHelper() {
+ return mHelper;
+ }
+
+ public String getUsername() {
+ return mUsername;
+ }
+
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /** Returns a clone of the transport associated with this store. */
+ MailTransport cloneTransport() {
+ return mTransport.clone();
+ }
+
+ /**
+ * Returns UIDs of Messages joined with "," as the separator.
+ */
+ static String joinMessageUids(Message[] messages) {
+ StringBuilder sb = new StringBuilder();
+ boolean notFirst = false;
+ for (Message m : messages) {
+ if (notFirst) {
+ sb.append(',');
+ }
+ sb.append(m.getUid());
+ notFirst = true;
+ }
+ return sb.toString();
+ }
+
+ static class ImapMessage extends MimeMessage {
+ private ImapFolder mFolder;
+
+ ImapMessage(String uid, ImapFolder folder) {
+ mUid = uid;
+ mFolder = folder;
+ }
+
+ public void setSize(int size) {
+ mSize = size;
+ }
+
+ @Override
+ public void parse(InputStream in) throws IOException, MessagingException {
+ super.parse(in);
+ }
+
+ public void setFlagInternal(String flag, boolean set) throws MessagingException {
+ super.setFlag(flag, set);
+ }
+
+ @Override
+ public void setFlag(String flag, boolean set) throws MessagingException {
+ super.setFlag(flag, set);
+ mFolder.setFlags(new Message[] { this }, new String[] { flag }, set);
+ }
+ }
+
+ static class ImapException extends MessagingException {
+ private static final long serialVersionUID = 1L;
+
+ private final String mStatus;
+ private final String mStatusMessage;
+ private final String mAlertText;
+ private final String mResponseCode;
+
+ public ImapException(String message, String status, String statusMessage, String alertText,
+ String responseCode) {
+ super(message);
+ mStatus = status;
+ mStatusMessage = statusMessage;
+ mAlertText = alertText;
+ mResponseCode = responseCode;
+ }
+
+ public String getStatus() {
+ return mStatus;
+ }
+
+ public String getStatusMessage() {
+ return mStatusMessage;
+ }
+
+ public String getAlertText() {
+ return mAlertText;
+ }
+
+ public String getResponseCode() {
+ return mResponseCode;
+ }
+ }
+
+ public void closeConnection() {
+ if (mConnection != null) {
+ mConnection.close();
+ mConnection = null;
+ }
+ }
+
+ public ImapConnection getConnection() {
+ if (mConnection == null) {
+ mConnection = new ImapConnection(this);
+ }
+ return mConnection;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java
new file mode 100644
index 000000000..b78f55293
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java
@@ -0,0 +1,335 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import android.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.util.ArrayMap;
+import android.util.Base64;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.mail.MailTransport;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.store.ImapStore;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class DigestMd5Utils {
+
+ private static final String TAG = "DigestMd5Utils";
+
+ private static final String DIGEST_CHARSET = "CHARSET";
+ private static final String DIGEST_USERNAME = "username";
+ private static final String DIGEST_REALM = "realm";
+ private static final String DIGEST_NONCE = "nonce";
+ private static final String DIGEST_NC = "nc";
+ private static final String DIGEST_CNONCE = "cnonce";
+ private static final String DIGEST_URI = "digest-uri";
+ private static final String DIGEST_RESPONSE = "response";
+ private static final String DIGEST_QOP = "qop";
+
+ private static final String RESPONSE_AUTH_HEADER = "rspauth=";
+ private static final String HEX_CHARS = "0123456789abcdef";
+
+ /**
+ * Represents the set of data we need to generate the DIGEST-MD5 response.
+ */
+ public static class Data {
+
+ private static final String CHARSET = "utf-8";
+
+ public String username;
+ public String password;
+ public String realm;
+ public String nonce;
+ public String nc;
+ public String cnonce;
+ public String digestUri;
+ public String qop;
+
+ @VisibleForTesting
+ Data() {
+ // Do nothing
+ }
+
+ public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) {
+ username = imapStore.getUsername();
+ password = imapStore.getPassword();
+ realm = challenge.getOrDefault(DIGEST_REALM, "");
+ nonce = challenge.get(DIGEST_NONCE);
+ cnonce = createCnonce();
+ nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1.
+ qop = "auth"; // Other config not supported
+ digestUri = "imap/" + transport.getHost();
+ }
+
+ private static String createCnonce() {
+ SecureRandom generator = new SecureRandom();
+
+ // At least 64 bits of entropy is required
+ byte[] rawBytes = new byte[8];
+ generator.nextBytes(rawBytes);
+
+ return Base64.encodeToString(rawBytes, Base64.NO_WRAP);
+ }
+
+ /**
+ * Verify the response-auth returned by the server is correct.
+ */
+ public void verifyResponseAuth(String response)
+ throws MessagingException {
+ if (!response.startsWith(RESPONSE_AUTH_HEADER)) {
+ throw new MessagingException("response-auth expected");
+ }
+ if (!response.substring(RESPONSE_AUTH_HEADER.length())
+ .equals(DigestMd5Utils.getResponse(this, true))) {
+ throw new MessagingException("invalid response-auth return from the server.");
+ }
+ }
+
+ public String createResponse() {
+ String response = getResponse(this, false);
+ ResponseBuilder builder = new ResponseBuilder();
+ builder
+ .append(DIGEST_CHARSET, CHARSET)
+ .appendQuoted(DIGEST_USERNAME, username)
+ .appendQuoted(DIGEST_REALM, realm)
+ .appendQuoted(DIGEST_NONCE, nonce)
+ .append(DIGEST_NC, nc)
+ .appendQuoted(DIGEST_CNONCE, cnonce)
+ .appendQuoted(DIGEST_URI, digestUri)
+ .append(DIGEST_RESPONSE, response)
+ .append(DIGEST_QOP, qop);
+ return builder.toString();
+ }
+
+ private static class ResponseBuilder {
+
+ private StringBuilder mBuilder = new StringBuilder();
+
+ public ResponseBuilder appendQuoted(String key, String value) {
+ if (mBuilder.length() != 0) {
+ mBuilder.append(",");
+ }
+ mBuilder.append(key).append("=\"").append(value).append("\"");
+ return this;
+ }
+
+ public ResponseBuilder append(String key, String value) {
+ if (mBuilder.length() != 0) {
+ mBuilder.append(",");
+ }
+ mBuilder.append(key).append("=").append(value);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return mBuilder.toString();
+ }
+ }
+ }
+
+ /*
+ response-value =
+ toHex( getKeyDigest ( toHex(getMd5(a1)),
+ { nonce-value, ":" nc-value, ":",
+ cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) }))
+ * @param isResponseAuth is the response the one the server is returning us. response-auth has
+ * different a2 format.
+ */
+ @VisibleForTesting
+ static String getResponse(Data data, boolean isResponseAuth) {
+ StringBuilder a1 = new StringBuilder();
+ a1.append(new String(
+ getMd5(data.username + ":" + data.realm + ":" + data.password),
+ StandardCharsets.ISO_8859_1));
+ a1.append(":").append(data.nonce).append(":").append(data.cnonce);
+
+ StringBuilder a2 = new StringBuilder();
+ if (!isResponseAuth) {
+ a2.append("AUTHENTICATE");
+ }
+ a2.append(":").append(data.digestUri);
+
+ return toHex(getKeyDigest(
+ toHex(getMd5(a1.toString())),
+ data.nonce + ":" + data.nc + ":" + data.cnonce + ":" + data.qop + ":" + toHex(
+ getMd5(a2.toString()))
+ ));
+ }
+
+ /**
+ * Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s.
+ */
+ private static byte[] getMd5(String s) {
+ try {
+ MessageDigest digester = MessageDigest.getInstance("MD5");
+ digester.update(s.getBytes(StandardCharsets.ISO_8859_1));
+ return digester.digest();
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /**
+ * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon and the
+ * string s.
+ */
+ private static byte[] getKeyDigest(String k, String s) {
+ StringBuilder builder = new StringBuilder(k).append(":").append(s);
+ return getMd5(builder.toString());
+ }
+
+ /**
+ * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits
+ * (with alphabetic characters always in lower case, since MD5 is case sensitive).
+ */
+ private static String toHex(byte[] n) {
+ StringBuilder result = new StringBuilder();
+ for (byte b : n) {
+ int unsignedByte = b & 0xFF;
+ result.append(HEX_CHARS.charAt(unsignedByte / 16))
+ .append(HEX_CHARS.charAt(unsignedByte % 16));
+ }
+ return result.toString();
+ }
+
+ public static Map<String, String> parseDigestMessage(String message) throws MessagingException {
+ Map<String, String> result = new DigestMessageParser(message).parse();
+ if (!result.containsKey(DIGEST_NONCE)) {
+ throw new MessagingException("nonce missing from server DIGEST-MD5 challenge");
+ }
+ return result;
+ }
+
+ /**
+ * Parse the key-value pair returned by the server.
+ */
+ private static class DigestMessageParser {
+
+ private final String mMessage;
+ private int mPosition = 0;
+ private Map<String, String> mResult = new ArrayMap<>();
+
+ public DigestMessageParser(String message) {
+ mMessage = message;
+ }
+
+ @Nullable
+ public Map<String, String> parse() {
+ try {
+ while (mPosition < mMessage.length()) {
+ parsePair();
+ if (mPosition != mMessage.length()) {
+ expect(',');
+ }
+ }
+ } catch (IndexOutOfBoundsException e) {
+ VvmLog.e(TAG, e.toString());
+ return null;
+ }
+ return mResult;
+ }
+
+ private void parsePair() {
+ String key = parseKey();
+ expect('=');
+ String value = parseValue();
+ mResult.put(key, value);
+ }
+
+ private void expect(char c) {
+ if (pop() != c) {
+ throw new IllegalStateException(
+ "unexpected character " + mMessage.charAt(mPosition));
+ }
+ }
+
+ private char pop() {
+ char result = peek();
+ mPosition++;
+ return result;
+ }
+
+ private char peek() {
+ return mMessage.charAt(mPosition);
+ }
+
+ private void goToNext(char c) {
+ while (peek() != c) {
+ mPosition++;
+ }
+ }
+
+ private String parseKey() {
+ int start = mPosition;
+ goToNext('=');
+ return mMessage.substring(start, mPosition);
+ }
+
+ private String parseValue() {
+ if (peek() == '"') {
+ return parseQuotedValue();
+ } else {
+ return parseUnquotedValue();
+ }
+ }
+
+ private String parseQuotedValue() {
+ expect('"');
+ StringBuilder result = new StringBuilder();
+ while (true) {
+ char c = pop();
+ if (c == '\\') {
+ result.append(pop());
+ } else if (c == '"') {
+ break;
+ } else {
+ result.append(c);
+ }
+ }
+ return result.toString();
+ }
+
+ private String parseUnquotedValue() {
+ StringBuilder result = new StringBuilder();
+ while (true) {
+ char c = pop();
+ if (c == '\\') {
+ result.append(pop());
+ } else if (c == ',') {
+ mPosition--;
+ break;
+ } else {
+ result.append(c);
+ }
+
+ if (mPosition == mMessage.length()) {
+ break;
+ }
+ }
+ return result.toString();
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java b/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java
new file mode 100644
index 000000000..d8e75752f
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java
@@ -0,0 +1,144 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import com.android.voicemailomtp.mail.store.ImapStore;
+
+import java.util.Locale;
+
+public final class ImapConstants {
+ private ImapConstants() {}
+
+ public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK";
+ public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]";
+ public static final String FETCH_FIELD_BODY_PEEK_SANE = String.format(
+ Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE);
+ public static final String FETCH_FIELD_HEADERS =
+ "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]";
+
+ public static final String ALERT = "ALERT";
+ public static final String APPEND = "APPEND";
+ public static final String AUTHENTICATE = "AUTHENTICATE";
+ public static final String BAD = "BAD";
+ public static final String BADCHARSET = "BADCHARSET";
+ public static final String BODY = "BODY";
+ public static final String BODY_BRACKET_HEADER = "BODY[HEADER";
+ public static final String BODYSTRUCTURE = "BODYSTRUCTURE";
+ public static final String BYE = "BYE";
+ public static final String CAPABILITY = "CAPABILITY";
+ public static final String CHECK = "CHECK";
+ public static final String CLOSE = "CLOSE";
+ public static final String COPY = "COPY";
+ public static final String COPYUID = "COPYUID";
+ public static final String CREATE = "CREATE";
+ public static final String DELETE = "DELETE";
+ public static final String EXAMINE = "EXAMINE";
+ public static final String EXISTS = "EXISTS";
+ public static final String EXPUNGE = "EXPUNGE";
+ public static final String FETCH = "FETCH";
+ public static final String FLAG_ANSWERED = "\\ANSWERED";
+ public static final String FLAG_DELETED = "\\DELETED";
+ public static final String FLAG_FLAGGED = "\\FLAGGED";
+ public static final String FLAG_NO_SELECT = "\\NOSELECT";
+ public static final String FLAG_SEEN = "\\SEEN";
+ public static final String FLAGS = "FLAGS";
+ public static final String FLAGS_SILENT = "FLAGS.SILENT";
+ public static final String ID = "ID";
+ public static final String INBOX = "INBOX";
+ public static final String INTERNALDATE = "INTERNALDATE";
+ public static final String LIST = "LIST";
+ public static final String LOGIN = "LOGIN";
+ public static final String LOGOUT = "LOGOUT";
+ public static final String LSUB = "LSUB";
+ public static final String NAMESPACE = "NAMESPACE";
+ public static final String NO = "NO";
+ public static final String NOOP = "NOOP";
+ public static final String OK = "OK";
+ public static final String PARSE = "PARSE";
+ public static final String PERMANENTFLAGS = "PERMANENTFLAGS";
+ public static final String PREAUTH = "PREAUTH";
+ public static final String READ_ONLY = "READ-ONLY";
+ public static final String READ_WRITE = "READ-WRITE";
+ public static final String RENAME = "RENAME";
+ public static final String RFC822_SIZE = "RFC822.SIZE";
+ public static final String SEARCH = "SEARCH";
+ public static final String SELECT = "SELECT";
+ public static final String STARTTLS = "STARTTLS";
+ public static final String STATUS = "STATUS";
+ public static final String STORE = "STORE";
+ public static final String SUBSCRIBE = "SUBSCRIBE";
+ public static final String TEXT = "TEXT";
+ public static final String TRYCREATE = "TRYCREATE";
+ public static final String UID = "UID";
+ public static final String UID_COPY = "UID COPY";
+ public static final String UID_FETCH = "UID FETCH";
+ public static final String UID_SEARCH = "UID SEARCH";
+ public static final String UID_STORE = "UID STORE";
+ public static final String UIDNEXT = "UIDNEXT";
+ public static final String UIDPLUS = "UIDPLUS";
+ public static final String UIDVALIDITY = "UIDVALIDITY";
+ public static final String UNSEEN = "UNSEEN";
+ public static final String UNSUBSCRIBE = "UNSUBSCRIBE";
+ public static final String XOAUTH2 = "XOAUTH2";
+ public static final String APPENDUID = "APPENDUID";
+ public static final String NIL = "NIL";
+
+ /**
+ * NO responses
+ */
+ public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed";
+ public static final String NO_RESERVATION_FAILED = "reservation failed";
+ public static final String NO_APPLICATION_ERROR = "application error";
+ public static final String NO_INVALID_PARAMETER = "invalid parameter";
+ public static final String NO_INVALID_COMMAND = "invalid command";
+ public static final String NO_UNKNOWN_COMMAND = "unknown command";
+ // AUTHENTICATE
+ // The subscriber can not be located in the system.
+ public static final String NO_UNKNOWN_USER = "unknown user";
+ // The Client Type or Protocol Version is unknown.
+ public static final String NO_UNKNOWN_CLIENT = "unknown client";
+ // The password received from the client does not match the password defined in the subscriber's profile.
+ public static final String NO_INVALID_PASSWORD = "invalid password";
+ // The subscriber's mailbox has not yet been initialised via the TUI
+ public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized";
+ // The subscriber has not been provisioned for the VVM service.
+ public static final String NO_SERVICE_IS_NOT_PROVISIONED =
+ "service is not provisioned";
+ // The subscriber is provisioned for the VVM service but the VVM service is currently not active
+ public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated";
+ // The Voice Mail Blocked flag in the subscriber's profile is set to YES.
+ public static final String NO_USER_IS_BLOCKED = "user is blocked";
+
+ /**
+ * extensions
+ */
+ public static final String GETQUOTA = "GETQUOTA";
+ public static final String GETQUOTAROOT = "GETQUOTAROOT";
+ public static final String QUOTAROOT = "QUOTAROOT";
+ public static final String QUOTA = "QUOTA";
+
+ /**
+ * capabilities
+ */
+ public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5";
+ public static final String CAPABILITY_STARTTLS = "STARTTLS";
+
+ /**
+ * authentication
+ */
+ public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5";
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java b/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java
new file mode 100644
index 000000000..9f272e31c
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java
@@ -0,0 +1,120 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+/**
+ * Class representing "element"s in IMAP responses.
+ *
+ * <p>Class hierarchy:
+ * <pre>
+ * ImapElement
+ * |
+ * |-- ImapElement.NONE (for 'index out of range')
+ * |
+ * |-- ImapList (isList() == true)
+ * | |
+ * | |-- ImapList.EMPTY
+ * | |
+ * | --- ImapResponse
+ * |
+ * --- ImapString (isString() == true)
+ * |
+ * |-- ImapString.EMPTY
+ * |
+ * |-- ImapSimpleString
+ * |
+ * |-- ImapMemoryLiteral
+ * |
+ * --- ImapTempFileLiteral
+ * </pre>
+ */
+public abstract class ImapElement {
+ /**
+ * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index
+ * is out of range.
+ */
+ public static final ImapElement NONE = new ImapElement() {
+ @Override public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override public boolean isList() {
+ return false;
+ }
+
+ @Override public boolean isString() {
+ return false;
+ }
+
+ @Override public String toString() {
+ return "[NO ELEMENT]";
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ return super.equalsForTest(that);
+ }
+ };
+
+ private boolean mDestroyed = false;
+
+ public abstract boolean isList();
+
+ public abstract boolean isString();
+
+ protected boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ /**
+ * Clean up the resources used by the instance.
+ * It's for removing a temp file used by {@link ImapTempFileLiteral}.
+ */
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ /**
+ * Throws {@link RuntimeException} if it's already destroyed.
+ */
+ protected final void checkNotDestroyed() {
+ if (mDestroyed) {
+ throw new RuntimeException("Already destroyed");
+ }
+ }
+
+ /**
+ * Return a string that represents this object; it's purely for the debug purpose. Don't
+ * mistake it for {@link ImapString#getString}.
+ *
+ * Abstract to force subclasses to implement it.
+ */
+ @Override
+ public abstract String toString();
+
+ /**
+ * The equals implementation that is intended to be used only for unit testing.
+ * (Because it may be heavy and has a special sense of "equal" for testing.)
+ */
+ public boolean equalsForTest(ImapElement that) {
+ if (that == null) {
+ return false;
+ }
+ return this.getClass() == that.getClass(); // Has to be the same class.
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapList.java b/java/com/android/voicemailomtp/mail/store/imap/ImapList.java
new file mode 100644
index 000000000..970423cbd
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapList.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.voicemailomtp.mail.store.imap;
+
+import java.util.ArrayList;
+
+/**
+ * Class represents an IMAP list.
+ */
+public class ImapList extends ImapElement {
+ /**
+ * {@link ImapList} representing an empty list.
+ */
+ public static final ImapList EMPTY = new ImapList() {
+ @Override public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override void add(ImapElement e) {
+ throw new RuntimeException();
+ }
+ };
+
+ private ArrayList<ImapElement> mList = new ArrayList<ImapElement>();
+
+ /* package */ void add(ImapElement e) {
+ if (e == null) {
+ throw new RuntimeException("Can't add null");
+ }
+ mList.add(e);
+ }
+
+ @Override
+ public final boolean isString() {
+ return false;
+ }
+
+ @Override
+ public final boolean isList() {
+ return true;
+ }
+
+ public final int size() {
+ return mList.size();
+ }
+
+ public final boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
+ * Return true if the element at {@code index} exists, is string, and equals to {@code s}.
+ * (case insensitive)
+ */
+ public final boolean is(int index, String s) {
+ return is(index, s, false);
+ }
+
+ /**
+ * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}.
+ */
+ public final boolean is(int index, String s, boolean prefixMatch) {
+ if (!prefixMatch) {
+ return getStringOrEmpty(index).is(s);
+ } else {
+ return getStringOrEmpty(index).startsWith(s);
+ }
+ }
+
+ /**
+ * Return the element at {@code index}.
+ * If {@code index} is out of range, returns {@link ImapElement#NONE}.
+ */
+ public final ImapElement getElementOrNone(int index) {
+ return (index >= mList.size()) ? ImapElement.NONE : mList.get(index);
+ }
+
+ /**
+ * Return the element at {@code index} if it's a list.
+ * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}.
+ */
+ public final ImapList getListOrEmpty(int index) {
+ ImapElement el = getElementOrNone(index);
+ return el.isList() ? (ImapList) el : EMPTY;
+ }
+
+ /**
+ * Return the element at {@code index} if it's a string.
+ * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}.
+ */
+ public final ImapString getStringOrEmpty(int index) {
+ ImapElement el = getElementOrNone(index);
+ return el.isString() ? (ImapString) el : ImapString.EMPTY;
+ }
+
+ /**
+ * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be
+ * at an even index.
+ */
+ /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) {
+ for (int i = 1; i < size(); i += 2) {
+ if (is(i-1, key, prefixMatch)) {
+ return mList.get(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return an {@link ImapList} keyed by {@code key}.
+ * Return {@link ImapList#EMPTY} if not found.
+ */
+ public final ImapList getKeyedListOrEmpty(String key) {
+ return getKeyedListOrEmpty(key, false);
+ }
+
+ /**
+ * Return an {@link ImapList} keyed by {@code key}.
+ * Return {@link ImapList#EMPTY} if not found.
+ */
+ public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) {
+ ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+ return (e != null) ? ((ImapList) e) : ImapList.EMPTY;
+ }
+
+ /**
+ * Return an {@link ImapString} keyed by {@code key}.
+ * Return {@link ImapString#EMPTY} if not found.
+ */
+ public final ImapString getKeyedStringOrEmpty(String key) {
+ return getKeyedStringOrEmpty(key, false);
+ }
+
+ /**
+ * Return an {@link ImapString} keyed by {@code key}.
+ * Return {@link ImapString#EMPTY} if not found.
+ */
+ public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) {
+ ImapElement e = getKeyedElementOrNull(key, prefixMatch);
+ return (e != null) ? ((ImapString) e) : ImapString.EMPTY;
+ }
+
+ /**
+ * Return true if it contains {@code s}.
+ */
+ public final boolean contains(String s) {
+ for (int i = 0; i < size(); i++) {
+ if (getStringOrEmpty(i).is(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void destroy() {
+ if (mList != null) {
+ for (ImapElement e : mList) {
+ e.destroy();
+ }
+ mList = null;
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return mList.toString();
+ }
+
+ /**
+ * Return the text representations of the contents concatenated with ",".
+ */
+ public final String flatten() {
+ return flatten(new StringBuilder()).toString();
+ }
+
+ /**
+ * Returns text representations (i.e. getString()) of contents joined together with
+ * "," as the separator.
+ *
+ * Only used for building the capability string passed to vendor policies.
+ *
+ * We can't use toString(), because it's for debugging (meaning the format may change any time),
+ * and it won't expand literals.
+ */
+ private final StringBuilder flatten(StringBuilder sb) {
+ sb.append('[');
+ for (int i = 0; i < mList.size(); i++) {
+ if (i > 0) {
+ sb.append(',');
+ }
+ final ImapElement e = getElementOrNone(i);
+ if (e.isList()) {
+ getListOrEmpty(i).flatten(sb);
+ } else if (e.isString()) {
+ sb.append(getStringOrEmpty(i).getString());
+ }
+ }
+ sb.append(']');
+ return sb;
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ ImapList thatList = (ImapList) that;
+ if (size() != thatList.size()) {
+ return false;
+ }
+ for (int i = 0; i < size(); i++) {
+ if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java
new file mode 100644
index 000000000..ad60ca7a4
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.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.voicemailomtp.mail.store.imap;
+
+import com.android.voicemailomtp.mail.FixedLengthInputStream;
+import com.android.voicemailomtp.VvmLog;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by an in-memory byte array.
+ */
+public class ImapMemoryLiteral extends ImapString {
+ private final String TAG = "ImapMemoryLiteral";
+ private byte[] mData;
+
+ /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException {
+ // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary
+ // copy....
+ mData = new byte[in.getLength()];
+ int pos = 0;
+ while (pos < mData.length) {
+ int read = in.read(mData, pos, mData.length - pos);
+ if (read < 0) {
+ break;
+ }
+ pos += read;
+ }
+ if (pos != mData.length) {
+ VvmLog.w(TAG, "length mismatch");
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mData = null;
+ super.destroy();
+ }
+
+ @Override
+ public String getString() {
+ try {
+ return new String(mData, "US-ASCII");
+ } catch (UnsupportedEncodingException e) {
+ VvmLog.e(TAG, "Unsupported encoding: ", e);
+ }
+ return null;
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ return new ByteArrayInputStream(mData);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(memory)}", mData.length);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java
new file mode 100644
index 000000000..412f16d8a
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java
@@ -0,0 +1,158 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+/**
+ * Class represents an IMAP response.
+ */
+public class ImapResponse extends ImapList {
+ private final String mTag;
+ private final boolean mIsContinuationRequest;
+
+ /* package */ ImapResponse(String tag, boolean isContinuationRequest) {
+ mTag = tag;
+ mIsContinuationRequest = isContinuationRequest;
+ }
+
+ /* package */ static boolean isStatusResponse(String symbol) {
+ return ImapConstants.OK.equalsIgnoreCase(symbol)
+ || ImapConstants.NO.equalsIgnoreCase(symbol)
+ || ImapConstants.BAD.equalsIgnoreCase(symbol)
+ || ImapConstants.PREAUTH.equalsIgnoreCase(symbol)
+ || ImapConstants.BYE.equalsIgnoreCase(symbol);
+ }
+
+ /**
+ * @return whether it's a tagged response.
+ */
+ public boolean isTagged() {
+ return mTag != null;
+ }
+
+ /**
+ * @return whether it's a continuation request.
+ */
+ public boolean isContinuationRequest() {
+ return mIsContinuationRequest;
+ }
+
+ public boolean isStatusResponse() {
+ return isStatusResponse(getStringOrEmpty(0).getString());
+ }
+
+ /**
+ * @return whether it's an OK response.
+ */
+ public boolean isOk() {
+ return is(0, ImapConstants.OK);
+ }
+
+ /**
+ * @return whether it's an BAD response.
+ */
+ public boolean isBad() {
+ return is(0, ImapConstants.BAD);
+ }
+
+ /**
+ * @return whether it's an NO response.
+ */
+ public boolean isNo() {
+ return is(0, ImapConstants.NO);
+ }
+
+ /**
+ * @return whether it's an {@code responseType} data response. (i.e. not tagged).
+ * @param index where {@code responseType} should appear. e.g. 1 for "FETCH"
+ * @param responseType e.g. "FETCH"
+ */
+ public final boolean isDataResponse(int index, String responseType) {
+ return !isTagged() && getStringOrEmpty(index).is(responseType);
+ }
+
+ /**
+ * @return Response code (RFC 3501 7.1) if it's a status response.
+ *
+ * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes"
+ */
+ public ImapString getResponseCodeOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY; // Not a status response.
+ }
+ return getListOrEmpty(1).getStringOrEmpty(0);
+ }
+
+ /**
+ * @return Alert message it it has ALERT response code.
+ *
+ * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes"
+ */
+ public ImapString getAlertTextOrEmpty() {
+ if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) {
+ return ImapString.EMPTY; // Not an ALERT
+ }
+ // The 3rd element contains all the rest of line.
+ return getStringOrEmpty(2);
+ }
+
+ /**
+ * @return Response text in a status response.
+ */
+ public ImapString getStatusResponseTextOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY;
+ }
+ return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1);
+ }
+
+ public ImapString getStatusOrEmpty() {
+ if (!isStatusResponse()) {
+ return ImapString.EMPTY;
+ }
+ return getStringOrEmpty(0);
+ }
+
+ @Override
+ public String toString() {
+ String tag = mTag;
+ if (isContinuationRequest()) {
+ tag = "+";
+ }
+ return "#" + tag + "# " + super.toString();
+ }
+
+ @Override
+ public boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ final ImapResponse thatResponse = (ImapResponse) that;
+ if (mTag == null) {
+ if (thatResponse.mTag != null) {
+ return false;
+ }
+ } else {
+ if (!mTag.equals(thatResponse.mTag)) {
+ return false;
+ }
+ }
+ if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) {
+ return false;
+ }
+ return true;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java
new file mode 100644
index 000000000..692596f14
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java
@@ -0,0 +1,432 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.voicemailomtp.mail.FixedLengthInputStream;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.mail.PeekableInputStream;
+import com.android.voicemailomtp.VvmLog;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+
+/**
+ * IMAP response parser.
+ */
+public class ImapResponseParser {
+ private static final String TAG = "ImapResponseParser";
+
+ /**
+ * Literal larger than this will be stored in temp file.
+ */
+ public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;
+
+ /** Input stream */
+ private final PeekableInputStream mIn;
+
+ private final int mLiteralKeepInMemoryThreshold;
+
+ /** StringBuilder used by readUntil() */
+ private final StringBuilder mBufferReadUntil = new StringBuilder();
+
+ /** StringBuilder used by parseBareString() */
+ private final StringBuilder mParseBareString = new StringBuilder();
+
+ /**
+ * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from
+ * time to time to destroy them and clear it.
+ */
+ private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
+
+ /**
+ * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated
+ * in the same way EOF does.
+ */
+ public static class ByeException extends IOException {
+ public static final String MESSAGE = "Received BYE";
+ public ByeException() {
+ super(MESSAGE);
+ }
+ }
+
+ /**
+ * Public constructor for normal use.
+ */
+ public ImapResponseParser(InputStream in) {
+ this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
+ }
+
+ /**
+ * Constructor for testing to override the literal size threshold.
+ */
+ /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) {
+ mIn = new PeekableInputStream(in);
+ mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
+ }
+
+ private static IOException newEOSException() {
+ final String message = "End of stream reached";
+ VvmLog.d(TAG, message);
+ return new IOException(message);
+ }
+
+ /**
+ * Peek next one byte.
+ *
+ * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
+ * we shouldn't see EOF during parsing.
+ */
+ private int peek() throws IOException {
+ final int next = mIn.peek();
+ if (next == -1) {
+ throw newEOSException();
+ }
+ return next;
+ }
+
+ /**
+ * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
+ *
+ * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n,
+ * we shouldn't see EOF during parsing.
+ */
+ private int readByte() throws IOException {
+ int next = mIn.read();
+ if (next == -1) {
+ throw newEOSException();
+ }
+ return next;
+ }
+
+ /**
+ * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
+ *
+ * @see #readResponse()
+ */
+ public void destroyResponses() {
+ for (ImapResponse r : mResponsesToDestroy) {
+ r.destroy();
+ }
+ mResponsesToDestroy.clear();
+ }
+
+ /**
+ * Reads the next response available on the stream and returns an
+ * {@link ImapResponse} object that represents it.
+ *
+ * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
+ * is stored in the internal storage. When the {@link ImapResponse} is no longer used
+ * {@link #destroyResponses} should be called to destroy all the responses in the array.
+ *
+ * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done
+ * and {@link ByeException} will be thrown.
+ * @return the parsed {@link ImapResponse} object.
+ * @exception ByeException when detects BYE and <code>byeExpected</code> is false.
+ */
+ public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException {
+ ImapResponse response = null;
+ try {
+ response = parseResponse();
+ } catch (RuntimeException e) {
+ // Parser crash -- log network activities.
+ onParseError(e);
+ throw e;
+ } catch (IOException e) {
+ // Network error, or received an unexpected char.
+ onParseError(e);
+ throw e;
+ }
+
+ // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE.
+ if (!byeExpected && response.is(0, ImapConstants.BYE)) {
+ Log.w(TAG, ByeException.MESSAGE);
+ response.destroy();
+ throw new ByeException();
+ }
+ mResponsesToDestroy.add(response);
+ return response;
+ }
+
+ private void onParseError(Exception e) {
+ // Read a few more bytes, so that the log will contain some more context, even if the parser
+ // crashes in the middle of a response.
+ // This also makes sure the byte in question will be logged, no matter where it crashes.
+ // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
+ // before actually reading it.
+ // However, we don't want to read too much, because then it may get into an email message.
+ try {
+ for (int i = 0; i < 4; i++) {
+ int b = readByte();
+ if (b == -1 || b == '\n') {
+ break;
+ }
+ }
+ } catch (IOException ignore) {
+ }
+ VvmLog.w(TAG, "Exception detected: " + e.getMessage());
+ }
+
+ /**
+ * Read next byte from stream and throw it away. If the byte is different from {@code expected}
+ * throw {@link MessagingException}.
+ */
+ /* package for test */ void expect(char expected) throws IOException {
+ final int next = readByte();
+ if (expected != next) {
+ throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
+ (int) expected, expected, next, (char) next));
+ }
+ }
+
+ /**
+ * Read bytes until we find {@code end}, and return all as string.
+ * The {@code end} will be read (rather than peeked) and won't be included in the result.
+ */
+ /* package for test */ String readUntil(char end) throws IOException {
+ mBufferReadUntil.setLength(0);
+ for (;;) {
+ final int ch = readByte();
+ if (ch != end) {
+ mBufferReadUntil.append((char) ch);
+ } else {
+ return mBufferReadUntil.toString();
+ }
+ }
+ }
+
+ /**
+ * Read all bytes until \r\n.
+ */
+ /* package */ String readUntilEol() throws IOException {
+ String ret = readUntil('\r');
+ expect('\n'); // TODO Should this really be error?
+ return ret;
+ }
+
+ /**
+ * Parse and return the response line.
+ */
+ private ImapResponse parseResponse() throws IOException, MessagingException {
+ // We need to destroy the response if we get an exception.
+ // So, we first store the response that's being built in responseToDestroy, until it's
+ // completely built, at which point we copy it into responseToReturn and null out
+ // responseToDestroyt.
+ // If responseToDestroy is not null in finally, we destroy it because that means
+ // we got an exception somewhere.
+ ImapResponse responseToDestroy = null;
+ final ImapResponse responseToReturn;
+
+ try {
+ final int ch = peek();
+ if (ch == '+') { // Continuation request
+ readByte(); // skip +
+ expect(' ');
+ responseToDestroy = new ImapResponse(null, true);
+
+ // If it's continuation request, we don't really care what's in it.
+ responseToDestroy.add(new ImapSimpleString(readUntilEol()));
+
+ // Response has successfully been built. Let's return it.
+ responseToReturn = responseToDestroy;
+ responseToDestroy = null;
+ } else {
+ // Status response or response data
+ final String tag;
+ if (ch == '*') {
+ tag = null;
+ readByte(); // skip *
+ expect(' ');
+ } else {
+ tag = readUntil(' ');
+ }
+ responseToDestroy = new ImapResponse(tag, false);
+
+ final ImapString firstString = parseBareString();
+ responseToDestroy.add(firstString);
+
+ // parseBareString won't eat a space after the string, so we need to skip it,
+ // if exists.
+ // If the next char is not ' ', it should be EOL.
+ if (peek() == ' ') {
+ readByte(); // skip ' '
+
+ if (responseToDestroy.isStatusResponse()) { // It's a status response
+
+ // Is there a response code?
+ final int next = peek();
+ if (next == '[') {
+ responseToDestroy.add(parseList('[', ']'));
+ if (peek() == ' ') { // Skip following space
+ readByte();
+ }
+ }
+
+ String rest = readUntilEol();
+ if (!TextUtils.isEmpty(rest)) {
+ // The rest is free-form text.
+ responseToDestroy.add(new ImapSimpleString(rest));
+ }
+ } else { // It's a response data.
+ parseElements(responseToDestroy, '\0');
+ }
+ } else {
+ expect('\r');
+ expect('\n');
+ }
+
+ // Response has successfully been built. Let's return it.
+ responseToReturn = responseToDestroy;
+ responseToDestroy = null;
+ }
+ } finally {
+ if (responseToDestroy != null) {
+ // We get an exception.
+ responseToDestroy.destroy();
+ }
+ }
+
+ return responseToReturn;
+ }
+
+ private ImapElement parseElement() throws IOException, MessagingException {
+ final int next = peek();
+ switch (next) {
+ case '(':
+ return parseList('(', ')');
+ case '[':
+ return parseList('[', ']');
+ case '"':
+ readByte(); // Skip "
+ return new ImapSimpleString(readUntil('"'));
+ case '{':
+ return parseLiteral();
+ case '\r': // CR
+ readByte(); // Consume \r
+ expect('\n'); // Should be followed by LF.
+ return null;
+ case '\n': // LF // There shouldn't be a bare LF, but just in case.
+ readByte(); // Consume \n
+ return null;
+ default:
+ return parseBareString();
+ }
+ }
+
+ /**
+ * Parses an atom.
+ *
+ * Special case: If an atom contains '[', everything until the next ']' will be considered
+ * a part of the atom.
+ * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
+ *
+ * If the value is "NIL", returns an empty string.
+ */
+ private ImapString parseBareString() throws IOException, MessagingException {
+ mParseBareString.setLength(0);
+ for (;;) {
+ final int ch = peek();
+
+ // TODO Can we clean this up? (This condition is from the old parser.)
+ if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
+ // ']' is not part of atom (it's in resp-specials)
+ ch == ']' ||
+ // docs claim that flags are \ atom but atom isn't supposed to
+ // contain
+ // * and some flags contain *
+ // ch == '%' || ch == '*' ||
+ ch == '%' ||
+ // TODO probably should not allow \ and should recognize
+ // it as a flag instead
+ // ch == '"' || ch == '\' ||
+ ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
+ if (mParseBareString.length() == 0) {
+ throw new MessagingException("Expected string, none found.");
+ }
+ String s = mParseBareString.toString();
+
+ // NIL will be always converted into the empty string.
+ if (ImapConstants.NIL.equalsIgnoreCase(s)) {
+ return ImapString.EMPTY;
+ }
+ return new ImapSimpleString(s);
+ } else if (ch == '[') {
+ // Eat all until next ']'
+ mParseBareString.append((char) readByte());
+ mParseBareString.append(readUntil(']'));
+ mParseBareString.append(']'); // readUntil won't include the end char.
+ } else {
+ mParseBareString.append((char) readByte());
+ }
+ }
+ }
+
+ private void parseElements(ImapList list, char end)
+ throws IOException, MessagingException {
+ for (;;) {
+ for (;;) {
+ final int next = peek();
+ if (next == end) {
+ return;
+ }
+ if (next != ' ') {
+ break;
+ }
+ // Skip space
+ readByte();
+ }
+ final ImapElement el = parseElement();
+ if (el == null) { // EOL
+ return;
+ }
+ list.add(el);
+ }
+ }
+
+ private ImapList parseList(char opening, char closing)
+ throws IOException, MessagingException {
+ expect(opening);
+ final ImapList list = new ImapList();
+ parseElements(list, closing);
+ expect(closing);
+ return list;
+ }
+
+ private ImapString parseLiteral() throws IOException, MessagingException {
+ expect('{');
+ final int size;
+ try {
+ size = Integer.parseInt(readUntil('}'));
+ } catch (NumberFormatException nfe) {
+ throw new MessagingException("Invalid length in literal");
+ }
+ if (size < 0) {
+ throw new MessagingException("Invalid negative length in literal");
+ }
+ expect('\r');
+ expect('\n');
+ FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
+ if (size > mLiteralKeepInMemoryThreshold) {
+ return new ImapTempFileLiteral(in);
+ } else {
+ return new ImapMemoryLiteral(in);
+ }
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java
new file mode 100644
index 000000000..22d8141a0
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java
@@ -0,0 +1,62 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import com.android.voicemailomtp.VvmLog;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Subclass of {@link ImapString} used for non literals.
+ */
+public class ImapSimpleString extends ImapString {
+ private final String TAG = "ImapSimpleString";
+ private String mString;
+
+ /* package */ ImapSimpleString(String string) {
+ mString = (string != null) ? string : "";
+ }
+
+ @Override
+ public void destroy() {
+ mString = null;
+ super.destroy();
+ }
+
+ @Override
+ public String getString() {
+ return mString;
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ try {
+ return new ByteArrayInputStream(mString.getBytes("US-ASCII"));
+ } catch (UnsupportedEncodingException e) {
+ VvmLog.e(TAG, "Unsupported encoding: ", e);
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ // Purposefully not return just mString, in order to prevent using it instead of getString.
+ return "\"" + mString + "\"";
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapString.java
new file mode 100644
index 000000000..83efb6479
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapString.java
@@ -0,0 +1,192 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import com.android.voicemailomtp.VvmLog;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * Class represents an IMAP "element" that is not a list.
+ *
+ * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too.
+ * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]".
+ * See {@link ImapResponseParser}.
+ */
+public abstract class ImapString extends ImapElement {
+ private static final byte[] EMPTY_BYTES = new byte[0];
+
+ public static final ImapString EMPTY = new ImapString() {
+ @Override public void destroy() {
+ // Don't call super.destroy().
+ // It's a shared object. We don't want the mDestroyed to be set on this.
+ }
+
+ @Override public String getString() {
+ return "";
+ }
+
+ @Override public InputStream getAsStream() {
+ return new ByteArrayInputStream(EMPTY_BYTES);
+ }
+
+ @Override public String toString() {
+ return "";
+ }
+ };
+
+ // This is used only for parsing IMAP's FETCH ENVELOPE command, in which
+ // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be
+ // handled by Locale.US
+ private final static SimpleDateFormat DATE_TIME_FORMAT =
+ new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US);
+
+ private boolean mIsInteger;
+ private int mParsedInteger;
+ private Date mParsedDate;
+
+ @Override
+ public final boolean isList() {
+ return false;
+ }
+
+ @Override
+ public final boolean isString() {
+ return true;
+ }
+
+ /**
+ * @return true if and only if the length of the string is larger than 0.
+ *
+ * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser
+ * #parseBareString}.
+ * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is
+ * treated literally.
+ */
+ public final boolean isEmpty() {
+ return getString().length() == 0;
+ }
+
+ public abstract String getString();
+
+ public abstract InputStream getAsStream();
+
+ /**
+ * @return whether it can be parsed as a number.
+ */
+ public final boolean isNumber() {
+ if (mIsInteger) {
+ return true;
+ }
+ try {
+ mParsedInteger = Integer.parseInt(getString());
+ mIsInteger = true;
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+
+ /**
+ * @return value parsed as a number, or 0 if the string is not a number.
+ */
+ public final int getNumberOrZero() {
+ return getNumber(0);
+ }
+
+ /**
+ * @return value parsed as a number, or {@code defaultValue} if the string is not a number.
+ */
+ public final int getNumber(int defaultValue) {
+ if (!isNumber()) {
+ return defaultValue;
+ }
+ return mParsedInteger;
+ }
+
+ /**
+ * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}.
+ */
+ public final boolean isDate() {
+ if (mParsedDate != null) {
+ return true;
+ }
+ if (isEmpty()) {
+ return false;
+ }
+ try {
+ mParsedDate = DATE_TIME_FORMAT.parse(getString());
+ return true;
+ } catch (ParseException e) {
+ VvmLog.w("ImapString", getString() + " can't be parsed as a date.");
+ return false;
+ }
+ }
+
+ /**
+ * @return value it can be parsed as a {@link Date}, or null otherwise.
+ */
+ public final Date getDateOrNull() {
+ if (!isDate()) {
+ return null;
+ }
+ return mParsedDate;
+ }
+
+ /**
+ * @return whether the value case-insensitively equals to {@code s}.
+ */
+ public final boolean is(String s) {
+ if (s == null) {
+ return false;
+ }
+ return getString().equalsIgnoreCase(s);
+ }
+
+
+ /**
+ * @return whether the value case-insensitively starts with {@code s}.
+ */
+ public final boolean startsWith(String prefix) {
+ if (prefix == null) {
+ return false;
+ }
+ final String me = this.getString();
+ if (me.length() < prefix.length()) {
+ return false;
+ }
+ return me.substring(0, prefix.length()).equalsIgnoreCase(prefix);
+ }
+
+ // To force subclasses to implement it.
+ @Override
+ public abstract String toString();
+
+ @Override
+ public final boolean equalsForTest(ImapElement that) {
+ if (!super.equalsForTest(that)) {
+ return false;
+ }
+ ImapString thatString = (ImapString) that;
+ return getString().equals(thatString.getString());
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java
new file mode 100644
index 000000000..efe5c3848
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java
@@ -0,0 +1,123 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import com.android.voicemailomtp.mail.FixedLengthInputStream;
+import com.android.voicemailomtp.mail.TempDirectory;
+import com.android.voicemailomtp.mail.utils.Utility;
+import com.android.voicemailomtp.mail.utils.LogUtils;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Subclass of {@link ImapString} used for literals backed by a temp file.
+ */
+public class ImapTempFileLiteral extends ImapString {
+ private final String TAG = "ImapTempFileLiteral";
+
+ /* package for test */ final File mFile;
+
+ /** Size is purely for toString() */
+ private final int mSize;
+
+ /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException {
+ mSize = stream.getLength();
+ mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory());
+
+ // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random
+ // so it'd simply cause a memory leak.
+ // deleteOnExit() simply adds filenames to a static list and the list will never shrink.
+ // mFile.deleteOnExit();
+ OutputStream out = new FileOutputStream(mFile);
+ IOUtils.copy(stream, out);
+ out.close();
+ }
+
+ /**
+ * Make sure we delete the temp file.
+ *
+ * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public InputStream getAsStream() {
+ checkNotDestroyed();
+ try {
+ return new FileInputStream(mFile);
+ } catch (FileNotFoundException e) {
+ // It's probably possible if we're low on storage and the system clears the cache dir.
+ LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found");
+
+ // Return 0 byte stream as a dummy...
+ return new ByteArrayInputStream(new byte[0]);
+ }
+ }
+
+ @Override
+ public String getString() {
+ checkNotDestroyed();
+ try {
+ byte[] bytes = IOUtils.toByteArray(getAsStream());
+ // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly
+ if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) {
+ throw new IOException();
+ }
+ return Utility.fromAscii(bytes);
+ } catch (IOException e) {
+ LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e);
+ return "";
+ }
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ if (!isDestroyed() && mFile.exists()) {
+ mFile.delete();
+ }
+ } catch (RuntimeException re) {
+ // Just log and ignore.
+ LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage());
+ }
+ super.destroy();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{%d byte literal(file)}", mSize);
+ }
+
+ public boolean tempFileExistsForTest() {
+ return mFile.exists();
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java b/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java
new file mode 100644
index 000000000..b045eb32f
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java
@@ -0,0 +1,125 @@
+/*
+ * 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.voicemailomtp.mail.store.imap;
+
+import com.android.voicemailomtp.mail.utils.LogUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Utility methods for use with IMAP.
+ */
+public class ImapUtility {
+ public static final String TAG = "ImapUtility";
+ /**
+ * Apply quoting rules per IMAP RFC,
+ * quoted = DQUOTE *QUOTED-CHAR DQUOTE
+ * QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
+ * quoted-specials = DQUOTE / "\"
+ *
+ * This is used primarily for IMAP login, but might be useful elsewhere.
+ *
+ * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check
+ * for trouble chars before calling the replace functions.
+ *
+ * @param s The string to be quoted.
+ * @return A copy of the string, having undergone quoting as described above
+ */
+ public static String imapQuoted(String s) {
+
+ // First, quote any backslashes by replacing \ with \\
+ // regex Pattern: \\ (Java string const = \\\\)
+ // Substitute: \\\\ (Java string const = \\\\\\\\)
+ String result = s.replaceAll("\\\\", "\\\\\\\\");
+
+ // Then, quote any double-quotes by replacing " with \"
+ // regex Pattern: " (Java string const = \")
+ // Substitute: \\" (Java string const = \\\\\")
+ result = result.replaceAll("\"", "\\\\\"");
+
+ // return string with quotes around it
+ return "\"" + result + "\"";
+ }
+
+ /**
+ * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
+ * list of individual numbers. If the set is invalid, an empty array is returned.
+ * <pre>
+ * sequence-number = nz-number / "*"
+ * sequence-range = sequence-number ":" sequence-number
+ * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+ * </pre>
+ */
+ public static String[] getImapSequenceValues(String set) {
+ ArrayList<String> list = new ArrayList<String>();
+ if (set != null) {
+ String[] setItems = set.split(",");
+ for (String item : setItems) {
+ if (item.indexOf(':') == -1) {
+ // simple item
+ try {
+ Integer.parseInt(item); // Don't need the value; just ensure it's valid
+ list.add(item);
+ } catch (NumberFormatException e) {
+ LogUtils.d(TAG, "Invalid UID value", e);
+ }
+ } else {
+ // range
+ for (String rangeItem : getImapRangeValues(item)) {
+ list.add(rangeItem);
+ }
+ }
+ }
+ }
+ String[] stringList = new String[list.size()];
+ return list.toArray(stringList);
+ }
+
+ /**
+ * Expand the given number range into a list of individual numbers. If the range is not valid,
+ * an empty array is returned.
+ * <pre>
+ * sequence-number = nz-number / "*"
+ * sequence-range = sequence-number ":" sequence-number
+ * sequence-set = (sequence-number / sequence-range) *("," sequence-set)
+ * </pre>
+ */
+ public static String[] getImapRangeValues(String range) {
+ ArrayList<String> list = new ArrayList<String>();
+ try {
+ if (range != null) {
+ int colonPos = range.indexOf(':');
+ if (colonPos > 0) {
+ int first = Integer.parseInt(range.substring(0, colonPos));
+ int second = Integer.parseInt(range.substring(colonPos + 1));
+ if (first < second) {
+ for (int i = first; i <= second; i++) {
+ list.add(Integer.toString(i));
+ }
+ } else {
+ for (int i = first; i >= second; i--) {
+ list.add(Integer.toString(i));
+ }
+ }
+ }
+ }
+ } catch (NumberFormatException e) {
+ LogUtils.d(TAG, "Invalid range value", e);
+ }
+ String[] stringList = new String[list.size()];
+ return list.toArray(stringList);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java b/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java
new file mode 100644
index 000000000..fdf81d44a
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * 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.voicemailomtp.mail.utility;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A simple pass-thru OutputStream that also counts how many bytes are written to it and
+ * makes that count available to callers.
+ */
+public class CountingOutputStream extends OutputStream {
+ private long mCount;
+ private final OutputStream mOutputStream;
+
+ public CountingOutputStream(OutputStream outputStream) {
+ mOutputStream = outputStream;
+ }
+
+ public long getCount() {
+ return mCount;
+ }
+
+ @Override
+ public void write(byte[] buffer, int offset, int count) throws IOException {
+ mOutputStream.write(buffer, offset, count);
+ mCount += count;
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ mOutputStream.write(oneByte);
+ mCount++;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java b/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java
new file mode 100644
index 000000000..5b93a92ab
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java
@@ -0,0 +1,48 @@
+/*
+ * 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.voicemailomtp.mail.utility;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class EOLConvertingOutputStream extends FilterOutputStream {
+ int lastChar;
+
+ public EOLConvertingOutputStream(OutputStream out) {
+ super(out);
+ }
+
+ @Override
+ public void write(int oneByte) throws IOException {
+ if (oneByte == '\n') {
+ if (lastChar != '\r') {
+ super.write('\r');
+ }
+ }
+ super.write(oneByte);
+ lastChar = oneByte;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (lastChar == '\r') {
+ super.write('\n');
+ lastChar = '\n';
+ }
+ super.flush();
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/mail/utils/LogUtils.java b/java/com/android/voicemailomtp/mail/utils/LogUtils.java
new file mode 100644
index 000000000..a213a835e
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/utils/LogUtils.java
@@ -0,0 +1,413 @@
+/**
+ * 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.voicemailomtp.mail.utils;
+
+import android.net.Uri;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.voicemailomtp.VvmLog;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class LogUtils {
+ public static final String TAG = "Email Log";
+
+ // "GMT" + "+" or "-" + 4 digits
+ private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE =
+ Pattern.compile("GMT([-+]\\d{4})$");
+
+ private static final String ACCOUNT_PREFIX = "account:";
+
+ /**
+ * Priority constant for the println method; use LogUtils.v.
+ */
+ public static final int VERBOSE = Log.VERBOSE;
+
+ /**
+ * Priority constant for the println method; use LogUtils.d.
+ */
+ public static final int DEBUG = Log.DEBUG;
+
+ /**
+ * Priority constant for the println method; use LogUtils.i.
+ */
+ public static final int INFO = Log.INFO;
+
+ /**
+ * Priority constant for the println method; use LogUtils.w.
+ */
+ public static final int WARN = Log.WARN;
+
+ /**
+ * Priority constant for the println method; use LogUtils.e.
+ */
+ public static final int ERROR = Log.ERROR;
+
+ /**
+ * Used to enable/disable logging that we don't want included in production releases. This should
+ * be set to DEBUG for production releases, and VERBOSE for internal builds.
+ */
+ private static final int MAX_ENABLED_LOG_LEVEL = DEBUG;
+
+ private static Boolean sDebugLoggingEnabledForTests = null;
+
+ /**
+ * Enable debug logging for unit tests.
+ */
+ @VisibleForTesting
+ public static void setDebugLoggingEnabledForTests(boolean enabled) {
+ setDebugLoggingEnabledForTestsInternal(enabled);
+ }
+
+ protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) {
+ sDebugLoggingEnabledForTests = Boolean.valueOf(enabled);
+ }
+
+ /**
+ * Returns true if the build configuration prevents debug logging.
+ */
+ @VisibleForTesting
+ public static boolean buildPreventsDebugLogging() {
+ return MAX_ENABLED_LOG_LEVEL > VERBOSE;
+ }
+
+ /**
+ * Returns a boolean indicating whether debug logging is enabled.
+ */
+ protected static boolean isDebugLoggingEnabled(String tag) {
+ if (buildPreventsDebugLogging()) {
+ return false;
+ }
+ if (sDebugLoggingEnabledForTests != null) {
+ return sDebugLoggingEnabledForTests.booleanValue();
+ }
+ return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG);
+ }
+
+ /**
+ * Returns a String for the specified content provider uri. This will do
+ * sanitation of the uri to remove PII if debug logging is not enabled.
+ */
+ public static String contentUriToString(final Uri uri) {
+ return contentUriToString(TAG, uri);
+ }
+
+ /**
+ * Returns a String for the specified content provider uri. This will do
+ * sanitation of the uri to remove PII if debug logging is not enabled.
+ */
+ public static String contentUriToString(String tag, Uri uri) {
+ if (isDebugLoggingEnabled(tag)) {
+ // Debug logging has been enabled, so log the uri as is
+ return uri.toString();
+ } else {
+ // Debug logging is not enabled, we want to remove the email address from the uri.
+ List<String> pathSegments = uri.getPathSegments();
+
+ Uri.Builder builder = new Uri.Builder()
+ .scheme(uri.getScheme())
+ .authority(uri.getAuthority())
+ .query(uri.getQuery())
+ .fragment(uri.getFragment());
+
+ // This assumes that the first path segment is the account
+ final String account = pathSegments.get(0);
+
+ builder = builder.appendPath(sanitizeAccountName(account));
+ for (int i = 1; i < pathSegments.size(); i++) {
+ builder.appendPath(pathSegments.get(i));
+ }
+ return builder.toString();
+ }
+ }
+
+ /**
+ * Sanitizes an account name. If debug logging is not enabled, a sanitized name
+ * is returned.
+ */
+ public static String sanitizeAccountName(String accountName) {
+ if (TextUtils.isEmpty(accountName)) {
+ return "";
+ }
+
+ return ACCOUNT_PREFIX + sanitizeName(TAG, accountName);
+ }
+
+ public static String sanitizeName(final String tag, final String name) {
+ if (TextUtils.isEmpty(name)) {
+ return "";
+ }
+
+ if (isDebugLoggingEnabled(tag)) {
+ return name;
+ }
+
+ return String.valueOf(name.hashCode());
+ }
+
+ /**
+ * Checks to see whether or not a log for the specified tag is loggable at the specified level.
+ */
+ public static boolean isLoggable(String tag, int level) {
+ if (MAX_ENABLED_LOG_LEVEL > level) {
+ return false;
+ }
+ return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level);
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int v(String tag, String format, Object... args) {
+ if (isLoggable(tag, VERBOSE)) {
+ return VvmLog.v(tag, String.format(format, args));
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #VERBOSE} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int v(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, VERBOSE)) {
+ return VvmLog.v(tag, String.format(format, args), tr);
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int d(String tag, String format, Object... args) {
+ if (isLoggable(tag, DEBUG)) {
+ return VvmLog.d(tag, String.format(format, args));
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #DEBUG} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int d(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, DEBUG)) {
+ return VvmLog.d(tag, String.format(format, args), tr);
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #INFO} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int i(String tag, String format, Object... args) {
+ if (isLoggable(tag, INFO)) {
+ return VvmLog.i(tag, String.format(format, args));
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #INFO} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int i(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, INFO)) {
+ return VvmLog.i(tag, String.format(format, args), tr);
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int w(String tag, String format, Object... args) {
+ if (isLoggable(tag, WARN)) {
+ return VvmLog.w(tag, String.format(format, args));
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #WARN} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int w(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, WARN)) {
+ return VvmLog.w(tag, String.format(format, args), tr);
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #ERROR} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int e(String tag, String format, Object... args) {
+ if (isLoggable(tag, ERROR)) {
+ return VvmLog.e(tag, String.format(format, args));
+ }
+ return 0;
+ }
+
+ /**
+ * Send a {@link #ERROR} log message.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int e(String tag, Throwable tr, String format, Object... args) {
+ if (isLoggable(tag, ERROR)) {
+ return VvmLog.e(tag, String.format(format, args), tr);
+ }
+ return 0;
+ }
+
+ /**
+ * What a Terrible Failure: Report a condition that should never happen.
+ * The error will always be logged at level ASSERT with the call stack.
+ * Depending on system configuration, a report may be added to the
+ * {@link android.os.DropBoxManager} and/or the process may be terminated
+ * immediately with an error dialog.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int wtf(String tag, String format, Object... args) {
+ return VvmLog.wtf(tag, String.format(format, args), new Error());
+ }
+
+ /**
+ * What a Terrible Failure: Report a condition that should never happen.
+ * The error will always be logged at level ASSERT with the call stack.
+ * Depending on system configuration, a report may be added to the
+ * {@link android.os.DropBoxManager} and/or the process may be terminated
+ * immediately with an error dialog.
+ * @param tag Used to identify the source of a log message. It usually identifies
+ * the class or activity where the log call occurs.
+ * @param tr An exception to log
+ * @param format the format string (see {@link java.util.Formatter#format})
+ * @param args
+ * the list of arguments passed to the formatter. If there are
+ * more arguments than required by {@code format},
+ * additional arguments are ignored.
+ */
+ public static int wtf(String tag, Throwable tr, String format, Object... args) {
+ return VvmLog.wtf(tag, String.format(format, args), tr);
+ }
+
+
+ /**
+ * Try to make a date MIME(RFC 2822/5322)-compliant.
+ *
+ * It fixes:
+ * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700"
+ * (4 digit zone value can't be preceded by "GMT")
+ * We got a report saying eBay sends a date in this format
+ */
+ public static String cleanUpMimeDate(String date) {
+ if (TextUtils.isEmpty(date)) {
+ return date;
+ }
+ date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1");
+ return date;
+ }
+
+
+ public static String byteToHex(int b) {
+ return byteToHex(new StringBuilder(), b).toString();
+ }
+
+ public static StringBuilder byteToHex(StringBuilder sb, int b) {
+ b &= 0xFF;
+ sb.append("0123456789ABCDEF".charAt(b >> 4));
+ sb.append("0123456789ABCDEF".charAt(b & 0xF));
+ return sb;
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/mail/utils/Utility.java b/java/com/android/voicemailomtp/mail/utils/Utility.java
new file mode 100644
index 000000000..c7286fa64
--- /dev/null
+++ b/java/com/android/voicemailomtp/mail/utils/Utility.java
@@ -0,0 +1,80 @@
+/**
+ * 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.voicemailomtp.mail.utils;
+
+import java.io.ByteArrayInputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+
+/**
+ * Simple utility methods used in email functions.
+ */
+public class Utility {
+ public static final Charset ASCII = Charset.forName("US-ASCII");
+
+ public static final String[] EMPTY_STRINGS = new String[0];
+
+ /**
+ * Returns a concatenated string containing the output of every Object's
+ * toString() method, each separated by the given separator character.
+ */
+ public static String combine(Object[] parts, char separator) {
+ if (parts == null) {
+ return null;
+ }
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < parts.length; i++) {
+ sb.append(parts[i].toString());
+ if (i < parts.length - 1) {
+ sb.append(separator);
+ }
+ }
+ return sb.toString();
+ }
+
+ /** Converts a String to ASCII bytes */
+ public static byte[] toAscii(String s) {
+ return encode(ASCII, s);
+ }
+
+ /** Builds a String from ASCII bytes */
+ public static String fromAscii(byte[] b) {
+ return decode(ASCII, b);
+ }
+
+ private static byte[] encode(Charset charset, String s) {
+ if (s == null) {
+ return null;
+ }
+ final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s));
+ final byte[] bytes = new byte[buffer.limit()];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ private static String decode(Charset charset, byte[] b) {
+ if (b == null) {
+ return null;
+ }
+ final CharBuffer cb = charset.decode(ByteBuffer.wrap(b));
+ return new String(cb.array(), 0, cb.length());
+ }
+
+ public static ByteArrayInputStream streamFromAsciiString(String ascii) {
+ return new ByteArrayInputStream(toAscii(ascii));
+ }
+}
diff --git a/java/com/android/voicemailomtp/permissions.xml b/java/com/android/voicemailomtp/permissions.xml
new file mode 100644
index 000000000..9326d803a
--- /dev/null
+++ b/java/com/android/voicemailomtp/permissions.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.voicemailomtp">
+
+ <uses-sdk
+ android:minSdkVersion="23"
+ android:targetSdkVersion="25"/>
+
+ <!-- Applications using this module should merge these permissions using android_manifest_merge -->
+
+ <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" />
+ <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" />
+ <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" />
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
+ <uses-permission android:name="android.permission.SEND_SMS"/>
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+
+ <application/>
+</manifest>
diff --git a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java
new file mode 100644
index 000000000..48ed99709
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java
@@ -0,0 +1,59 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.sms.OmtpCvvmMessageSender;
+import com.android.voicemailomtp.sms.OmtpMessageSender;
+
+/**
+ * A flavor of OMTP protocol with a different mobile originated (MO) format
+ *
+ * Used by carriers such as T-Mobile
+ */
+public class CvvmProtocol extends VisualVoicemailProtocol {
+
+ private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+ private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+ private static String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+ @Override
+ public OmtpMessageSender createMessageSender(Context context,
+ PhoneAccountHandle phoneAccountHandle, short applicationPort,
+ String destinationNumber) {
+ return new OmtpCvvmMessageSender(context, phoneAccountHandle, applicationPort,
+ destinationNumber);
+ }
+
+ @Override
+ public String getCommand(String command) {
+ if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
+ return IMAP_CHANGE_TUI_PWD_FORMAT;
+ }
+ if (command == OmtpConstants.IMAP_CLOSE_NUT) {
+ return IMAP_CLOSE_NUT;
+ }
+ if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
+ return IMAP_CHANGE_VM_LANG_FORMAT;
+ }
+ return super.getCommand(command);
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java
new file mode 100644
index 000000000..d88a23285
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java
@@ -0,0 +1,37 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.sms.OmtpMessageSender;
+import com.android.voicemailomtp.sms.OmtpStandardMessageSender;
+
+public class OmtpProtocol extends VisualVoicemailProtocol {
+
+ @Override
+ public OmtpMessageSender createMessageSender(Context context,
+ PhoneAccountHandle phoneAccountHandle, short applicationPort,
+ String destinationNumber) {
+ return new OmtpStandardMessageSender(context, phoneAccountHandle, applicationPort,
+ destinationNumber,
+ null, OmtpConstants.PROTOCOL_VERSION1_1, null);
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java
new file mode 100644
index 000000000..4fca199bf
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java
@@ -0,0 +1,43 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.telephony.SmsManager;
+import android.text.TextUtils;
+
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.sms.OmtpMessageSender;
+
+public class ProtocolHelper {
+
+ private static final String TAG = "ProtocolHelper";
+
+ public static OmtpMessageSender getMessageSender(VisualVoicemailProtocol protocol,
+ OmtpVvmCarrierConfigHelper config) {
+
+ int applicationPort = config.getApplicationPort();
+ String destinationNumber = config.getDestinationNumber();
+ if (TextUtils.isEmpty(destinationNumber)) {
+ VvmLog.w(TAG, "No destination number for this carrier.");
+ return null;
+ }
+
+ return protocol.createMessageSender(config.getContext(), config.getPhoneAccountHandle(),
+ (short) applicationPort, destinationNumber);
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java
new file mode 100644
index 000000000..9ff2ed167
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java
@@ -0,0 +1,100 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import com.android.voicemailomtp.ActivationTask;
+import com.android.voicemailomtp.DefaultOmtpEventHandler;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.sms.OmtpMessageSender;
+import com.android.voicemailomtp.sms.StatusMessage;
+
+public abstract class VisualVoicemailProtocol {
+
+ /**
+ * Activation should cause the carrier to respond with a STATUS SMS.
+ */
+ public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) {
+ OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+ if (messageSender != null) {
+ messageSender.requestVvmActivation(sentIntent);
+ }
+ }
+
+ public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+ OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+ if (messageSender != null) {
+ messageSender.requestVvmDeactivation(null);
+ }
+ }
+
+ public boolean supportsProvisioning() {
+ return false;
+ }
+
+ public void startProvisioning(ActivationTask task, PhoneAccountHandle handle,
+ OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor editor, StatusMessage message,
+ Bundle data) {
+ // Do nothing
+ }
+
+ public void requestStatus(OmtpVvmCarrierConfigHelper config,
+ @Nullable PendingIntent sentIntent) {
+ OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config);
+ if (messageSender != null) {
+ messageSender.requestVvmStatus(sentIntent);
+ }
+ }
+
+ public abstract OmtpMessageSender createMessageSender(Context context,
+ PhoneAccountHandle phoneAccountHandle,
+ short applicationPort, String destinationNumber);
+
+ /**
+ * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI
+ * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD.
+ *
+ * @param command A String command in {@link com.android.voicemailomtp.OmtpConstants}, the exact
+ * instance should be used instead of its' value.
+ * @returns Translated command, or {@code null} if not available in this protocol
+ */
+ public String getCommand(String command) {
+ return command;
+ }
+
+ public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status, OmtpEvents event) {
+ DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+ }
+
+ /**
+ * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into
+ * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated.
+ */
+ @Nullable
+ public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event,
+ Bundle data) {
+ return null;
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java
new file mode 100644
index 000000000..b74f503c6
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.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.voicemailomtp.protocol;
+
+import android.content.res.Resources;
+import android.support.annotation.Nullable;
+import android.telephony.TelephonyManager;
+import com.android.voicemailomtp.VvmLog;
+
+public class VisualVoicemailProtocolFactory {
+
+ private static final String TAG = "VvmProtocolFactory";
+
+ private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3";
+
+ @Nullable
+ public static VisualVoicemailProtocol create(Resources resources, String type) {
+ if (type == null) {
+ return null;
+ }
+ switch (type) {
+ case TelephonyManager.VVM_TYPE_OMTP:
+ return new OmtpProtocol();
+ case TelephonyManager.VVM_TYPE_CVVM:
+ return new CvvmProtocol();
+ case VVM_TYPE_VVM3:
+ return new Vvm3Protocol();
+ default:
+ VvmLog.e(TAG, "Unexpected visual voicemail type: " + type);
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java
new file mode 100644
index 000000000..72646386c
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java
@@ -0,0 +1,271 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.content.Context;
+import android.support.annotation.IntDef;
+import android.util.Log;
+import com.android.voicemailomtp.DefaultOmtpEventHandler;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpEvents.Type;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.settings.VoicemailChangePinActivity;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom
+ * error codes into the voicemail status table so support on the dialer side is required.
+ *
+ * TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured.
+ */
+public class Vvm3EventHandler {
+
+ private static final String TAG = "Vvm3EventHandler";
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({VMS_DNS_FAILURE, VMG_DNS_FAILURE, SPG_DNS_FAILURE, VMS_NO_CELLULAR, VMG_NO_CELLULAR,
+ SPG_NO_CELLULAR, VMS_TIMEOUT, VMG_TIMEOUT, STATUS_SMS_TIMEOUT, SUBSCRIBER_BLOCKED,
+ UNKNOWN_USER, UNKNOWN_DEVICE, INVALID_PASSWORD, MAILBOX_NOT_INITIALIZED,
+ SERVICE_NOT_PROVISIONED, SERVICE_NOT_ACTIVATED, USER_BLOCKED, IMAP_GETQUOTA_ERROR,
+ IMAP_SELECT_ERROR, IMAP_ERROR, VMG_INTERNAL_ERROR, VMG_DB_ERROR,
+ VMG_COMMUNICATION_ERROR, SPG_URL_NOT_FOUND, VMG_UNKNOWN_ERROR, PIN_NOT_SET})
+ public @interface ErrorCode {
+
+ }
+
+ public static final int VMS_DNS_FAILURE = -9001;
+ public static final int VMG_DNS_FAILURE = -9002;
+ public static final int SPG_DNS_FAILURE = -9003;
+ public static final int VMS_NO_CELLULAR = -9004;
+ public static final int VMG_NO_CELLULAR = -9005;
+ public static final int SPG_NO_CELLULAR = -9006;
+ public static final int VMS_TIMEOUT = -9007;
+ public static final int VMG_TIMEOUT = -9008;
+ public static final int STATUS_SMS_TIMEOUT = -9009;
+
+ public static final int SUBSCRIBER_BLOCKED = -9990;
+ public static final int UNKNOWN_USER = -9991;
+ public static final int UNKNOWN_DEVICE = -9992;
+ public static final int INVALID_PASSWORD = -9993;
+ public static final int MAILBOX_NOT_INITIALIZED = -9994;
+ public static final int SERVICE_NOT_PROVISIONED = -9995;
+ public static final int SERVICE_NOT_ACTIVATED = -9996;
+ public static final int USER_BLOCKED = -9998;
+ public static final int IMAP_GETQUOTA_ERROR = -9997;
+ public static final int IMAP_SELECT_ERROR = -9989;
+ public static final int IMAP_ERROR = -9999;
+
+ public static final int VMG_INTERNAL_ERROR = -101;
+ public static final int VMG_DB_ERROR = -102;
+ public static final int VMG_COMMUNICATION_ERROR = -103;
+ public static final int SPG_URL_NOT_FOUND = -301;
+
+ // Non VVM3 codes:
+ public static final int VMG_UNKNOWN_ERROR = -1;
+ public static final int PIN_NOT_SET = -100;
+ // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer
+ // support.
+ public static final int SUBSCRIBER_UNKNOWN = -99;
+
+
+ public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status, OmtpEvents event) {
+ boolean handled = false;
+ switch (event.getType()) {
+ case Type.CONFIGURATION:
+ handled = handleConfigurationEvent(context, status, event);
+ break;
+ case Type.DATA_CHANNEL:
+ handled = handleDataChannelEvent(status, event);
+ break;
+ case Type.NOTIFICATION_CHANNEL:
+ handled = handleNotificationChannelEvent(status, event);
+ break;
+ case Type.OTHER:
+ handled = handleOtherEvent(status, event);
+ break;
+ default:
+ Log.wtf(TAG, "invalid event type " + event.getType() + " for " + event);
+ }
+ if (!handled) {
+ DefaultOmtpEventHandler.handleEvent(context, config, status, event);
+ }
+ }
+
+ private static boolean handleConfigurationEvent(Context context, VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event) {
+ case CONFIG_REQUEST_STATUS_SUCCESS:
+ if (status.getPhoneAccountHandle() == null) {
+ // This should never happen.
+ Log.e(TAG, "status editor has null phone account handle");
+ return true;
+ }
+
+ if (!VoicemailChangePinActivity
+ .isDefaultOldPinSet(context, status.getPhoneAccountHandle())) {
+ return false;
+ } else {
+ postError(status, PIN_NOT_SET);
+ }
+ break;
+ case CONFIG_DEFAULT_PIN_REPLACED:
+ postError(status, PIN_NOT_SET);
+ break;
+ case CONFIG_STATUS_SMS_TIME_OUT:
+ postError(status, STATUS_SMS_TIMEOUT);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) {
+ switch (event) {
+ case DATA_NO_CONNECTION:
+ case DATA_NO_CONNECTION_CELLULAR_REQUIRED:
+ case DATA_ALL_SOCKET_CONNECTION_FAILED:
+ postError(status, VMS_NO_CELLULAR);
+ break;
+ case DATA_SSL_INVALID_HOST_NAME:
+ case DATA_CANNOT_ESTABLISH_SSL_SESSION:
+ case DATA_IOE_ON_OPEN:
+ postError(status, VMS_TIMEOUT);
+ break;
+ case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK:
+ postError(status, VMS_DNS_FAILURE);
+ break;
+ case DATA_BAD_IMAP_CREDENTIAL:
+ postError(status, IMAP_ERROR);
+ break;
+ case DATA_AUTH_UNKNOWN_USER:
+ postError(status, UNKNOWN_USER);
+ break;
+ case DATA_AUTH_UNKNOWN_DEVICE:
+ postError(status, UNKNOWN_DEVICE);
+ break;
+ case DATA_AUTH_INVALID_PASSWORD:
+ postError(status, INVALID_PASSWORD);
+ break;
+ case DATA_AUTH_MAILBOX_NOT_INITIALIZED:
+ postError(status, MAILBOX_NOT_INITIALIZED);
+ break;
+ case DATA_AUTH_SERVICE_NOT_PROVISIONED:
+ postError(status, SERVICE_NOT_PROVISIONED);
+ break;
+ case DATA_AUTH_SERVICE_NOT_ACTIVATED:
+ postError(status, SERVICE_NOT_ACTIVATED);
+ break;
+ case DATA_AUTH_USER_IS_BLOCKED:
+ postError(status, USER_BLOCKED);
+ break;
+ case DATA_REJECTED_SERVER_RESPONSE:
+ case DATA_INVALID_INITIAL_SERVER_RESPONSE:
+ case DATA_SSL_EXCEPTION:
+ postError(status, IMAP_ERROR);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private static boolean handleNotificationChannelEvent(VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ return false;
+ }
+
+ private static boolean handleOtherEvent(VoicemailStatus.Editor status,
+ OmtpEvents event) {
+ switch (event) {
+ case VVM3_NEW_USER_SETUP_FAILED:
+ postError(status, MAILBOX_NOT_INITIALIZED);
+ break;
+ case VVM3_VMG_DNS_FAILURE:
+ postError(status, VMG_DNS_FAILURE);
+ break;
+ case VVM3_SPG_DNS_FAILURE:
+ postError(status, SPG_DNS_FAILURE);
+ break;
+ case VVM3_VMG_CONNECTION_FAILED:
+ postError(status, VMG_NO_CELLULAR);
+ break;
+ case VVM3_SPG_CONNECTION_FAILED:
+ postError(status, SPG_NO_CELLULAR);
+ break;
+ case VVM3_VMG_TIMEOUT:
+ postError(status, VMG_TIMEOUT);
+ break;
+ case VVM3_SUBSCRIBER_PROVISIONED:
+ postError(status, SERVICE_NOT_ACTIVATED);
+ break;
+ case VVM3_SUBSCRIBER_BLOCKED:
+ postError(status, SUBSCRIBER_BLOCKED);
+ break;
+ case VVM3_SUBSCRIBER_UNKNOWN:
+ postError(status, SUBSCRIBER_UNKNOWN);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) {
+ switch (errorCode) {
+ case VMG_DNS_FAILURE:
+ case SPG_DNS_FAILURE:
+ case VMG_NO_CELLULAR:
+ case SPG_NO_CELLULAR:
+ case VMG_TIMEOUT:
+ case SUBSCRIBER_BLOCKED:
+ case UNKNOWN_USER:
+ case UNKNOWN_DEVICE:
+ case INVALID_PASSWORD:
+ case MAILBOX_NOT_INITIALIZED:
+ case SERVICE_NOT_PROVISIONED:
+ case SERVICE_NOT_ACTIVATED:
+ case USER_BLOCKED:
+ case VMG_UNKNOWN_ERROR:
+ case SPG_URL_NOT_FOUND:
+ case VMG_INTERNAL_ERROR:
+ case VMG_DB_ERROR:
+ case VMG_COMMUNICATION_ERROR:
+ case PIN_NOT_SET:
+ case SUBSCRIBER_UNKNOWN:
+ editor.setConfigurationState(errorCode);
+ break;
+ case VMS_NO_CELLULAR:
+ case VMS_DNS_FAILURE:
+ case VMS_TIMEOUT:
+ case IMAP_GETQUOTA_ERROR:
+ case IMAP_SELECT_ERROR:
+ case IMAP_ERROR:
+ editor.setDataChannelState(errorCode);
+ break;
+ case STATUS_SMS_TIMEOUT:
+ editor.setNotificationChannelState(errorCode);
+ break;
+ default:
+ Log.wtf(TAG, "unknown error code: " + errorCode);
+ }
+ editor.apply();
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java
new file mode 100644
index 000000000..652d1010a
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java
@@ -0,0 +1,301 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.annotation.TargetApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemailomtp.ActivationTask;
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VisualVoicemailPreferences;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.imap.ImapHelper;
+import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemailomtp.settings.VoicemailChangePinActivity;
+import com.android.voicemailomtp.sms.OmtpMessageSender;
+import com.android.voicemailomtp.sms.StatusMessage;
+import com.android.voicemailomtp.sms.Vvm3MessageSender;
+import com.android.voicemailomtp.sync.VvmNetworkRequest;
+import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.util.Locale;
+
+/**
+ * A flavor of OMTP protocol with a different provisioning process
+ *
+ * <p>Used by carriers such as Verizon Wireless
+ */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class Vvm3Protocol extends VisualVoicemailProtocol {
+
+ private static final String TAG = "Vvm3Protocol";
+
+ private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED";
+ private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd";
+ private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS";
+ private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url";
+
+ private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s";
+ private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s";
+ private static final String IMAP_CLOSE_NUT = "CLOSE_NUT";
+
+ private static final String ISO639_Spanish = "es";
+
+ /**
+ * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link
+ * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value,
+ * the user can self-provision visual voicemail service. For other response codes, the user must
+ * contact customer support to resolve the issue.
+ */
+ private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2";
+
+ // Default prompt level when using the telephone user interface.
+ // Standard prompt when the user call into the voicemail, and no prompts when someone else is
+ // leaving a voicemail.
+ private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5";
+ private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6";
+
+ private static final int DEFAULT_PIN_LENGTH = 6;
+
+ @Override
+ public void startActivation(OmtpVvmCarrierConfigHelper config,
+ @Nullable PendingIntent sentIntent) {
+ // VVM3 does not support activation SMS.
+ // Send a status request which will start the provisioning process if the user is not
+ // provisioned.
+ VvmLog.i(TAG, "Activating");
+ config.requestStatus(sentIntent);
+ }
+
+ @Override
+ public void startDeactivation(OmtpVvmCarrierConfigHelper config) {
+ // VVM3 does not support deactivation.
+ // do nothing.
+ }
+
+ @Override
+ public boolean supportsProvisioning() {
+ return true;
+ }
+
+ @Override
+ public void startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle,
+ OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message,
+ Bundle data) {
+ VvmLog.i(TAG, "start vvm3 provisioning");
+ if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "Provisioning status: Unknown");
+ if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE
+ .equals(message.getReturnCode())) {
+ VvmLog.i(TAG, "Self provisioning available, subscribing");
+ new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe();
+ } else {
+ config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN);
+ }
+ } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "setting up new user");
+ // Save the IMAP credentials in preferences so they are persistent and can be retrieved.
+ VisualVoicemailPreferences prefs =
+ new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle);
+ message.putStatus(prefs.edit()).apply();
+
+ startProvisionNewUser(task, phoneAccountHandle, config, status, message);
+ } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "User provisioned but not activated, disabling VVM");
+ VisualVoicemailSettingsUtil
+ .setEnabled(config.getContext(), phoneAccountHandle, false);
+ } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) {
+ VvmLog.i(TAG, "User blocked");
+ config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED);
+ }
+ }
+
+ @Override
+ public OmtpMessageSender createMessageSender(Context context,
+ PhoneAccountHandle phoneAccountHandle, short applicationPort,
+ String destinationNumber) {
+ return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort,
+ destinationNumber);
+ }
+
+ @Override
+ public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config,
+ VoicemailStatus.Editor status, OmtpEvents event) {
+ Vvm3EventHandler.handleEvent(context, config, status, event);
+ }
+
+ @Override
+ public String getCommand(String command) {
+ if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) {
+ return IMAP_CHANGE_TUI_PWD_FORMAT;
+ }
+ if (command == OmtpConstants.IMAP_CLOSE_NUT) {
+ return IMAP_CLOSE_NUT;
+ }
+ if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) {
+ return IMAP_CHANGE_VM_LANG_FORMAT;
+ }
+ return super.getCommand(command);
+ }
+
+ @Override
+ public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event,
+ Bundle data) {
+ // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned
+ // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status
+ // so provisioning can be done.
+ if (!SMS_EVENT_UNRECOGNIZED.equals(event)) {
+ return null;
+ }
+ if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) {
+ return null;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN);
+ bundle.putString(OmtpConstants.RETURN_CODE,
+ VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE);
+ String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY);
+ if (TextUtils.isEmpty(vmgUrl)) {
+ VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config");
+ return null;
+ }
+ bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl);
+ VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS");
+ return bundle;
+ }
+
+ private void startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle,
+ OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status,
+ StatusMessage message) {
+ try (NetworkWrapper wrapper = VvmNetworkRequest
+ .getNetwork(config, phoneAccountHandle, status)) {
+ Network network = wrapper.get();
+
+ VvmLog.i(TAG, "new user: network available");
+ try (ImapHelper helper = new ImapHelper(config.getContext(), phoneAccountHandle,
+ network, status)) {
+ // VVM3 has inconsistent error language code to OMTP. Just issue a raw command
+ // here.
+ // TODO(b/29082671): use LocaleList
+ if (Locale.getDefault().getLanguage()
+ .equals(new Locale(ISO639_Spanish).getLanguage())) {
+ // Spanish
+ helper.changeVoicemailTuiLanguage(
+ VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS);
+ } else {
+ // English
+ helper.changeVoicemailTuiLanguage(
+ VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS);
+ }
+ VvmLog.i(TAG, "new user: language set");
+
+ if (setPin(config.getContext(), phoneAccountHandle, helper, message)) {
+ // Only close new user tutorial if the PIN has been changed.
+ helper.closeNewUserTutorial();
+ VvmLog.i(TAG, "new user: NUT closed");
+
+ config.requestStatus(null);
+ }
+ } catch (InitializingException | MessagingException | IOException e) {
+ config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED);
+ task.fail();
+ VvmLog.e(TAG, e.toString());
+ }
+ } catch (RequestFailedException e) {
+ config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+ task.fail();
+ }
+
+ }
+
+
+ private static boolean setPin(Context context, PhoneAccountHandle phoneAccountHandle,
+ ImapHelper helper, StatusMessage message)
+ throws IOException, MessagingException {
+ String defaultPin = getDefaultPin(message);
+ if (defaultPin == null) {
+ VvmLog.i(TAG, "cannot generate default PIN");
+ return false;
+ }
+
+ if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) {
+ // The pin was already set
+ VvmLog.i(TAG, "PIN already set");
+ return true;
+ }
+ String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle));
+ if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin);
+ helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED);
+ }
+ VvmLog.i(TAG, "new user: PIN set");
+ return true;
+ }
+
+ @Nullable
+ private static String getDefaultPin(StatusMessage message) {
+ // The IMAP username is [phone number]@example.com
+ String username = message.getImapUserName();
+ try {
+ String number = username.substring(0, username.indexOf('@'));
+ if (number.length() < 4) {
+ VvmLog.e(TAG, "unable to extract number from IMAP username");
+ return null;
+ }
+ return "1" + number.substring(number.length() - 4);
+ } catch (StringIndexOutOfBoundsException e) {
+ VvmLog.e(TAG, "unable to extract number from IMAP username");
+ return null;
+ }
+
+ }
+
+ private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) {
+ VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context,
+ phoneAccountHandle);
+ // The OMTP pin length format is {min}-{max}
+ String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+ if (lengths.length == 2) {
+ try {
+ return Integer.parseInt(lengths[0]);
+ } catch (NumberFormatException e) {
+ return DEFAULT_PIN_LENGTH;
+ }
+ }
+ return DEFAULT_PIN_LENGTH;
+ }
+
+ private static String generatePin(int length) {
+ SecureRandom random = new SecureRandom();
+ return String.format(Locale.US, "%010d", Math.abs(random.nextLong()))
+ .substring(0, length);
+
+ }
+}
diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java
new file mode 100644
index 000000000..0a4d792b2
--- /dev/null
+++ b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java
@@ -0,0 +1,326 @@
+/*
+ * 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.voicemailomtp.protocol;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
+import android.text.Html;
+import android.text.Spanned;
+import android.text.style.URLSpan;
+import android.util.ArrayMap;
+import com.android.voicemailomtp.ActivationTask;
+import com.android.voicemailomtp.Assert;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.sync.VvmNetworkRequest;
+import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Request;
+import com.android.volley.RequestQueue;
+import com.android.volley.toolbox.HurlStack;
+import com.android.volley.toolbox.RequestFuture;
+import com.android.volley.toolbox.StringRequest;
+import com.android.volley.toolbox.Volley;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required
+ * when the user is unprovisioned. This could happen when the user is on a legacy service, or
+ * switched over from devices that used other type of visual voicemail.
+ *
+ * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find
+ * the self provisioning gateway URL that we can modify voicemail services.
+ *
+ * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us
+ * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the
+ * subscription. This link should be clicked through cellular network, and have cookies enabled.
+ *
+ * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or
+ * ready user.
+ */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class Vvm3Subscriber {
+
+ private static final String TAG = "Vvm3Subscriber";
+
+ private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL";
+ private static final String SPG_URL_TAG = "spgurl";
+ private static final String TRANSACTION_ID_TAG = "transactionid";
+ //language=XML
+ private static final String VMG_XML_REQUEST_FORMAT = ""
+ + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
+ + "<VMGVVMRequest>"
+ + " <MessageHeader>"
+ + " <transactionid>%1$s</transactionid>"
+ + " </MessageHeader>"
+ + " <MessageBody>"
+ + " <mdn>%2$s</mdn>"
+ + " <operation>%3$s</operation>"
+ + " <source>Device</source>"
+ + " <devicemodel>%4$s</devicemodel>"
+ + " </MessageBody>"
+ + "</VMGVVMRequest>";
+
+ static final String VMG_URL_KEY = "vmg_url";
+
+ // Self provisioning POST key/values. VVM3 API 2.1.0 12.3
+ private static final String SPG_VZW_MDN_PARAM = "VZW_MDN";
+ private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE";
+ private static final String SPG_VZW_SERVICE_BASIC = "BVVM";
+ private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL";
+ // Value for all android device
+ private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G";
+ private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN";
+ private static final String SPG_APP_TOKEN = "q8e3t5u2o1";
+ private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM";
+ private static final String SPG_LANGUAGE_EN = "ENGLISH";
+
+ private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail";
+
+ private static final int REQUEST_TIMEOUT_SECONDS = 30;
+
+ private final ActivationTask mTask;
+ private final PhoneAccountHandle mHandle;
+ private final OmtpVvmCarrierConfigHelper mHelper;
+ private final VoicemailStatus.Editor mStatus;
+ private final Bundle mData;
+
+ private final String mNumber;
+
+ private RequestQueue mRequestQueue;
+
+ private static class ProvisioningException extends Exception {
+
+ public ProvisioningException(String message) {
+ super(message);
+ }
+ }
+
+ static {
+ // Set the default cookie handler to retain session data for the self provisioning gateway.
+ // Note; this is not ideal as it is application-wide, and can easily get clobbered.
+ // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually
+ // managing cookies will greatly increase complexity.
+ CookieManager cookieManager = new CookieManager();
+ CookieHandler.setDefault(cookieManager);
+ }
+
+ @WorkerThread
+ public Vvm3Subscriber(ActivationTask task, PhoneAccountHandle handle,
+ OmtpVvmCarrierConfigHelper helper, VoicemailStatus.Editor status, Bundle data) {
+ Assert.isNotMainThread();
+ mTask = task;
+ mHandle = handle;
+ mHelper = helper;
+ mStatus = status;
+ mData = data;
+
+ // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username
+ // is not included in the status SMS, thus no other way to get the current phone number.
+ mNumber = mHelper.getContext().getSystemService(TelephonyManager.class)
+ .createForPhoneAccountHandle(mHandle).getLine1Number();
+ }
+
+ @WorkerThread
+ public void subscribe() {
+ Assert.isNotMainThread();
+ // Cellular data is required to subscribe.
+ // processSubscription() is called after network is available.
+ VvmLog.i(TAG, "Subscribing");
+
+ try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) {
+ Network network = wrapper.get();
+ VvmLog.d(TAG, "provisioning: network available");
+ mRequestQueue = Volley
+ .newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network));
+ processSubscription();
+ } catch (RequestFailedException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+ mTask.fail();
+ }
+ }
+
+ private void processSubscription() {
+ try {
+ String gatewayUrl = getSelfProvisioningGateway();
+ String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl);
+ String subscribeLink = findSubscribeLink(selfProvisionResponse);
+ clickSubscribeLink(subscribeLink);
+ } catch (ProvisioningException e) {
+ VvmLog.e(TAG, e.toString());
+ mTask.fail();
+ }
+ }
+
+ /**
+ * Get the URL to perform self-provisioning from the voicemail management gateway.
+ */
+ private String getSelfProvisioningGateway() throws ProvisioningException {
+ VvmLog.i(TAG, "retrieving SPG URL");
+ String response = vvm3XmlRequest(OPERATION_GET_SPG_URL);
+ return extractText(response, SPG_URL_TAG);
+ }
+
+ /**
+ * Sent a request to the self-provisioning gateway, which will return us with a webpage. The
+ * page might contain a "Subscribe to Basic Visual Voice Mail" link to complete the
+ * subscription. The cookie from this response and cellular data is required to click the link.
+ */
+ private String getSelfProvisionResponse(String url) throws ProvisioningException {
+ VvmLog.i(TAG, "Retrieving self provisioning response");
+
+ RequestFuture<String> future = RequestFuture.newFuture();
+
+ StringRequest stringRequest = new StringRequest(Request.Method.POST, url, future, future) {
+ @Override
+ protected Map<String, String> getParams() {
+ Map<String, String> params = new ArrayMap<>();
+ params.put(SPG_VZW_MDN_PARAM, mNumber);
+ params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC);
+ params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID);
+ params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN);
+ // Language to display the subscription page. The page is never shown to the user
+ // so just use English.
+ params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN);
+ return params;
+ }
+ };
+
+ mRequestQueue.add(stringRequest);
+ try {
+ return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+ throw new ProvisioningException(e.toString());
+ }
+ }
+
+ private void clickSubscribeLink(String subscribeLink) throws ProvisioningException {
+ VvmLog.i(TAG, "Clicking subscribe link");
+ RequestFuture<String> future = RequestFuture.newFuture();
+
+ StringRequest stringRequest = new StringRequest(Request.Method.POST,
+ subscribeLink, future, future);
+ mRequestQueue.add(stringRequest);
+ try {
+ // A new STATUS SMS will be sent after this request.
+ future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ } catch (TimeoutException | ExecutionException | InterruptedException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED);
+ throw new ProvisioningException(e.toString());
+ }
+ // It could take very long for the STATUS SMS to return. Waiting for it is unreliable.
+ // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always
+ // manually retry if it took too long.
+ }
+
+ private String vvm3XmlRequest(String operation) throws ProvisioningException {
+ VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation);
+ String voicemailManagementGateway = mData.getString(VMG_URL_KEY);
+ if (voicemailManagementGateway == null) {
+ VvmLog.e(TAG, "voicemailManagementGateway url unknown");
+ return null;
+ }
+ String transactionId = createTransactionId();
+ String body = String.format(Locale.US, VMG_XML_REQUEST_FORMAT,
+ transactionId, mNumber, operation, Build.MODEL);
+
+ RequestFuture<String> future = RequestFuture.newFuture();
+ StringRequest stringRequest = new StringRequest(Request.Method.POST,
+ voicemailManagementGateway, future, future) {
+ @Override
+ public byte[] getBody() throws AuthFailureError {
+ return body.getBytes();
+ }
+ };
+ mRequestQueue.add(stringRequest);
+
+ try {
+ String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+ if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) {
+ throw new ProvisioningException("transactionId mismatch");
+ }
+ return response;
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED);
+ throw new ProvisioningException(e.toString());
+ }
+ }
+
+ private String findSubscribeLink(String response) throws ProvisioningException {
+ Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY);
+ URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class);
+ StringBuilder fulltext = new StringBuilder();
+ for (URLSpan span : spans) {
+ String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString();
+ if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) {
+ return span.getURL();
+ }
+ fulltext.append(text);
+ }
+ throw new ProvisioningException("Subscribe link not found: " + fulltext);
+ }
+
+ private String createTransactionId() {
+ return String.valueOf(Math.abs(new Random().nextLong()));
+ }
+
+ private String extractText(String xml, String tag) throws ProvisioningException {
+ Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">");
+ Matcher matcher = pattern.matcher(xml);
+ if (matcher.find()) {
+ return matcher.group(1);
+ }
+ throw new ProvisioningException("Tag " + tag + " not found in xml response");
+ }
+
+ private static class NetworkSpecifiedHurlStack extends HurlStack {
+
+ private final Network mNetwork;
+
+ public NetworkSpecifiedHurlStack(Network network) {
+ mNetwork = network;
+ }
+
+ @Override
+ protected HttpURLConnection createConnection(URL url) throws IOException {
+ return (HttpURLConnection) mNetwork.openConnection(url);
+ }
+
+ }
+}
diff --git a/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml b/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml
new file mode 100644
index 000000000..b0db64b12
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2014, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_horizontal"
+ android:orientation="vertical">
+ <!-- header text ('Enter Pin') -->
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ android:paddingTop="48dp"
+ android:paddingStart="48dp"
+ android:paddingEnd="48dp">
+ <TextView
+ android:id="@+id/headerText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:lines="2"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"
+ android:accessibilityLiveRegion="polite"/>
+
+ <!-- hint text ('PIN too short') -->
+ <TextView
+ android:id="@+id/hintText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:lines="2" />
+
+ <!-- error text ('PIN too short') -->
+ <TextView
+ android:id="@+id/errorText"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:lines="2"
+ android:textColor="@android:color/holo_red_dark"/>
+
+ <!-- Password entry field -->
+ <EditText
+ android:id="@+id/pin_entry"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:imeOptions="actionNext|flagNoExtractUi"
+ android:inputType="numberPassword"
+ android:textSize="24sp"/>
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="end"
+ android:orientation="horizontal">
+
+ <!-- left : cancel -->
+ <Button
+ android:id="@+id/cancel_button"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/change_pin_cancel_label"/>
+
+ <!-- right : continue -->
+ <Button
+ android:id="@+id/next_button"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/change_pin_continue_label"/>
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/java/com/android/voicemailomtp/res/values/arrays.xml b/java/com/android/voicemailomtp/res/values/arrays.xml
new file mode 100644
index 000000000..95714cf4d
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/arrays.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/attrs.xml b/java/com/android/voicemailomtp/res/values/attrs.xml
new file mode 100644
index 000000000..d1c7329d5
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/attrs.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<resources>
+
+ <attr name="preferenceBackgroundColor" format="color" />
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/colors.xml b/java/com/android/voicemailomtp/res/values/colors.xml
new file mode 100644
index 000000000..8a897ab94
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/colors.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/config.xml b/java/com/android/voicemailomtp/res/values/config.xml
new file mode 100644
index 000000000..2f5603083
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/config.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/dimens.xml b/java/com/android/voicemailomtp/res/values/dimens.xml
new file mode 100644
index 000000000..e66ca0921
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/dimens.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/ids.xml b/java/com/android/voicemailomtp/res/values/ids.xml
new file mode 100644
index 000000000..84c685a14
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/ids.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2015 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<resources>
+
+</resources> \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/res/values/strings.xml b/java/com/android/voicemailomtp/res/values/strings.xml
new file mode 100644
index 000000000..7a1407371
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/strings.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+ <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. -->
+ <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string>
+
+ <!-- Call settings screen, setting option name -->
+ <string translatable="false" name="voicemail_settings_label">Voicemail</string>
+
+ <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. -->
+ <string translatable="false" name="voicemail_visual_voicemail_key">
+ voicemail_visual_voicemail_key
+ </string>
+ <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. -->
+ <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string>
+
+ <!-- Visual voicemail on/off title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string>
+
+ <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string>
+ <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] -->
+ <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string>
+
+ <!-- Hint for the old PIN field in the change vociemail PIN dialog -->
+ <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string>
+ <!-- Hint for the new PIN field in the change vociemail PIN dialog -->
+ <string translatable="false" name="vm_change_pin_new_pin">New PIN</string>
+
+ <!-- Message on the dialog when PIN changing is in progress -->
+ <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string>
+ <!-- Error message for the voicemail PIN change if the PIN is too short -->
+ <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string>
+ <!-- Error message for the voicemail PIN change if the PIN is too long -->
+ <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string>
+ <!-- Error message for the voicemail PIN change if the PIN is too weak -->
+ <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string>
+ <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match -->
+ <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string>
+ <!-- Error message for the voicemail PIN change if the new PIN contains invalid character -->
+ <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string>
+ <!-- Error message for the voicemail PIN change if operation has failed -->
+ <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string>
+ <!-- Message to replace the transcription if a visual voicemail message is not supported-->
+ <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string>
+
+ <!-- The title for the change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_title">Change Voicemail PIN</string>
+ <!-- The label for the continue button in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_continue_label">Continue</string>
+ <!-- The label for the cancel button in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_cancel_label">Cancel</string>
+ <!-- The label for the ok button in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_ok_label">Ok</string>
+ <!-- The title for the enter old pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string>
+ <!-- The hint for the enter old pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string>
+ <!-- The title for the enter new pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string>
+ <!-- The hint for the enter new pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string>
+ <!-- The title for the confirm new pin step in change voicemail PIN activity -->
+ <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string>
+ <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered -->
+ <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string>
+ <!-- The toast to show after the voicemail PIN has been successfully changed -->
+ <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string>
+ <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN -->
+ <string translatable="false" name="change_pin_system_error">Unable to set PIN</string>
+</resources>
diff --git a/java/com/android/voicemailomtp/res/values/styles.xml b/java/com/android/voicemailomtp/res/values/styles.xml
new file mode 100644
index 000000000..8a897ab94
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/values/styles.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2007 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+
+</resources>
diff --git a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml b/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml
new file mode 100644
index 000000000..03bc34efc
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2014 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+ android:title="@string/voicemail_settings_label">
+
+ <SwitchPreference
+ android:key="@string/voicemail_visual_voicemail_key"
+ android:title="@string/voicemail_visual_voicemail_switch_title"/>"
+
+ <Preference
+ android:key="@string/voicemail_change_pin_key"
+ android:title="@string/voicemail_change_pin_dialog_title"/>
+</PreferenceScreen>
diff --git a/java/com/android/voicemailomtp/res/xml/vvm_config.xml b/java/com/android/voicemailomtp/res/xml/vvm_config.xml
new file mode 100644
index 000000000..19c667e13
--- /dev/null
+++ b/java/com/android/voicemailomtp/res/xml/vvm_config.xml
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<list name="carrier_config_list">
+ <pbundle_as_map>
+ <!-- Test -->
+ <string-array name="mccmnc">
+ <item value="TEST"/>
+ </string-array>
+ </pbundle_as_map>
+
+ <pbundle_as_map>
+ <!-- Orange France -->
+ <string-array name="mccmnc">
+ <item value="20801"/>
+ <item value="20802"/>
+ </string-array>
+
+ <int name="vvm_port_number_int" value="20481"/>
+ <string name="vvm_destination_number_string">21101</string>
+ <string-array name="carrier_vvm_package_name_string_array">
+ <item value="com.orange.vvm"/>
+ </string-array>
+ <string name="vvm_type_string">vvm_type_omtp</string>
+ <boolean name="vvm_cellular_data_required_bool" value="true"/>
+ <string-array name="vvm_disabled_capabilities_string_array">
+ <!-- b/32365569 -->
+ <item value="STARTTLS"/>
+ </string-array>
+ </pbundle_as_map>
+
+ <pbundle_as_map>
+ <!-- T-Mobile USA-->
+ <string-array name="mccmnc">
+ <item value="310160"/>
+ <item value="310200"/>
+ <item value="310210"/>
+ <item value="310220"/>
+ <item value="310230"/>
+ <item value="310240"/>
+ <item value="310250"/>
+ <item value="310260"/>
+ <item value="310270"/>
+ <item value="310300"/>
+ <item value="310310"/>
+ <item value="310490"/>
+ <item value="310530"/>
+ <item value="310590"/>
+ <item value="310640"/>
+ <item value="310660"/>
+ <item value="310800"/>
+ </string-array>
+
+ <int name="vvm_port_number_int" value="1808"/>
+ <int name="vvm_ssl_port_number_int" value="993"/>
+ <string name="vvm_destination_number_string">122</string>
+ <string-array name="carrier_vvm_package_name_string_array">
+ <item value="com.tmobile.vvm.application"/>
+ </string-array>
+ <string name="vvm_type_string">vvm_type_cvvm</string>>
+ <string-array name="vvm_disabled_capabilities_string_array">
+ <!-- b/28717550 -->
+ <item value="AUTH=DIGEST-MD5"/>
+ </string-array>
+ </pbundle_as_map>
+
+ <pbundle_as_map>
+ <!-- Verizon USA -->
+ <string-array name="mccmnc">
+ <item value="310004"/>
+ <item value="310010"/>
+ <item value="310012"/>
+ <item value="310013"/>
+ <item value="310590"/>
+ <item value="310890"/>
+ <item value="310910"/>
+ <item value="311110"/>
+ <item value="311270"/>
+ <item value="311271"/>
+ <item value="311272"/>
+ <item value="311273"/>
+ <item value="311274"/>
+ <item value="311275"/>
+ <item value="311276"/>
+ <item value="311277"/>
+ <item value="311278"/>
+ <item value="311279"/>
+ <item value="311280"/>
+ <item value="311281"/>
+ <item value="311282"/>
+ <item value="311283"/>
+ <item value="311284"/>
+ <item value="311285"/>
+ <item value="311286"/>
+ <item value="311287"/>
+ <item value="311288"/>
+ <item value="311289"/>
+ <item value="311390"/>
+ <item value="311480"/>
+ <item value="311481"/>
+ <item value="311482"/>
+ <item value="311483"/>
+ <item value="311484"/>
+ <item value="311485"/>
+ <item value="311486"/>
+ <item value="311487"/>
+ <item value="311488"/>
+ <item value="311489"/>
+ </string-array>
+
+ <int name="vvm_port_number_int" value="0"/>
+ <string name="vvm_destination_number_string">900080006200</string>
+ <string name="vvm_type_string">vvm_type_vvm3</string>
+ <string name="vvm_client_prefix_string">//VZWVVM</string>
+ <boolean name="vvm_cellular_data_required_bool" value="true"/>
+ <boolean name="vvm_legacy_mode_enabled_bool" value="true"/>
+ <!-- VVM3 specific value for the voicemail management gateway to use if the SMS didn't provide
+ one -->
+ <string name="default_vmg_url">https://mobile.vzw.com/VMGIMS/VMServices</string>
+ </pbundle_as_map>
+</list>
diff --git a/java/com/android/voicemailomtp/scheduling/BaseTask.java b/java/com/android/voicemailomtp/scheduling/BaseTask.java
new file mode 100644
index 000000000..8097bb4dc
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/BaseTask.java
@@ -0,0 +1,206 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.annotation.CallSuper;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SubscriptionManager;
+import com.android.voicemailomtp.Assert;
+import com.android.voicemailomtp.NeededForTesting;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides common utilities for task implementations, such as execution time and managing {@link
+ * Policy}
+ */
+public abstract class BaseTask implements Task {
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+ private Context mContext;
+
+ private int mId;
+ private PhoneAccountHandle mPhoneAccountHandle;
+
+ private boolean mHasStarted;
+ private volatile boolean mHasFailed;
+
+ @NonNull
+ private final List<Policy> mPolicies = new ArrayList<>();
+
+ private long mExecutionTime;
+
+ private static Clock sClock = new Clock();
+
+ protected BaseTask(int id) {
+ mId = id;
+ mExecutionTime = getTimeMillis();
+ }
+
+ /**
+ * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link
+ * #onCreate(Context, Intent, int, int)} returns.
+ */
+ @MainThread
+ public void setId(int id) {
+ Assert.isMainThread();
+ mId = id;
+ }
+
+ @MainThread
+ public boolean hasStarted() {
+ Assert.isMainThread();
+ return mHasStarted;
+ }
+
+ @MainThread
+ public boolean hasFailed() {
+ Assert.isMainThread();
+ return mHasFailed;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ public PhoneAccountHandle getPhoneAccountHandle() {
+ return mPhoneAccountHandle;
+ }
+ /**
+ * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will
+ * be missed.
+ */
+ @MainThread
+ public BaseTask addPolicy(Policy policy) {
+ Assert.isMainThread();
+ mPolicies.add(policy);
+ return this;
+ }
+
+ /**
+ * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution
+ * ends. This mechanism is used by policies for actions such as determining whether to schedule
+ * a retry. Must be call inside {@link #onExecuteInBackgroundThread()}
+ */
+ @WorkerThread
+ public void fail() {
+ Assert.isNotMainThread();
+ mHasFailed = true;
+ }
+
+ @MainThread
+ public void setExecutionTime(long timeMillis) {
+ Assert.isMainThread();
+ mExecutionTime = timeMillis;
+ }
+
+ public long getTimeMillis() {
+ return sClock.getTimeMillis();
+ }
+
+ /**
+ * Creates an intent that can be used to restart the current task. Derived class should build
+ * their intent upon this.
+ */
+ public Intent createRestartIntent() {
+ return createIntent(getContext(), this.getClass(), mPhoneAccountHandle);
+ }
+
+ /**
+ * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class
+ * should build their intent upon this.
+ */
+ public static Intent createIntent(Context context, Class<? extends BaseTask> task,
+ PhoneAccountHandle phoneAccountHandle) {
+ Intent intent = TaskSchedulerService.createIntent(context, task);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle);
+ return intent;
+ }
+
+ @Override
+ public TaskId getId() {
+ return new TaskId(mId, mPhoneAccountHandle);
+ }
+
+ @Override
+ @CallSuper
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ mContext = context;
+ mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ for (Policy policy : mPolicies) {
+ policy.onCreate(this, intent, flags, startId);
+ }
+ }
+
+ @Override
+ public long getReadyInMilliSeconds() {
+ return mExecutionTime - getTimeMillis();
+ }
+
+ @Override
+ @CallSuper
+ public void onBeforeExecute() {
+ for (Policy policy : mPolicies) {
+ policy.onBeforeExecute();
+ }
+ mHasStarted = true;
+ }
+
+ @Override
+ @CallSuper
+ public void onCompleted() {
+ if (mHasFailed) {
+ for (Policy policy : mPolicies) {
+ policy.onFail();
+ }
+ }
+
+ for (Policy policy : mPolicies) {
+ policy.onCompleted();
+ }
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded(Task task) {
+ for (Policy policy : mPolicies) {
+ policy.onDuplicatedTaskAdded();
+ }
+ }
+
+ @NeededForTesting
+ static class Clock {
+
+ public long getTimeMillis() {
+ return SystemClock.elapsedRealtime();
+ }
+ }
+
+ /**
+ * Used to replace the clock with an deterministic clock
+ */
+ @NeededForTesting
+ static void setClockForTesting(Clock clock) {
+ sClock = clock;
+ }
+}
diff --git a/java/com/android/voicemailomtp/scheduling/BlockerTask.java b/java/com/android/voicemailomtp/scheduling/BlockerTask.java
new file mode 100644
index 000000000..55ad9a7fd
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/BlockerTask.java
@@ -0,0 +1,55 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.voicemailomtp.VvmLog;
+
+/**
+ * Task to block another task of the same ID from being queued for a certain amount of time.
+ */
+public class BlockerTask extends BaseTask {
+
+ private static final String TAG = "BlockerTask";
+
+ public static final String EXTRA_TASK_ID = "extra_task_id";
+ public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis";
+
+ public BlockerTask() {
+ super(TASK_INVALID);
+ }
+
+ @Override
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID));
+ setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0));
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ // Do nothing.
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded(Task task) {
+ VvmLog
+ .v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining");
+ }
+}
diff --git a/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java
new file mode 100644
index 000000000..bef449b30
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java
@@ -0,0 +1,69 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Intent;
+
+import com.android.voicemailomtp.scheduling.Task.TaskId;
+
+/**
+ * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the
+ * task will be queued immediately, preventing the same task from running for a certain amount of
+ * time.
+ */
+public class MinimalIntervalPolicy implements Policy {
+
+ BaseTask mTask;
+ TaskId mId;
+ int mBlockForMillis;
+
+ public MinimalIntervalPolicy(int blockForMillis) {
+ mBlockForMillis = blockForMillis;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mId = mTask.getId();
+ }
+
+ @Override
+ public void onBeforeExecute() {
+
+ }
+
+ @Override
+ public void onCompleted() {
+ if (!mTask.hasFailed()) {
+ Intent intent = mTask
+ .createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle);
+ intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id);
+ intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis);
+ mTask.getContext().startService(intent);
+ }
+ }
+
+ @Override
+ public void onFail() {
+
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded() {
+
+ }
+}
diff --git a/java/com/android/voicemailomtp/scheduling/Policy.java b/java/com/android/voicemailomtp/scheduling/Policy.java
new file mode 100644
index 000000000..4a475d2ed
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/Policy.java
@@ -0,0 +1,36 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Intent;
+
+/**
+ * A set of listeners managed by {@link BaseTask} for common behaviors such as retrying. Call {@link
+ * BaseTask#addPolicy(Policy)} to add a policy.
+ */
+public interface Policy {
+
+ void onCreate(BaseTask task, Intent intent, int flags, int startId);
+
+ void onBeforeExecute();
+
+ void onCompleted();
+
+ void onFail();
+
+ void onDuplicatedTaskAdded();
+}
diff --git a/java/com/android/voicemailomtp/scheduling/PostponePolicy.java b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java
new file mode 100644
index 000000000..27a82f0ef
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java
@@ -0,0 +1,69 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Intent;
+
+import com.android.voicemailomtp.VvmLog;
+
+/**
+ * A task with Postpone policy will not be executed immediately. It will wait for a while and if a
+ * duplicated task is queued during the duration, the task will be postponed further. The task will
+ * only be executed if no new task was added in postponeMillis. Useful to batch small tasks in quick
+ * succession together.
+ */
+public class PostponePolicy implements Policy {
+
+ private static final String TAG = "PostponePolicy";
+
+ private final int mPostponeMillis;
+ private BaseTask mTask;
+
+ public PostponePolicy(int postponeMillis) {
+ mPostponeMillis = postponeMillis;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+ }
+
+ @Override
+ public void onBeforeExecute() {
+ // Do nothing
+ }
+
+ @Override
+ public void onCompleted() {
+ // Do nothing
+ }
+
+ @Override
+ public void onFail() {
+ // Do nothing
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded() {
+ if (mTask.hasStarted()) {
+ return;
+ }
+ VvmLog.d(TAG, "postponing " + mTask);
+ mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis);
+ }
+}
diff --git a/java/com/android/voicemailomtp/scheduling/RetryPolicy.java b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java
new file mode 100644
index 000000000..463657483
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java
@@ -0,0 +1,117 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+
+/**
+ * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been
+ * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most
+ * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between.
+ */
+public class RetryPolicy implements Policy {
+
+ private static final String TAG = "RetryPolicy";
+ private static final String EXTRA_RETRY_COUNT = "extra_retry_count";
+
+ private final int mRetryLimit;
+ private final int mRetryDelayMillis;
+
+ private BaseTask mTask;
+
+ private int mRetryCount;
+ private boolean mFailed;
+
+ private VoicemailStatus.DeferredEditor mVoicemailStatusEditor;
+
+ public RetryPolicy(int retryLimit, int retryDelayMillis) {
+ mRetryLimit = retryLimit;
+ mRetryDelayMillis = retryDelayMillis;
+ }
+
+ private boolean hasMoreRetries() {
+ return mRetryCount < mRetryLimit;
+ }
+
+ /**
+ * Error status should only be set if retries has exhausted or the task is successful. Status
+ * writes to this editor will be deferred until the task has ended, and will only be committed
+ * if the task is successful or there are no retries left.
+ */
+ public VoicemailStatus.Editor getVoicemailStatusEditor() {
+ return mVoicemailStatusEditor;
+ }
+
+ @Override
+ public void onCreate(BaseTask task, Intent intent, int flags, int startId) {
+ mTask = task;
+ mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0);
+ if (mRetryCount > 0) {
+ VvmLog.d(TAG, "retry #" + mRetryCount + " for " + mTask + " queued, executing in "
+ + mRetryDelayMillis);
+ mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis);
+ }
+ PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ VvmLog.e(TAG,
+ "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle());
+ // This should never happen, but continue on if it does. The status write will be
+ // discarded.
+ }
+ mVoicemailStatusEditor = VoicemailStatus
+ .deferredEdit(task.getContext(), phoneAccountHandle);
+ }
+
+ @Override
+ public void onBeforeExecute() {
+
+ }
+
+ @Override
+ public void onCompleted() {
+ if (!mFailed || !hasMoreRetries()) {
+ if (!mFailed) {
+ VvmLog.d(TAG, mTask.toString() + " completed successfully");
+ }
+ if (!hasMoreRetries()) {
+ VvmLog.d(TAG, "Retry limit for " + mTask + " reached");
+ }
+ VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues());
+ mVoicemailStatusEditor.deferredApply();
+ return;
+ }
+ VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues());
+ Intent intent = mTask.createRestartIntent();
+ intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1);
+
+ mTask.getContext().startService(intent);
+ }
+
+ @Override
+ public void onFail() {
+ mFailed = true;
+ }
+
+ @Override
+ public void onDuplicatedTaskAdded() {
+
+ }
+}
diff --git a/java/com/android/voicemailomtp/scheduling/Task.java b/java/com/android/voicemailomtp/scheduling/Task.java
new file mode 100644
index 000000000..61c35396b
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/Task.java
@@ -0,0 +1,133 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+
+import java.util.Objects;
+
+/**
+ * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to
+ * the scheduler, The task must be constructable with the intent. Specifically, It must have a
+ * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link
+ * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the
+ * Task.
+ *
+ * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread.
+ */
+public interface Task {
+
+ /**
+ * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should
+ * be set before {@link Task#onCreate(Context, Intent, int, int) returns}
+ */
+ int TASK_INVALID = -1;
+
+ /**
+ * TaskId to indicate it should always be queued regardless of duplicates. {@link
+ * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId.
+ */
+ int TASK_ALLOW_DUPLICATES = -2;
+
+ int TASK_UPLOAD = 1;
+ int TASK_SYNC = 2;
+ int TASK_ACTIVATION = 3;
+
+ /**
+ * Used to differentiate between types of tasks. If a task with the same TaskId is already in
+ * the queue the new task will be rejected.
+ */
+ class TaskId {
+
+ /**
+ * Indicates the operation type of the task.
+ */
+ public final int id;
+ /**
+ * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used
+ * to differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a
+ * sync task for their own.
+ */
+ public final PhoneAccountHandle phoneAccountHandle;
+
+ public TaskId(int id, PhoneAccountHandle phoneAccountHandle) {
+ this.id = id;
+ this.phoneAccountHandle = phoneAccountHandle;
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (!(object instanceof TaskId)) {
+ return false;
+ }
+ TaskId other = (TaskId) object;
+ return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, phoneAccountHandle);
+ }
+ }
+
+ TaskId getId();
+
+ @MainThread
+ void onCreate(Context context, Intent intent, int flags, int startId);
+
+ /**
+ * @return number of milliSeconds the scheduler should wait before running this task. A value
+ * less than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready.
+ * If no tasks are ready, the scheduler will sleep for this amount of time before doing another
+ * check (it will still wake if a new task is added). The first task in the queue that is ready
+ * will be executed.
+ */
+ @MainThread
+ long getReadyInMilliSeconds();
+
+ /**
+ * Called on the main thread when the scheduler is about to send the task into the worker
+ * thread, calling {@link #onExecuteInBackgroundThread()}
+ */
+ @MainThread
+ void onBeforeExecute();
+
+ /**
+ * The actual payload of the task, executed on the worker thread.
+ */
+ @WorkerThread
+ void onExecuteInBackgroundThread();
+
+ /**
+ * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown
+ * an uncaught exception. The task is already removed from the queue at this point, and a same
+ * task can be queued again.
+ */
+ @MainThread
+ void onCompleted();
+
+ /**
+ * Another task with the same TaskId has been added. Necessary data can be retrieved from the
+ * other task, and after this returns the task will be discarded.
+ */
+ @MainThread
+ void onDuplicatedTaskAdded(Task task);
+}
diff --git a/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java
new file mode 100644
index 000000000..90b50e913
--- /dev/null
+++ b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java
@@ -0,0 +1,392 @@
+/*
+ * 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.voicemailomtp.scheduling;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemClock;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+import android.support.annotation.WorkerThread;
+import com.android.voicemailomtp.Assert;
+import com.android.voicemailomtp.NeededForTesting;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.scheduling.Task.TaskId;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
+/**
+ * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time,
+ * and same task cannot exist in the queue at the same time. The service will be started when a
+ * intent is received, and stopped when there are no more tasks in the queue.
+ */
+public class TaskSchedulerService extends Service {
+
+ private static final String TAG = "VvmTaskScheduler";
+
+ private static final String ACTION_WAKEUP = "action_wakeup";
+
+ private static final int READY_TOLERANCE_MILLISECONDS = 100;
+
+ /**
+ * Threshold to determine whether to do a short or long sleep when a task is scheduled in the
+ * future.
+ *
+ * <p>A short sleep will continue to held the wake lock and use {@link
+ * Handler#postDelayed(Runnable, long)} to wait for the next task.
+ *
+ * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is
+ * exact and will wake up the device. Note: as this service is run in the telephony process it
+ * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The
+ * unbundled version should take doze into account.
+ */
+ private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000;
+ /**
+ * When there are no more tasks to be run the service should be stopped. But when all tasks has
+ * finished there might still be more tasks in the message queue waiting to be processed,
+ * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping
+ * the service to make sure there are no pending messages.
+ */
+ private static final int STOP_DELAY_MILLISECONDS = 5_000;
+ private static final String EXTRA_CLASS_NAME = "extra_class_name";
+
+ private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock";
+
+ // The thread to run tasks on
+ private volatile WorkerThreadHandler mWorkerThreadHandler;
+
+ private Context mContext = this;
+ /**
+ * Used by tests to turn task handling into a single threaded process by calling {@link
+ * Handler#handleMessage(Message)} directly
+ */
+ private MessageSender mMessageSender = new MessageSender();
+
+ private MainThreadHandler mMainThreadHandler;
+
+ private WakeLock mWakeLock;
+
+ /**
+ * Main thread only, access through {@link #getTasks()}
+ */
+ private final Queue<Task> mTasks = new ArrayDeque<>();
+ private boolean mWorkerThreadIsBusy = false;
+
+ private final Runnable mStopServiceWithDelay = new Runnable() {
+ @Override
+ public void run() {
+ VvmLog.d(TAG, "Stopping service");
+ stopSelf();
+ }
+ };
+ /**
+ * Should attempt to run the next task when a task has finished or been added.
+ */
+ private boolean mTaskAutoRunDisabledForTesting = false;
+
+ @VisibleForTesting
+ final class WorkerThreadHandler extends Handler {
+
+ public WorkerThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @WorkerThread
+ public void handleMessage(Message msg) {
+ Assert.isNotMainThread();
+ Task task = (Task) msg.obj;
+ try {
+ VvmLog.v(TAG, "executing task " + task);
+ task.onExecuteInBackgroundThread();
+ } catch (Throwable throwable) {
+ VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable);
+ }
+
+ Message schedulerMessage = mMainThreadHandler.obtainMessage();
+ schedulerMessage.obj = task;
+ mMessageSender.send(schedulerMessage);
+ }
+ }
+
+ @VisibleForTesting
+ final class MainThreadHandler extends Handler {
+
+ public MainThreadHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ @MainThread
+ public void handleMessage(Message msg) {
+ Assert.isMainThread();
+ Task task = (Task) msg.obj;
+ getTasks().remove(task);
+ task.onCompleted();
+ mWorkerThreadIsBusy = false;
+ maybeRunNextTask();
+ }
+ }
+
+ @Override
+ @MainThread
+ public void onCreate() {
+ super.onCreate();
+ mWakeLock = getSystemService(PowerManager.class)
+ .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);
+ mWakeLock.setReferenceCounted(false);
+ HandlerThread thread = new HandlerThread("VvmTaskSchedulerService");
+ thread.start();
+
+ mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper());
+ mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper());
+ }
+
+ @Override
+ public void onDestroy() {
+ mWorkerThreadHandler.getLooper().quit();
+ mWakeLock.release();
+ }
+
+ @Override
+ @MainThread
+ public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
+ Assert.isMainThread();
+ // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping
+ // the service.
+ mWakeLock.acquire();
+ if (ACTION_WAKEUP.equals(intent.getAction())) {
+ VvmLog.d(TAG, "woke up by AlarmManager");
+ } else {
+ Task task = createTask(intent, flags, startId);
+ if (task == null) {
+ VvmLog.e(TAG, "cannot create task form intent");
+ } else {
+ addTask(task);
+ }
+ }
+ maybeRunNextTask();
+ // STICKY means the service will be automatically restarted will the last intent if it is
+ // killed.
+ return START_NOT_STICKY;
+ }
+
+ @MainThread
+ @VisibleForTesting
+ void addTask(Task task) {
+ Assert.isMainThread();
+ if (task.getId().id == Task.TASK_INVALID) {
+ throw new AssertionError("Task id was not set to a valid value before adding.");
+ }
+ if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) {
+ Task oldTask = getTask(task.getId());
+ if (oldTask != null) {
+ oldTask.onDuplicatedTaskAdded(task);
+ return;
+ }
+ }
+ mMainThreadHandler.removeCallbacks(mStopServiceWithDelay);
+ getTasks().add(task);
+ maybeRunNextTask();
+ }
+
+ @MainThread
+ @Nullable
+ private Task getTask(TaskId taskId) {
+ Assert.isMainThread();
+ for (Task task : getTasks()) {
+ if (task.getId().equals(taskId)) {
+ return task;
+ }
+ }
+ return null;
+ }
+
+ @MainThread
+ private Queue<Task> getTasks() {
+ Assert.isMainThread();
+ return mTasks;
+ }
+
+ /**
+ * Create an intent that will queue the <code>task</code>
+ */
+ public static Intent createIntent(Context context, Class<? extends Task> task) {
+ Intent intent = new Intent(context, TaskSchedulerService.class);
+ intent.putExtra(EXTRA_CLASS_NAME, task.getName());
+ return intent;
+ }
+
+ @VisibleForTesting
+ @MainThread
+ @Nullable
+ Task createTask(@Nullable Intent intent, int flags, int startId) {
+ Assert.isMainThread();
+ if (intent == null) {
+ return null;
+ }
+ String className = intent.getStringExtra(EXTRA_CLASS_NAME);
+ VvmLog.d(TAG, "create task:" + className);
+ if (className == null) {
+ throw new IllegalArgumentException("EXTRA_CLASS_NAME expected");
+ }
+ try {
+ Task task = (Task) Class.forName(className).newInstance();
+ task.onCreate(mContext, intent, flags, startId);
+ return task;
+ } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @MainThread
+ private void maybeRunNextTask() {
+ Assert.isMainThread();
+ if (mWorkerThreadIsBusy) {
+ return;
+ }
+ if (mTaskAutoRunDisabledForTesting) {
+ // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called
+ // to run the next task.
+ return;
+ }
+
+ runNextTask();
+ }
+
+ @VisibleForTesting
+ @MainThread
+ void runNextTask() {
+ Assert.isMainThread();
+ // The current alarm is no longer valid, a new one will be set up if required.
+ getSystemService(AlarmManager.class).cancel(getWakeupIntent());
+ if (getTasks().isEmpty()) {
+ prepareStop();
+ return;
+ }
+ Long minimalWaitTime = null;
+ for (Task task : getTasks()) {
+ long waitTime = task.getReadyInMilliSeconds();
+ if (waitTime < READY_TOLERANCE_MILLISECONDS) {
+ task.onBeforeExecute();
+ Message message = mWorkerThreadHandler.obtainMessage();
+ message.obj = task;
+ mWorkerThreadIsBusy = true;
+ mMessageSender.send(message);
+ return;
+ } else {
+ if (minimalWaitTime == null || waitTime < minimalWaitTime) {
+ minimalWaitTime = waitTime;
+ }
+ }
+ }
+ VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime);
+ if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) {
+ // No tasks are currently ready. Sleep until the next one should be.
+ // If a new task is added during the sleep the service will wake immediately.
+ sleep(minimalWaitTime);
+ }
+ }
+
+ private void sleep(long timeMillis) {
+ if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) {
+ mMainThreadHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ maybeRunNextTask();
+ }
+ }, timeMillis);
+ return;
+ }
+
+ // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could
+ // optimize the battery usage. As this service currently run in the telephony process the
+ // OS give it privileges to behave the same as setExact(), but set() is the targeted
+ // behavior once this is unbundled.
+ getSystemService(AlarmManager.class).set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
+ SystemClock.elapsedRealtime() + timeMillis,
+ getWakeupIntent());
+ mWakeLock.release();
+ VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis");
+ }
+
+ private PendingIntent getWakeupIntent() {
+ Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass());
+ return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ private void prepareStop() {
+ VvmLog.d(TAG,
+ "No more tasks, stopping service if no task are added in "
+ + STOP_DELAY_MILLISECONDS + " millis");
+ mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS);
+ }
+
+ static class MessageSender {
+
+ public void send(Message message) {
+ message.sendToTarget();
+ }
+ }
+
+ @NeededForTesting
+ void setContextForTest(Context context) {
+ mContext = context;
+ }
+
+ @NeededForTesting
+ void setTaskAutoRunDisabledForTest(boolean value) {
+ mTaskAutoRunDisabledForTesting = value;
+ }
+
+ @NeededForTesting
+ void setMessageSenderForTest(MessageSender sender) {
+ mMessageSender = sender;
+ }
+
+ @NeededForTesting
+ void clearTasksForTest() {
+ mTasks.clear();
+ }
+
+ @Override
+ @Nullable
+ public IBinder onBind(Intent intent) {
+ return new LocalBinder();
+ }
+
+ @NeededForTesting
+ class LocalBinder extends Binder {
+
+ @NeededForTesting
+ public TaskSchedulerService getService() {
+ return TaskSchedulerService.this;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java
new file mode 100644
index 000000000..5cec52842
--- /dev/null
+++ b/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java
@@ -0,0 +1,77 @@
+/*
+ * 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.voicemailomtp.settings;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VisualVoicemailPreferences;
+import com.android.voicemailomtp.sync.OmtpVvmSourceManager;
+
+/**
+ * Save whether or not a particular account is enabled in shared to be retrieved later.
+ */
+public class VisualVoicemailSettingsUtil {
+
+ private static final String IS_ENABLED_KEY = "is_enabled";
+
+
+ public static void setEnabled(Context context, PhoneAccountHandle phoneAccount,
+ boolean isEnabled) {
+ new VisualVoicemailPreferences(context, phoneAccount).edit()
+ .putBoolean(IS_ENABLED_KEY, isEnabled)
+ .apply();
+ OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount);
+ if (isEnabled) {
+ OmtpVvmSourceManager.getInstance(context).addPhoneStateListener(phoneAccount);
+ config.startActivation();
+ } else {
+ OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount);
+ config.startDeactivation();
+ }
+ }
+
+ public static boolean isEnabled(Context context,
+ PhoneAccountHandle phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+
+ VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+ if (prefs.contains(IS_ENABLED_KEY)) {
+ // isEnableByDefault is a bit expensive, so don't use it as default value of
+ // getBoolean(). The "false" here should never be actually used.
+ return prefs.getBoolean(IS_ENABLED_KEY, false);
+ }
+ return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault();
+ }
+
+ /**
+ * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM
+ * app is installed). This is used to determine whether to disable the client when the carrier
+ * VVM app is installed. If the carrier VVM app is installed the client should give priority to
+ * it if the settings are not touched.
+ */
+ public static boolean isEnabledUserSet(Context context,
+ PhoneAccountHandle phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+ VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount);
+ return prefs.contains(IS_ENABLED_KEY);
+ }
+}
diff --git a/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java b/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java
new file mode 100644
index 000000000..e679e9970
--- /dev/null
+++ b/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java
@@ -0,0 +1,634 @@
+/*
+ * 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.voicemailomtp.settings;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputFilter.LengthFilter;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+import android.widget.Toast;
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpConstants.ChangePinResult;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.R;
+import com.android.voicemailomtp.VisualVoicemailPreferences;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.imap.ImapHelper;
+import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
+import com.android.voicemailomtp.mail.MessagingException;
+import com.android.voicemailomtp.sync.VvmNetworkRequestCallback;
+
+/**
+ * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing
+ * traditional voicemail through phone call. The intent to launch this activity must contain {@link
+ * #EXTRA_PHONE_ACCOUNT_HANDLE}
+ */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class VoicemailChangePinActivity extends Activity
+ implements OnClickListener, OnEditorActionListener, TextWatcher {
+
+ private static final String TAG = "VmChangePinActivity";
+
+ public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+
+ private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin";
+
+ private static final int MESSAGE_HANDLE_RESULT = 1;
+
+ private PhoneAccountHandle mPhoneAccountHandle;
+ private OmtpVvmCarrierConfigHelper mConfig;
+
+ private int mPinMinLength;
+ private int mPinMaxLength;
+
+ private State mUiState = State.Initial;
+ private String mOldPin;
+ private String mFirstPin;
+
+ private ProgressDialog mProgressDialog;
+
+ private TextView mHeaderText;
+ private TextView mHintText;
+ private TextView mErrorText;
+ private EditText mPinEntry;
+ private Button mCancelButton;
+ private Button mNextButton;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ if (message.what == MESSAGE_HANDLE_RESULT) {
+ mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1);
+ }
+ }
+ };
+
+ private enum State {
+ /**
+ * Empty state to handle initial state transition. Will immediately switch into {@link
+ * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin}
+ * if not.
+ */
+ Initial,
+ /**
+ * Prompt the user to enter old PIN. The PIN will be verified with the server before
+ * proceeding to {@link #EnterNewPin}.
+ */
+ EnterOldPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.setHeader(R.string.change_pin_enter_old_pin_header);
+ activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint);
+ activity.mNextButton.setText(R.string.change_pin_continue_label);
+ activity.mErrorText.setText(null);
+ }
+
+ @Override
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0);
+ }
+
+
+ @Override
+ public void handleNext(VoicemailChangePinActivity activity) {
+ activity.mOldPin = activity.getCurrentPasswordInput();
+ activity.verifyOldPin();
+ }
+
+ @Override
+ public void handleResult(VoicemailChangePinActivity activity,
+ @ChangePinResult int result) {
+ if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ activity.updateState(State.EnterNewPin);
+ } else {
+ CharSequence message = activity.getChangePinResultMessage(result);
+ activity.showError(message);
+ activity.mPinEntry.setText("");
+ }
+ }
+ },
+ /**
+ * The default old PIN is found. Show a blank screen while verifying with the server to make
+ * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}.
+ * If not, the user probably changed the PIN through other means, proceed to {@link
+ * #EnterOldPin}. If any other issue caused the verifying to fail, show an error and exit.
+ */
+ VerifyOldPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE);
+ activity.verifyOldPin();
+ }
+
+ @Override
+ public void handleResult(final VoicemailChangePinActivity activity,
+ @ChangePinResult int result) {
+ if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ activity.updateState(State.EnterNewPin);
+ } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) {
+ activity.getWindow().setSoftInputMode(
+ WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ activity.showError(activity.getString(R.string.change_pin_system_error),
+ new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ activity.finish();
+ }
+ });
+ } else {
+ VvmLog.e(TAG, "invalid default old PIN: " + activity
+ .getChangePinResultMessage(result));
+ // If the default old PIN is rejected by the server, the PIN is probably changed
+ // through other means, or the generated pin is invalid
+ // Wipe the default old PIN so the old PIN input box will be shown to the user
+ // on the next time.
+ setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+ activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+ activity.updateState(State.EnterOldPin);
+ }
+ }
+
+ @Override
+ public void onLeave(VoicemailChangePinActivity activity) {
+ activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE);
+ }
+ },
+ /**
+ * Let the user enter the new PIN and validate the format. Only length is enforced, PIN
+ * strength check relies on the server. After a valid PIN is entered, proceed to {@link
+ * #ConfirmNewPin}
+ */
+ EnterNewPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header);
+ activity.mNextButton.setText(R.string.change_pin_continue_label);
+ activity.mHintText.setText(
+ activity.getString(R.string.change_pin_enter_new_pin_hint,
+ activity.mPinMinLength, activity.mPinMaxLength));
+ }
+
+ @Override
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ String password = activity.getCurrentPasswordInput();
+ if (password.length() == 0) {
+ activity.setNextEnabled(false);
+ return;
+ }
+ CharSequence error = activity.validatePassword(password);
+ if (error != null) {
+ activity.mErrorText.setText(error);
+ activity.setNextEnabled(false);
+ } else {
+ activity.mErrorText.setText(null);
+ activity.setNextEnabled(true);
+ }
+ }
+
+ @Override
+ public void handleNext(VoicemailChangePinActivity activity) {
+ CharSequence errorMsg;
+ errorMsg = activity.validatePassword(activity.getCurrentPasswordInput());
+ if (errorMsg != null) {
+ activity.showError(errorMsg);
+ return;
+ }
+ activity.mFirstPin = activity.getCurrentPasswordInput();
+ activity.updateState(State.ConfirmNewPin);
+ }
+ },
+ /**
+ * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a
+ * PIN change to the server. Finish the activity if succeeded. Return to {@link
+ * #EnterOldPin} if the old PIN is rejected, {@link #EnterNewPin} for other failure.
+ */
+ ConfirmNewPin {
+ @Override
+ public void onEnter(VoicemailChangePinActivity activity) {
+ activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header);
+ activity.mHintText.setText(null);
+ activity.mNextButton.setText(R.string.change_pin_ok_label);
+ }
+
+ @Override
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ if (activity.getCurrentPasswordInput().length() == 0) {
+ activity.setNextEnabled(false);
+ return;
+ }
+ if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) {
+ activity.setNextEnabled(true);
+ activity.mErrorText.setText(null);
+ } else {
+ activity.setNextEnabled(false);
+ activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match);
+ }
+ }
+
+ @Override
+ public void handleResult(VoicemailChangePinActivity activity,
+ @ChangePinResult int result) {
+ if (result == OmtpConstants.CHANGE_PIN_SUCCESS) {
+ // If the PIN change succeeded we no longer know what the old (current) PIN is.
+ // Wipe the default old PIN so the old PIN input box will be shown to the user
+ // on the next time.
+ setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null);
+ activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET);
+
+ activity.finish();
+
+ Toast.makeText(activity, activity.getString(R.string.change_pin_succeeded),
+ Toast.LENGTH_SHORT).show();
+ } else {
+ CharSequence message = activity.getChangePinResultMessage(result);
+ VvmLog.i(TAG, "Change PIN failed: " + message);
+ activity.showError(message);
+ if (result == OmtpConstants.CHANGE_PIN_MISMATCH) {
+ // Somehow the PIN has changed, prompt to enter the old PIN again.
+ activity.updateState(State.EnterOldPin);
+ } else {
+ // The new PIN failed to fulfil other restrictions imposed by the server.
+ activity.updateState(State.EnterNewPin);
+ }
+
+ }
+
+ }
+
+ @Override
+ public void handleNext(VoicemailChangePinActivity activity) {
+ activity.processPinChange(activity.mOldPin, activity.mFirstPin);
+ }
+ };
+
+ /**
+ * The activity has switched from another state to this one.
+ */
+ public void onEnter(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ /**
+ * The user has typed something into the PIN input field. Also called after {@link
+ * #onEnter(VoicemailChangePinActivity)}
+ */
+ public void onInputChanged(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ /**
+ * The asynchronous call to change the PIN on the server has returned.
+ */
+ public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) {
+ // Do nothing
+ }
+
+ /**
+ * The user has pressed the "next" button.
+ */
+ public void handleNext(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ /**
+ * The activity has switched from this state to another one.
+ */
+ public void onLeave(VoicemailChangePinActivity activity) {
+ // Do nothing
+ }
+
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle);
+ setContentView(R.layout.voicemail_change_pin);
+ setTitle(R.string.change_pin_title);
+
+ readPinLength();
+
+ View view = findViewById(android.R.id.content);
+
+ mCancelButton = (Button) view.findViewById(R.id.cancel_button);
+ mCancelButton.setOnClickListener(this);
+ mNextButton = (Button) view.findViewById(R.id.next_button);
+ mNextButton.setOnClickListener(this);
+
+ mPinEntry = (EditText) view.findViewById(R.id.pin_entry);
+ mPinEntry.setOnEditorActionListener(this);
+ mPinEntry.addTextChangedListener(this);
+ if (mPinMaxLength != 0) {
+ mPinEntry.setFilters(new InputFilter[]{new LengthFilter(mPinMaxLength)});
+ }
+
+
+ mHeaderText = (TextView) view.findViewById(R.id.headerText);
+ mHintText = (TextView) view.findViewById(R.id.hintText);
+ mErrorText = (TextView) view.findViewById(R.id.errorText);
+
+ if (isDefaultOldPinSet(this, mPhoneAccountHandle)) {
+ mOldPin = getDefaultOldPin(this, mPhoneAccountHandle);
+ updateState(State.VerifyOldPin);
+ } else {
+ updateState(State.EnterOldPin);
+ }
+ }
+
+ private void handleOmtpEvent(OmtpEvents event) {
+ mConfig.handleEvent(getVoicemailStatusEditor(), event);
+ }
+
+ private VoicemailStatus.Editor getVoicemailStatusEditor() {
+ // This activity does not have any automatic retry mechanism, errors should be written right
+ // away.
+ return VoicemailStatus.edit(this, mPhoneAccountHandle);
+ }
+
+ /**
+ * Extracts the pin length requirement sent by the server with a STATUS SMS.
+ */
+ private void readPinLength() {
+ VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this,
+ mPhoneAccountHandle);
+ // The OMTP pin length format is {min}-{max}
+ String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-");
+ if (lengths.length == 2) {
+ try {
+ mPinMinLength = Integer.parseInt(lengths[0]);
+ mPinMaxLength = Integer.parseInt(lengths[1]);
+ } catch (NumberFormatException e) {
+ mPinMinLength = 0;
+ mPinMaxLength = 0;
+ }
+ } else {
+ mPinMinLength = 0;
+ mPinMaxLength = 0;
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ updateState(mUiState);
+
+ }
+
+ public void handleNext() {
+ if (mPinEntry.length() == 0) {
+ return;
+ }
+ mUiState.handleNext(this);
+ }
+
+ public void onClick(View v) {
+ if (v.getId() == R.id.next_button) {
+ handleNext();
+ } else if (v.getId() == R.id.cancel_button) {
+ finish();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (!mNextButton.isEnabled()) {
+ return true;
+ }
+ // Check if this was the result of hitting the enter or "done" key
+ if (actionId == EditorInfo.IME_NULL
+ || actionId == EditorInfo.IME_ACTION_DONE
+ || actionId == EditorInfo.IME_ACTION_NEXT) {
+ handleNext();
+ return true;
+ }
+ return false;
+ }
+
+ public void afterTextChanged(Editable s) {
+ mUiState.onInputChanged(this);
+ }
+
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing
+ }
+
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Do nothing
+ }
+
+ /**
+ * After replacing the default PIN with a random PIN, call this to store the random PIN. The
+ * stored PIN will be automatically entered when the user attempts to change the PIN.
+ */
+ public static void setDefaultOldPIN(Context context, PhoneAccountHandle phoneAccountHandle,
+ String pin) {
+ new VisualVoicemailPreferences(context, phoneAccountHandle)
+ .edit().putString(KEY_DEFAULT_OLD_PIN, pin).apply();
+ }
+
+ public static boolean isDefaultOldPinSet(Context context,
+ PhoneAccountHandle phoneAccountHandle) {
+ return getDefaultOldPin(context, phoneAccountHandle) != null;
+ }
+
+ private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) {
+ return new VisualVoicemailPreferences(context, phoneAccountHandle)
+ .getString(KEY_DEFAULT_OLD_PIN);
+ }
+
+ private String getCurrentPasswordInput() {
+ return mPinEntry.getText().toString();
+ }
+
+ private void updateState(State state) {
+ State previousState = mUiState;
+ mUiState = state;
+ if (previousState != state) {
+ previousState.onLeave(this);
+ mPinEntry.setText("");
+ mUiState.onEnter(this);
+ }
+ mUiState.onInputChanged(this);
+ }
+
+ /**
+ * Validates PIN and returns a message to display if PIN fails test.
+ *
+ * @param password the raw password the user typed in
+ * @return error message to show to user or null if password is OK
+ */
+ private CharSequence validatePassword(String password) {
+ if (mPinMinLength == 0 && mPinMaxLength == 0) {
+ // Invalid length requirement is sent by the server, just accept anything and let the
+ // server decide.
+ return null;
+ }
+
+ if (password.length() < mPinMinLength) {
+ return getString(R.string.vm_change_pin_error_too_short);
+ }
+ return null;
+ }
+
+ private void setHeader(int text) {
+ mHeaderText.setText(text);
+ mPinEntry.setContentDescription(mHeaderText.getText());
+ }
+
+ /**
+ * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not
+ * {@link OmtpConstants#CHANGE_PIN_SUCCESS}
+ */
+ private CharSequence getChangePinResultMessage(@ChangePinResult int result) {
+ switch (result) {
+ case OmtpConstants.CHANGE_PIN_TOO_SHORT:
+ return getString(R.string.vm_change_pin_error_too_short);
+ case OmtpConstants.CHANGE_PIN_TOO_LONG:
+ return getString(R.string.vm_change_pin_error_too_long);
+ case OmtpConstants.CHANGE_PIN_TOO_WEAK:
+ return getString(R.string.vm_change_pin_error_too_weak);
+ case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER:
+ return getString(R.string.vm_change_pin_error_invalid);
+ case OmtpConstants.CHANGE_PIN_MISMATCH:
+ return getString(R.string.vm_change_pin_error_mismatch);
+ case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR:
+ return getString(R.string.vm_change_pin_error_system_error);
+ default:
+ VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result);
+ return null;
+ }
+ }
+
+ private void verifyOldPin() {
+ processPinChange(mOldPin, mOldPin);
+ }
+
+ private void setNextEnabled(boolean enabled) {
+ mNextButton.setEnabled(enabled);
+ }
+
+
+ private void showError(CharSequence message) {
+ showError(message, null);
+ }
+
+ private void showError(CharSequence message, @Nullable OnDismissListener callback) {
+ new AlertDialog.Builder(this)
+ .setMessage(message)
+ .setPositiveButton(android.R.string.ok, null)
+ .setOnDismissListener(callback)
+ .show();
+ }
+
+ /**
+ * Asynchronous call to change the PIN on the server.
+ */
+ private void processPinChange(String oldPin, String newPin) {
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message));
+ mProgressDialog.show();
+
+ ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin,
+ newPin);
+ callback.requestNetwork();
+ }
+
+ private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+ private final String mOldPin;
+ private final String mNewPin;
+
+ public ChangePinNetworkRequestCallback(String oldPin, String newPin) {
+ super(mConfig, mPhoneAccountHandle,
+ VoicemailChangePinActivity.this.getVoicemailStatusEditor());
+ mOldPin = oldPin;
+ mNewPin = newPin;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ try (ImapHelper helper =
+ new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network,
+ getVoicemailStatusEditor())) {
+
+ @ChangePinResult int result =
+ helper.changePin(mOldPin, mNewPin);
+ sendResult(result);
+ } catch (InitializingException | MessagingException e) {
+ VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e);
+ sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+ }
+ }
+
+ @Override
+ public void onFailed(String reason) {
+ super.onFailed(reason);
+ sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR);
+ }
+
+ private void sendResult(@ChangePinResult int result) {
+ VvmLog.i(TAG, "Change PIN result: " + result);
+ if (mProgressDialog.isShowing() && !VoicemailChangePinActivity.this.isDestroyed() &&
+ !VoicemailChangePinActivity.this.isFinishing()) {
+ mProgressDialog.dismiss();
+ } else {
+ VvmLog.i(TAG, "Dialog not visible, not dismissing");
+ }
+ mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget();
+ releaseNetwork();
+ }
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java b/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java
new file mode 100644
index 000000000..ac0df6fab
--- /dev/null
+++ b/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java
@@ -0,0 +1,222 @@
+/**
+ * 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.voicemailomtp.settings;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.R;
+import com.android.voicemailomtp.SubscriptionInfoHelper;
+import com.android.voicemailomtp.VisualVoicemailPreferences;
+
+public class VoicemailSettingsActivity extends PreferenceActivity implements
+ Preference.OnPreferenceChangeListener {
+ private static final String LOG_TAG = VoicemailSettingsActivity.class.getSimpleName();
+ private static final boolean DBG = true;
+
+ /**
+ * Intent action to bring up Voicemail Provider settings
+ * DO NOT RENAME. There are existing apps which use this intent value.
+ */
+ public static final String ACTION_ADD_VOICEMAIL =
+ "com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL";
+
+ /**
+ * Intent action to bring up the {@code VoicemailSettingsActivity}.
+ * DO NOT RENAME. There are existing apps which use this intent value.
+ */
+ public static final String ACTION_CONFIGURE_VOICEMAIL =
+ "com.android.voicemailomtp.CallFeaturesSetting.CONFIGURE_VOICEMAIL";
+
+ // Extra put in the return from VM provider config containing voicemail number to set
+ public static final String VM_NUMBER_EXTRA = "com.android.voicemailomtp.VoicemailNumber";
+ // Extra put in the return from VM provider config containing call forwarding number to set
+ public static final String FWD_NUMBER_EXTRA = "com.android.voicemailomtp.ForwardingNumber";
+ // Extra put in the return from VM provider config containing call forwarding number to set
+ public static final String FWD_NUMBER_TIME_EXTRA = "com.android.voicemailomtp.ForwardingNumberTime";
+ // If the VM provider returns non null value in this extra we will force the user to
+ // choose another VM provider
+ public static final String SIGNOUT_EXTRA = "com.android.voicemailomtp.Signout";
+
+ /**
+ * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden
+ * in the list of providers presented to the user. This allows a provider which is being
+ * disabled (e.g. GV user logging out) to force the user to pick some other provider.
+ */
+ public static final String IGNORE_PROVIDER_EXTRA = "com.android.voicemailomtp.ProviderToIgnore";
+
+ /**
+ * String Extra put into ACTION_ADD_VOICEMAIL to indicate that the voicemail setup screen should
+ * be opened.
+ */
+ public static final String SETUP_VOICEMAIL_EXTRA = "com.android.voicemailomtp.SetupVoicemail";
+
+ /** Event for Async voicemail change call */
+ private static final int EVENT_VOICEMAIL_CHANGED = 500;
+ private static final int EVENT_FORWARDING_CHANGED = 501;
+ private static final int EVENT_FORWARDING_GET_COMPLETED = 502;
+
+ /** Handle to voicemail pref */
+ private static final int VOICEMAIL_PREF_ID = 1;
+ private static final int VOICEMAIL_PROVIDER_CFG_ID = 2;
+
+ /**
+ * Used to indicate that the voicemail preference should be shown.
+ */
+ private boolean mShowVoicemailPreference = false;
+
+ private int mSubId;
+ private PhoneAccountHandle mPhoneAccountHandle;
+ private SubscriptionInfoHelper mSubscriptionInfoHelper;
+ private OmtpVvmCarrierConfigHelper mOmtpVvmCarrierConfigHelper;
+
+ private SwitchPreference mVoicemailVisualVoicemail;
+ private Preference mVoicemailChangePinPreference;
+
+ //*********************************************************************************************
+ // Preference Activity Methods
+ //*********************************************************************************************
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ // Show the voicemail preference in onResume if the calling intent specifies the
+ // ACTION_ADD_VOICEMAIL action.
+ mShowVoicemailPreference = (icicle == null) &&
+ TextUtils.equals(getIntent().getAction(), ACTION_ADD_VOICEMAIL);
+
+ mSubscriptionInfoHelper = new SubscriptionInfoHelper(this, getIntent());
+ mSubscriptionInfoHelper.setActionBarTitle(
+ getActionBar(), getResources(), R.string.voicemail_settings_with_label);
+ mSubId = mSubscriptionInfoHelper.getSubId();
+ // TODO: scrap this activity.
+ /*
+ mPhoneAccountHandle = PhoneAccountHandleConverter
+ .fromSubId(this, mSubId);
+
+ mOmtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(
+ this, mSubId);
+ */
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ PreferenceScreen preferenceScreen = getPreferenceScreen();
+ if (preferenceScreen != null) {
+ preferenceScreen.removeAll();
+ }
+
+ addPreferencesFromResource(R.xml.voicemail_settings);
+
+ PreferenceScreen prefSet = getPreferenceScreen();
+
+ mVoicemailVisualVoicemail = (SwitchPreference) findPreference(
+ getResources().getString(R.string.voicemail_visual_voicemail_key));
+
+ mVoicemailChangePinPreference = findPreference(
+ getResources().getString(R.string.voicemail_change_pin_key));
+ Intent changePinIntent = new Intent(new Intent(this, VoicemailChangePinActivity.class));
+ changePinIntent.putExtra(VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE,
+ mPhoneAccountHandle);
+
+ mVoicemailChangePinPreference.setIntent(changePinIntent);
+ if (VoicemailChangePinActivity.isDefaultOldPinSet(this, mPhoneAccountHandle)) {
+ mVoicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title);
+ } else {
+ mVoicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title);
+ }
+
+ if (mOmtpVvmCarrierConfigHelper.isValid()) {
+ mVoicemailVisualVoicemail.setOnPreferenceChangeListener(this);
+ mVoicemailVisualVoicemail.setChecked(
+ VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle));
+ if (!isVisualVoicemailActivated()) {
+ prefSet.removePreference(mVoicemailChangePinPreference);
+ }
+ } else {
+ prefSet.removePreference(mVoicemailVisualVoicemail);
+ prefSet.removePreference(mVoicemailChangePinPreference);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Implemented to support onPreferenceChangeListener to look for preference changes.
+ *
+ * @param preference is the preference to be changed
+ * @param objValue should be the value of the selection, NOT its localized
+ * display value.
+ */
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (DBG) log("onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\"");
+ if (preference.getKey().equals(mVoicemailVisualVoicemail.getKey())) {
+ boolean isEnabled = (boolean) objValue;
+ VisualVoicemailSettingsUtil
+ .setEnabled(this, mPhoneAccountHandle, isEnabled);
+ PreferenceScreen prefSet = getPreferenceScreen();
+ if (isVisualVoicemailActivated()) {
+ prefSet.addPreference(mVoicemailChangePinPreference);
+ } else {
+ prefSet.removePreference(mVoicemailChangePinPreference);
+ }
+ }
+
+ // Always let the preference setting proceed.
+ return true;
+ }
+
+ private boolean isVisualVoicemailActivated() {
+ if (!VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle)) {
+ return false;
+ }
+ VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this,
+ mPhoneAccountHandle);
+ return preferences.getString(OmtpConstants.SERVER_ADDRESS, null) != null;
+
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java b/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java
new file mode 100644
index 000000000..bb722bffc
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2016 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.voicemailomtp.sms;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.TelephonyManagerStub;
+import com.android.voicemailomtp.VvmLog;
+
+/**
+ * Class ot handle voicemail SMS under legacy mode
+ *
+ * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled()
+ */
+public class LegacyModeSmsHandler {
+
+ private static final String TAG = "LegacyModeSmsHandler";
+
+ public static void handle(Context context, VisualVoicemailSms sms) {
+ VvmLog.v(TAG, "processing VVM SMS on legacy mode");
+ String eventType = sms.getPrefix();
+ Bundle data = sms.getFields();
+ PhoneAccountHandle handle = sms.getPhoneAccountHandle();
+
+ if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+ SyncMessage message = new SyncMessage(data);
+ VvmLog.v(TAG, "Received SYNC sms for " + handle +
+ " with event " + message.getSyncTriggerEvent());
+
+ switch (message.getSyncTriggerEvent()) {
+ case OmtpConstants.NEW_MESSAGE:
+ case OmtpConstants.MAILBOX_UPDATE:
+ // The user has called into the voicemail and the new message count could
+ // change.
+ // For some carriers new message count could be set to 0 even if there are still
+ // unread messages, to clear the message waiting indicator.
+ VvmLog.v(TAG, "updating MWI");
+
+ // Setting voicemail message count to non-zero will show the telephony voicemail
+ // notification, and zero will clear it.
+ TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount());
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java
new file mode 100644
index 000000000..63af2c13d
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemailomtp.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import com.android.voicemailomtp.OmtpConstants;
+
+/**
+ * An implementation of the OmtpMessageSender for T-Mobile.
+ */
+public class OmtpCvvmMessageSender extends OmtpMessageSender {
+ public OmtpCvvmMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
+ short applicationPort, String destinationNumber) {
+ super(context, phoneAccountHandle, applicationPort, destinationNumber);
+ }
+
+ @Override
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+ sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent);
+ }
+
+ @Override
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+ sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent);
+ }
+
+ @Override
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+ sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent);
+ }
+
+ private void sendCvvmMessage(String request, PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(request);
+ sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+ appendField(sb, "dt", "15");
+ sendSms(sb.toString(), sentIntent);
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java b/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java
new file mode 100644
index 000000000..c4ad2085f
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java
@@ -0,0 +1,162 @@
+/*
+ * 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.voicemailomtp.sms;
+
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.UserManager;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemailomtp.ActivationTask;
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpService;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.Voicemail;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
+import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemailomtp.sync.OmtpVvmSyncService;
+import com.android.voicemailomtp.sync.SyncOneTask;
+import com.android.voicemailomtp.sync.SyncTask;
+import com.android.voicemailomtp.sync.VoicemailsQueryHelper;
+import com.android.voicemailomtp.utils.VoicemailDatabaseUtil;
+
+/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class OmtpMessageReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "OmtpMessageReceiver";
+
+ private Context mContext;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mContext = context;
+ VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+ PhoneAccountHandle phone = sms.getPhoneAccountHandle();
+
+ if (phone == null) {
+ // This should never happen
+ VvmLog.i(TAG, "Received message for null phone account");
+ return;
+ }
+
+ if (!context.getSystemService(UserManager.class).isUserUnlocked()) {
+ VvmLog.i(TAG, "Received message on locked device");
+ // LegacyModeSmsHandler can handle new message notifications without storage access
+ LegacyModeSmsHandler.handle(context, sms);
+ // A full sync will happen after the device is unlocked, so nothing else need to be
+ // done.
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone);
+ if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) {
+ if (helper.isLegacyModeEnabled()) {
+ LegacyModeSmsHandler.handle(context, sms);
+ } else {
+ VvmLog.i(TAG, "Received vvm message for disabled vvm source.");
+ }
+ return;
+ }
+
+ String eventType = sms.getPrefix();
+ Bundle data = sms.getFields();
+
+ if (eventType == null || data == null) {
+ VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring");
+ return;
+ }
+
+ if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+ SyncMessage message = new SyncMessage(data);
+
+ VvmLog.v(TAG, "Received SYNC sms for " + phone +
+ " with event " + message.getSyncTriggerEvent());
+ processSync(phone, message);
+ } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+ VvmLog.v(TAG, "Received Status sms for " + phone);
+ // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject
+ // the follow request. Providing the data will also prevent ActivationTask from
+ // requesting another STATUS SMS. The following task will only run if the carrier
+ // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated.
+ ActivationTask.start(context, phone, data);
+ } else {
+ VvmLog.w(TAG, "Unknown prefix: " + eventType);
+ VisualVoicemailProtocol protocol = helper.getProtocol();
+ if (protocol == null) {
+ return;
+ }
+ Bundle statusData = helper.getProtocol()
+ .translateStatusSmsBundle(helper, eventType, data);
+ if (statusData != null) {
+ VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating");
+ ActivationTask.start(context, phone, data);
+ }
+ }
+ }
+
+ /**
+ * A sync message has two purposes: to signal a new voicemail message, and to indicate the
+ * voicemails on the server have changed remotely (usually through the TUI). Save the new
+ * message to the voicemail provider if it is the former case and perform a full sync in the
+ * latter case.
+ *
+ * @param message The sync message to extract data from.
+ */
+ private void processSync(PhoneAccountHandle phone, SyncMessage message) {
+ switch (message.getSyncTriggerEvent()) {
+ case OmtpConstants.NEW_MESSAGE:
+ if (!OmtpConstants.VOICE.equals(message.getContentType())) {
+ VvmLog.i(TAG, "Non-voice message of type '" + message.getContentType()
+ + "' received, ignoring");
+ return;
+ }
+
+ Voicemail.Builder builder = Voicemail.createForInsertion(
+ message.getTimestampMillis(), message.getSender())
+ .setPhoneAccount(phone)
+ .setSourceData(message.getId())
+ .setDuration(message.getLength())
+ .setSourcePackage(mContext.getPackageName());
+ Voicemail voicemail = builder.build();
+
+ VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+ if (queryHelper.isVoicemailUnique(voicemail)) {
+ Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail);
+ voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build();
+ SyncOneTask.start(mContext, phone, voicemail);
+ }
+ break;
+ case OmtpConstants.MAILBOX_UPDATE:
+ SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY);
+ break;
+ case OmtpConstants.GREETINGS_UPDATE:
+ // Not implemented in V1
+ break;
+ default:
+ VvmLog.e(TAG,
+ "Unrecognized sync trigger event: " + message.getSyncTriggerEvent());
+ break;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpMessageSender.java
new file mode 100644
index 000000000..2323e4bcf
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/OmtpMessageSender.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemailomtp.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailService;
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.TelephonyManagerStub;
+import com.android.voicemailomtp.VvmLog;
+import java.io.UnsupportedEncodingException;
+import java.util.Locale;
+
+/**
+ * Send client originated OMTP messages to the OMTP server.
+ * <p>
+ * Uses {@link PendingIntent} instead of a call back to notify when the message is
+ * sent. This is primarily to keep the implementation simple and reuse what the underlying
+ * {@link SmsManager} interface provides.
+ * <p>
+ * Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server.
+ */
+public abstract class OmtpMessageSender {
+ protected static final String TAG = "OmtpMessageSender";
+ protected final Context mContext;
+ protected final PhoneAccountHandle mPhoneAccountHandle;
+ protected final short mApplicationPort;
+ protected final String mDestinationNumber;
+
+
+ public OmtpMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber) {
+ mContext = context;
+ mPhoneAccountHandle = phoneAccountHandle;
+ mApplicationPort = applicationPort;
+ mDestinationNumber = destinationNumber;
+ }
+
+ /**
+ * Sends a request to the VVM server to activate VVM for the current subscriber.
+ *
+ * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
+ * successfully sent, or failed.
+ */
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {}
+
+ /**
+ * Sends a request to the VVM server to deactivate VVM for the current subscriber.
+ *
+ * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
+ * successfully sent, or failed.
+ */
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {}
+
+ /**
+ * Send a request to the VVM server to get account status of the current subscriber.
+ *
+ * @param sentIntent If not NULL this PendingIntent is broadcast when the message is
+ * successfully sent, or failed.
+ */
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {}
+
+ protected void sendSms(String text, PendingIntent sentIntent) {
+ VisualVoicemailService
+ .sendVisualVoicemailSms(mContext, mPhoneAccountHandle, mDestinationNumber,
+ mApplicationPort, text, sentIntent);
+ }
+
+ protected void appendField(StringBuilder sb, String field, Object value) {
+ sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value);
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java
new file mode 100644
index 000000000..aa8374781
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.voicemailomtp.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.text.TextUtils;
+import com.android.voicemailomtp.OmtpConstants;
+
+/**
+ * A implementation of the OmtpMessageSender using the standard OMTP sms protocol.
+ */
+public class OmtpStandardMessageSender extends OmtpMessageSender {
+ private final String mClientType;
+ private final String mProtocolVersion;
+ private final String mClientPrefix;
+
+ /**
+ * Creates a new instance of OmtpStandardMessageSender.
+ *
+ * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+ * Otherwise, a standard text SMS is sent.
+ * @param destinationNumber Destination number to be used.
+ * @param clientType The "ct" field to be set in the MO message. This is the value used by the
+ * VVM server to identify the client. Certain VVM servers require a specific agreed
+ * value for this field.
+ * @param protocolVersion OMTP protocol version.
+ * @param clientPrefix The client prefix requested to be used by the server in its MT messages.
+ */
+ public OmtpStandardMessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
+ short applicationPort,
+ String destinationNumber, String clientType, String protocolVersion,
+ String clientPrefix) {
+ super(context, phoneAccountHandle, applicationPort, destinationNumber);
+ mClientType = clientType;
+ mProtocolVersion = protocolVersion;
+ mClientPrefix = clientPrefix;
+ }
+
+ // Activate message:
+ // V1.1: Activate:pv=<value>;ct=<value>
+ // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+ // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+ @Override
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST);
+
+ appendProtocolVersionAndClientType(sb);
+ if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2) ||
+ TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+ appendApplicationPort(sb);
+ appendClientPrefix(sb);
+ }
+
+ sendSms(sb.toString(), sentIntent);
+ }
+
+ // Deactivate message:
+ // V1.1: Deactivate:pv=<value>;ct=<string>
+ // V1.2: Deactivate:pv=<value>;ct=<string>
+ // V1.3: Deactivate:pv=<value>;ct=<string>
+ @Override
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST);
+ appendProtocolVersionAndClientType(sb);
+
+ sendSms(sb.toString(), sentIntent);
+ }
+
+ // Status message:
+ // V1.1: STATUS
+ // V1.2: STATUS
+ // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix>
+ @Override
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+ StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST);
+
+ if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) {
+ appendProtocolVersionAndClientType(sb);
+ appendApplicationPort(sb);
+ appendClientPrefix(sb);
+ }
+
+ sendSms(sb.toString(), sentIntent);
+ }
+
+ private void appendProtocolVersionAndClientType(StringBuilder sb) {
+ sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR);
+ appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion);
+ sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+ appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType);
+ }
+
+ private void appendApplicationPort(StringBuilder sb) {
+ sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+ appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort);
+ }
+
+ private void appendClientPrefix(StringBuilder sb) {
+ sb.append(OmtpConstants.SMS_FIELD_SEPARATOR);
+ sb.append(mClientPrefix);
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/StatusMessage.java b/java/com/android/voicemailomtp/sms/StatusMessage.java
new file mode 100644
index 000000000..3dfd4973e
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/StatusMessage.java
@@ -0,0 +1,209 @@
+/*
+ * 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.voicemailomtp.sms;
+
+import android.os.Bundle;
+import com.android.voicemailomtp.NeededForTesting;
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.VisualVoicemailPreferences;
+import com.android.voicemailomtp.VvmLog;
+
+/**
+ * Structured data representation of OMTP STATUS message.
+ *
+ * The getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class StatusMessage {
+ // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
+ // to be useful for initial omtp source implementation.
+ // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
+
+ private final String mProvisioningStatus;
+ private final String mStatusReturnCode;
+ private final String mSubscriptionUrl;
+ private final String mServerAddress;
+ private final String mTuiAccessNumber;
+ private final String mClientSmsDestinationNumber;
+ private final String mImapPort;
+ private final String mImapUserName;
+ private final String mImapPassword;
+ private final String mSmtpPort;
+ private final String mSmtpUserName;
+ private final String mSmtpPassword;
+ private final String mTuiPasswordLength;
+
+ @Override
+ public String toString() {
+ return "StatusMessage [mProvisioningStatus=" + mProvisioningStatus
+ + ", mStatusReturnCode=" + mStatusReturnCode
+ + ", mSubscriptionUrl=" + mSubscriptionUrl
+ + ", mServerAddress=" + mServerAddress
+ + ", mTuiAccessNumber=" + mTuiAccessNumber
+ + ", mClientSmsDestinationNumber=" + mClientSmsDestinationNumber
+ + ", mImapPort=" + mImapPort
+ + ", mImapUserName=" + mImapUserName
+ + ", mImapPassword=" + VvmLog.pii(mImapPassword)
+ + ", mSmtpPort=" + mSmtpPort
+ + ", mSmtpUserName=" + mSmtpUserName
+ + ", mSmtpPassword=" + VvmLog.pii(mSmtpPassword)
+ + ", mTuiPasswordLength=" + mTuiPasswordLength + "]";
+ }
+
+ public StatusMessage(Bundle wrappedData) {
+ mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS));
+ mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE);
+ mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL);
+ mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS);
+ mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER);
+ mClientSmsDestinationNumber = getString(wrappedData,
+ OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
+ mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT);
+ mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME);
+ mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD);
+ mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT);
+ mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME);
+ mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD);
+ mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH);
+ }
+
+ private static String unquote(String string) {
+ if (string.length() < 2) {
+ return string;
+ }
+ if (string.startsWith("\"") && string.endsWith("\"")) {
+ return string.substring(1, string.length() - 1);
+ }
+ return string;
+ }
+
+ /**
+ * @return the subscriber's VVM provisioning status.
+ */
+ public String getProvisioningStatus() {
+ return mProvisioningStatus;
+ }
+
+ /**
+ * @return the return-code of the status SMS.
+ */
+ public String getReturnCode() {
+ return mStatusReturnCode;
+ }
+
+ /**
+ * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
+ * to the visual voicemail service.
+ */
+ @NeededForTesting
+ public String getSubscriptionUrl() {
+ return mSubscriptionUrl;
+ }
+
+ /**
+ * @return the voicemail server address. Either server IP address or fully qualified domain
+ * name.
+ */
+ public String getServerAddress() {
+ return mServerAddress;
+ }
+
+ /**
+ * @return the Telephony User Interface number to call to access voicemails directly from the
+ * IVR.
+ */
+ @NeededForTesting
+ public String getTuiAccessNumber() {
+ return mTuiAccessNumber;
+ }
+
+ /**
+ * @return the number to which client originated SMSes should be sent to.
+ */
+ @NeededForTesting
+ public String getClientSmsDestinationNumber() {
+ return mClientSmsDestinationNumber;
+ }
+
+ /**
+ * @return the IMAP server port to talk to.
+ */
+ public String getImapPort() {
+ return mImapPort;
+ }
+
+ /**
+ * @return the IMAP user name to be used for authentication.
+ */
+ public String getImapUserName() {
+ return mImapUserName;
+ }
+
+ /**
+ * @return the IMAP password to be used for authentication.
+ */
+ public String getImapPassword() {
+ return mImapPassword;
+ }
+
+ /**
+ * @return the SMTP server port to talk to.
+ */
+ @NeededForTesting
+ public String getSmtpPort() {
+ return mSmtpPort;
+ }
+
+ /**
+ * @return the SMTP user name to be used for SMTP authentication.
+ */
+ @NeededForTesting
+ public String getSmtpUserName() {
+ return mSmtpUserName;
+ }
+
+ /**
+ * @return the SMTP password to be used for SMTP authentication.
+ */
+ @NeededForTesting
+ public String getSmtpPassword() {
+ return mSmtpPassword;
+ }
+
+ public String getTuiPasswordLength() {
+ return mTuiPasswordLength;
+ }
+
+ private static String getString(Bundle bundle, String key) {
+ String value = bundle.getString(key);
+ if (value == null) {
+ return "";
+ }
+ return value;
+ }
+
+ /**
+ * Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved.
+ */
+ public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) {
+ return editor
+ .putString(OmtpConstants.IMAP_PORT, getImapPort())
+ .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress())
+ .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName())
+ .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword())
+ .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength());
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java b/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java
new file mode 100644
index 000000000..4e10c0e43
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java
@@ -0,0 +1,162 @@
+/*
+ * 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.voicemailomtp.sms;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.support.annotation.MainThread;
+import android.support.annotation.Nullable;
+import android.support.annotation.WorkerThread;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+import android.telephony.VisualVoicemailSms;
+import com.android.voicemailomtp.Assert;
+import com.android.voicemailomtp.OmtpConstants;
+import com.android.voicemailomtp.OmtpService;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.protocol.VisualVoicemailProtocol;
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/** Intercepts a incoming STATUS SMS with a blocking call. */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class StatusSmsFetcher extends BroadcastReceiver implements Closeable {
+
+ private static final String TAG = "VvmStatusSmsFetcher";
+
+ private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000;
+
+ private static final String ACTION_REQUEST_SENT_INTENT
+ = "com.android.voicemailomtp.sms.REQUEST_SENT";
+ private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0;
+
+ private CompletableFuture<Bundle> mFuture = new CompletableFuture<>();
+
+ private final Context mContext;
+ private final PhoneAccountHandle mPhoneAccountHandle;
+
+ public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) {
+ mContext = context;
+ mPhoneAccountHandle = phoneAccountHandle;
+ IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT);
+ filter.addAction(OmtpService.ACTION_SMS_RECEIVED);
+ context.registerReceiver(this, filter);
+ }
+
+ @Override
+ public void close() throws IOException {
+ mContext.unregisterReceiver(this);
+ }
+
+ @WorkerThread
+ @Nullable
+ public Bundle get() throws InterruptedException, ExecutionException, TimeoutException,
+ CancellationException {
+ Assert.isNotMainThread();
+ return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ }
+
+ public PendingIntent getSentIntent() {
+ Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT);
+ intent.setPackage(mContext.getPackageName());
+ // Because the receiver is registered dynamically, implicit intent must be used.
+ // There should only be a single status SMS request at a time.
+ return PendingIntent.getBroadcast(mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ @Override
+ @MainThread
+ public void onReceive(Context context, Intent intent) {
+ Assert.isMainThread();
+ if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) {
+ int resultCode = getResultCode();
+
+ if (resultCode == Activity.RESULT_OK) {
+ VvmLog.d(TAG, "Request SMS successfully sent");
+ return;
+ }
+
+ VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode));
+ mFuture.cancel(true);
+ return;
+ }
+
+ VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS);
+
+ if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) {
+ return;
+ }
+ String eventType = sms.getPrefix();
+
+ if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) {
+ mFuture.complete(sms.getFields());
+ return;
+ }
+
+ if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) {
+ return;
+ }
+
+ VvmLog.i(TAG, "VVM SMS with event " + eventType
+ + " received, attempting to translate to STATUS SMS");
+ OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context,
+ mPhoneAccountHandle);
+ VisualVoicemailProtocol protocol = helper.getProtocol();
+ if (protocol == null) {
+ return;
+ }
+ Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType,
+ sms.getFields());
+
+ if (translatedBundle != null) {
+ VvmLog.i(TAG, "Translated to STATUS SMS");
+ mFuture.complete(translatedBundle);
+ }
+ }
+
+ private static String sentSmsResultToString(int resultCode) {
+ switch (resultCode) {
+ case Activity.RESULT_OK:
+ return "OK";
+ case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ case SmsManager.RESULT_ERROR_NO_SERVICE:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ case SmsManager.RESULT_ERROR_NULL_PDU:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ case SmsManager.RESULT_ERROR_RADIO_OFF:
+ return "RESULT_ERROR_GENERIC_FAILURE";
+ default:
+ return "UNKNOWN CODE: " + resultCode;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/sms/SyncMessage.java b/java/com/android/voicemailomtp/sms/SyncMessage.java
new file mode 100644
index 000000000..89cfc0f19
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/SyncMessage.java
@@ -0,0 +1,166 @@
+/*
+ * 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.voicemailomtp.sms;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import com.android.voicemailomtp.NeededForTesting;
+import com.android.voicemailomtp.OmtpConstants;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * Structured data representation of an OMTP SYNC message.
+ *
+ * Getters will return null if the field was not set in the message body or it could not be parsed.
+ */
+public class SyncMessage {
+ // Sync event that triggered this message.
+ private final String mSyncTriggerEvent;
+ // Total number of new messages on the server.
+ private final int mNewMessageCount;
+ // UID of the new message.
+ private final String mMessageId;
+ // Length of the message.
+ private final int mMessageLength;
+ // Content type (voice, video, fax...) of the new message.
+ private final String mContentType;
+ // Sender of the new message.
+ private final String mSender;
+ // Timestamp (in millis) of the new message.
+ private final long mMsgTimeMillis;
+
+ @Override
+ public String toString() {
+ return "SyncMessage [mSyncTriggerEvent=" + mSyncTriggerEvent
+ + ", mNewMessageCount=" + mNewMessageCount
+ + ", mMessageId=" + mMessageId
+ + ", mMessageLength=" + mMessageLength
+ + ", mContentType=" + mContentType
+ + ", mSender=" + mSender
+ + ", mMsgTimeMillis=" + mMsgTimeMillis + "]";
+ }
+
+ public SyncMessage(Bundle wrappedData) {
+ mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT);
+ mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID);
+ mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH);
+ mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE);
+ mSender = getString(wrappedData, OmtpConstants.SENDER);
+ mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT);
+ mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME));
+ }
+
+ private static long parseTime(@Nullable String value) {
+ if (value == null) {
+ return 0L;
+ }
+ try {
+ return new SimpleDateFormat(
+ OmtpConstants.DATE_TIME_FORMAT, Locale.US)
+ .parse(value).getTime();
+ } catch (ParseException e) {
+ return 0L;
+ }
+ }
+ /**
+ * @return the event that triggered the sync message. This is a mandatory field and must always
+ * be set.
+ */
+ public String getSyncTriggerEvent() {
+ return mSyncTriggerEvent;
+ }
+
+ /**
+ * @return the number of new messages stored on the voicemail server.
+ */
+ @NeededForTesting
+ public int getNewMessageCount() {
+ return mNewMessageCount;
+ }
+
+ /**
+ * @return the message ID of the new message.
+ * <p>
+ * Expected to be set only for
+ * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
+ */
+ public String getId() {
+ return mMessageId;
+ }
+
+ /**
+ * @return the content type of the new message.
+ * <p>
+ * Expected to be set only for
+ * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
+ */
+ @NeededForTesting
+ public String getContentType() {
+ return mContentType;
+ }
+
+ /**
+ * @return the message length of the new message.
+ * <p>
+ * Expected to be set only for
+ * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
+ */
+ public int getLength() {
+ return mMessageLength;
+ }
+
+ /**
+ * @return the sender's phone number of the new message specified as MSISDN.
+ * <p>
+ * Expected to be set only for
+ * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
+ */
+ public String getSender() {
+ return mSender;
+ }
+
+ /**
+ * @return the timestamp as milliseconds for the new message.
+ * <p>
+ * Expected to be set only for
+ * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE}
+ */
+ public long getTimestampMillis() {
+ return mMsgTimeMillis;
+ }
+
+ private static int getInt(Bundle wrappedData, String key) {
+ String value = wrappedData.getString(key);
+ if (value == null) {
+ return 0;
+ }
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return 0;
+ }
+ }
+
+ private static String getString(Bundle wrappedData, String key) {
+ String value = wrappedData.getString(key);
+ if (value == null) {
+ return "";
+ }
+ return value;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java b/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java
new file mode 100644
index 000000000..02e465967
--- /dev/null
+++ b/java/com/android/voicemailomtp/sms/Vvm3MessageSender.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.voicemailomtp.sms;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.support.annotation.Nullable;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.SmsManager;
+
+public class Vvm3MessageSender extends OmtpMessageSender {
+
+ /**
+ * Creates a new instance of Vvm3MessageSender.
+ *
+ * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number.
+ * Otherwise, a standard text SMS is sent.
+ */
+ public Vvm3MessageSender(Context context, PhoneAccountHandle phoneAccountHandle,
+ short applicationPort, String destinationNumber) {
+ super(context, phoneAccountHandle, applicationPort, destinationNumber);
+ }
+
+ @Override
+ public void requestVvmActivation(@Nullable PendingIntent sentIntent) {
+ // Activation not supported for VVM3, send a status request instead.
+ requestVvmStatus(sentIntent);
+ }
+
+ @Override
+ public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {
+ // Deactivation not supported for VVM3, do nothing
+ }
+
+
+ @Override
+ public void requestVvmStatus(@Nullable PendingIntent sentIntent) {
+ // Status message:
+ // STATUS
+ StringBuilder sb = new StringBuilder().append("STATUS");
+ sendSms(sb.toString(), sentIntent);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java b/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java
new file mode 100644
index 000000000..b41450790
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java
@@ -0,0 +1,1202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.io;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayWriter;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * General IO stream manipulation utilities.
+ * <p>
+ * This class provides static utility methods for input/output operations.
+ * <ul>
+ * <li>closeQuietly - these methods close a stream ignoring nulls and exceptions
+ * <li>toXxx/read - these methods read data from a stream
+ * <li>write - these methods write data to a stream
+ * <li>copy - these methods copy all the data from one stream to another
+ * <li>contentEquals - these methods compare the content of two streams
+ * </ul>
+ * <p>
+ * The byte-to-char methods and char-to-byte methods involve a conversion step.
+ * Two methods are provided in each case, one that uses the platform default
+ * encoding and the other which allows you to specify an encoding. You are
+ * encouraged to always specify an encoding because relying on the platform
+ * default can lead to unexpected results, for example when moving from
+ * development to production.
+ * <p>
+ * All the methods in this class that read a stream are buffered internally.
+ * This means that there is no cause to use a <code>BufferedInputStream</code>
+ * or <code>BufferedReader</code>. The default buffer size of 4K has been shown
+ * to be efficient in tests.
+ * <p>
+ * Wherever possible, the methods in this class do <em>not</em> flush or close
+ * the stream. This is to avoid making non-portable assumptions about the
+ * streams' origin and further use. Thus the caller is still responsible for
+ * closing streams after use.
+ * <p>
+ * Origin of code: Excalibur.
+ *
+ * @author Peter Donald
+ * @author Jeff Turner
+ * @author Matthew Hawthorne
+ * @author Stephen Colebourne
+ * @author Gareth Davis
+ * @author Ian Springer
+ * @author Niall Pemberton
+ * @author Sandy McArthur
+ * @version $Id: IOUtils.java 481854 2006-12-03 18:30:07Z scolebourne $
+ */
+public class IOUtils {
+ // NOTE: This class is focussed on InputStream, OutputStream, Reader and
+ // Writer. Each method should take at least one of these as a parameter,
+ // or return one of them.
+
+ /**
+ * The Unix directory separator character.
+ */
+ public static final char DIR_SEPARATOR_UNIX = '/';
+ /**
+ * The Windows directory separator character.
+ */
+ public static final char DIR_SEPARATOR_WINDOWS = '\\';
+ /**
+ * The system directory separator character.
+ */
+ public static final char DIR_SEPARATOR = File.separatorChar;
+ /**
+ * The Unix line separator string.
+ */
+ public static final String LINE_SEPARATOR_UNIX = "\n";
+ /**
+ * The Windows line separator string.
+ */
+ public static final String LINE_SEPARATOR_WINDOWS = "\r\n";
+ /**
+ * The system line separator string.
+ */
+ public static final String LINE_SEPARATOR;
+ static {
+ // avoid security issues
+ StringWriter buf = new StringWriter(4);
+ PrintWriter out = new PrintWriter(buf);
+ out.println();
+ LINE_SEPARATOR = buf.toString();
+ }
+
+ /**
+ * The default buffer size to use.
+ */
+ private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+ /**
+ * Instances should NOT be constructed in standard programming.
+ */
+ public IOUtils() {
+ super();
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Unconditionally close an <code>Reader</code>.
+ * <p>
+ * Equivalent to {@link Reader#close()}, except any exceptions will be ignored.
+ * This is typically used in finally blocks.
+ *
+ * @param input the Reader to close, may be null or already closed
+ */
+ public static void closeQuietly(Reader input) {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ioe) {
+ // ignore
+ }
+ }
+
+ /**
+ * Unconditionally close a <code>Writer</code>.
+ * <p>
+ * Equivalent to {@link Writer#close()}, except any exceptions will be ignored.
+ * This is typically used in finally blocks.
+ *
+ * @param output the Writer to close, may be null or already closed
+ */
+ public static void closeQuietly(Writer output) {
+ try {
+ if (output != null) {
+ output.close();
+ }
+ } catch (IOException ioe) {
+ // ignore
+ }
+ }
+
+ /**
+ * Unconditionally close an <code>InputStream</code>.
+ * <p>
+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
+ * This is typically used in finally blocks.
+ *
+ * @param input the InputStream to close, may be null or already closed
+ */
+ public static void closeQuietly(InputStream input) {
+ try {
+ if (input != null) {
+ input.close();
+ }
+ } catch (IOException ioe) {
+ // ignore
+ }
+ }
+
+ /**
+ * Unconditionally close an <code>OutputStream</code>.
+ * <p>
+ * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored.
+ * This is typically used in finally blocks.
+ *
+ * @param output the OutputStream to close, may be null or already closed
+ */
+ public static void closeQuietly(OutputStream output) {
+ try {
+ if (output != null) {
+ output.close();
+ }
+ } catch (IOException ioe) {
+ // ignore
+ }
+ }
+
+ // read toByteArray
+ //-----------------------------------------------------------------------
+ /**
+ * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ /**
+ * Get the contents of a <code>Reader</code> as a <code>byte[]</code>
+ * using the default character encoding of the platform.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static byte[] toByteArray(Reader input) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output);
+ return output.toByteArray();
+ }
+
+ /**
+ * Get the contents of a <code>Reader</code> as a <code>byte[]</code>
+ * using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @param encoding the encoding to use, null means platform default
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static byte[] toByteArray(Reader input, String encoding)
+ throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ copy(input, output, encoding);
+ return output.toByteArray();
+ }
+
+ /**
+ * Get the contents of a <code>String</code> as a <code>byte[]</code>
+ * using the default character encoding of the platform.
+ * <p>
+ * This is the same as {@link String#getBytes()}.
+ *
+ * @param input the <code>String</code> to convert
+ * @return the requested byte array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs (never occurs)
+ * @deprecated Use {@link String#getBytes()}
+ */
+ @Deprecated
+ public static byte[] toByteArray(String input) throws IOException {
+ return input.getBytes();
+ }
+
+ // read char[]
+ //-----------------------------------------------------------------------
+ /**
+ * Get the contents of an <code>InputStream</code> as a character array
+ * using the default character encoding of the platform.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param is the <code>InputStream</code> to read from
+ * @return the requested character array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static char[] toCharArray(InputStream is) throws IOException {
+ CharArrayWriter output = new CharArrayWriter();
+ copy(is, output);
+ return output.toCharArray();
+ }
+
+ /**
+ * Get the contents of an <code>InputStream</code> as a character array
+ * using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param is the <code>InputStream</code> to read from
+ * @param encoding the encoding to use, null means platform default
+ * @return the requested character array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static char[] toCharArray(InputStream is, String encoding)
+ throws IOException {
+ CharArrayWriter output = new CharArrayWriter();
+ copy(is, output, encoding);
+ return output.toCharArray();
+ }
+
+ /**
+ * Get the contents of a <code>Reader</code> as a character array.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @return the requested character array
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static char[] toCharArray(Reader input) throws IOException {
+ CharArrayWriter sw = new CharArrayWriter();
+ copy(input, sw);
+ return sw.toCharArray();
+ }
+
+ // read toString
+ //-----------------------------------------------------------------------
+ /**
+ * Get the contents of an <code>InputStream</code> as a String
+ * using the default character encoding of the platform.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @return the requested String
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static String toString(InputStream input) throws IOException {
+ StringWriter sw = new StringWriter();
+ copy(input, sw);
+ return sw.toString();
+ }
+
+ /**
+ * Get the contents of an <code>InputStream</code> as a String
+ * using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @param encoding the encoding to use, null means platform default
+ * @return the requested String
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static String toString(InputStream input, String encoding)
+ throws IOException {
+ StringWriter sw = new StringWriter();
+ copy(input, sw, encoding);
+ return sw.toString();
+ }
+
+ /**
+ * Get the contents of a <code>Reader</code> as a String.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @return the requested String
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static String toString(Reader input) throws IOException {
+ StringWriter sw = new StringWriter();
+ copy(input, sw);
+ return sw.toString();
+ }
+
+ /**
+ * Get the contents of a <code>byte[]</code> as a String
+ * using the default character encoding of the platform.
+ *
+ * @param input the byte array to read from
+ * @return the requested String
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs (never occurs)
+ * @deprecated Use {@link String#String(byte[])}
+ */
+ @Deprecated
+ public static String toString(byte[] input) throws IOException {
+ return new String(input);
+ }
+
+ /**
+ * Get the contents of a <code>byte[]</code> as a String
+ * using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ *
+ * @param input the byte array to read from
+ * @param encoding the encoding to use, null means platform default
+ * @return the requested String
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs (never occurs)
+ * @deprecated Use {@link String#String(byte[],String)}
+ */
+ @Deprecated
+ public static String toString(byte[] input, String encoding)
+ throws IOException {
+ if (encoding == null) {
+ return new String(input);
+ } else {
+ return new String(input, encoding);
+ }
+ }
+
+ // readLines
+ //-----------------------------------------------------------------------
+ /**
+ * Get the contents of an <code>InputStream</code> as a list of Strings,
+ * one entry per line, using the default character encoding of the platform.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from, not null
+ * @return the list of Strings, never null
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static List<String> readLines(InputStream input) throws IOException {
+ InputStreamReader reader = new InputStreamReader(input);
+ return readLines(reader);
+ }
+
+ /**
+ * Get the contents of an <code>InputStream</code> as a list of Strings,
+ * one entry per line, using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from, not null
+ * @param encoding the encoding to use, null means platform default
+ * @return the list of Strings, never null
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static List<String> readLines(InputStream input, String encoding) throws IOException {
+ if (encoding == null) {
+ return readLines(input);
+ } else {
+ InputStreamReader reader = new InputStreamReader(input, encoding);
+ return readLines(reader);
+ }
+ }
+
+ /**
+ * Get the contents of a <code>Reader</code> as a list of Strings,
+ * one entry per line.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ *
+ * @param input the <code>Reader</code> to read from, not null
+ * @return the list of Strings, never null
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static List<String> readLines(Reader input) throws IOException {
+ BufferedReader reader = new BufferedReader(input);
+ List<String> list = new ArrayList<String>();
+ String line = reader.readLine();
+ while (line != null) {
+ list.add(line);
+ line = reader.readLine();
+ }
+ return list;
+ }
+
+ //-----------------------------------------------------------------------
+ /**
+ * Convert the specified string to an input stream, encoded as bytes
+ * using the default character encoding of the platform.
+ *
+ * @param input the string to convert
+ * @return an input stream
+ * @since Commons IO 1.1
+ */
+ public static InputStream toInputStream(String input) {
+ byte[] bytes = input.getBytes();
+ return new ByteArrayInputStream(bytes);
+ }
+
+ /**
+ * Convert the specified string to an input stream, encoded as bytes
+ * using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ *
+ * @param input the string to convert
+ * @param encoding the encoding to use, null means platform default
+ * @throws IOException if the encoding is invalid
+ * @return an input stream
+ * @since Commons IO 1.1
+ */
+ public static InputStream toInputStream(String input, String encoding) throws IOException {
+ byte[] bytes = encoding != null ? input.getBytes(encoding) : input.getBytes();
+ return new ByteArrayInputStream(bytes);
+ }
+
+ // write byte[]
+ //-----------------------------------------------------------------------
+ /**
+ * Writes bytes from a <code>byte[]</code> to an <code>OutputStream</code>.
+ *
+ * @param data the byte array to write, do not modify during output,
+ * null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(byte[] data, OutputStream output)
+ throws IOException {
+ if (data != null) {
+ output.write(data);
+ }
+ }
+
+ /**
+ * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
+ * using the default character encoding of the platform.
+ * <p>
+ * This method uses {@link String#String(byte[])}.
+ *
+ * @param data the byte array to write, do not modify during output,
+ * null ignored
+ * @param output the <code>Writer</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(byte[] data, Writer output) throws IOException {
+ if (data != null) {
+ output.write(new String(data));
+ }
+ }
+
+ /**
+ * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code>
+ * using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method uses {@link String#String(byte[], String)}.
+ *
+ * @param data the byte array to write, do not modify during output,
+ * null ignored
+ * @param output the <code>Writer</code> to write to
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(byte[] data, Writer output, String encoding)
+ throws IOException {
+ if (data != null) {
+ if (encoding == null) {
+ write(data, output);
+ } else {
+ output.write(new String(data, encoding));
+ }
+ }
+ }
+
+ // write char[]
+ //-----------------------------------------------------------------------
+ /**
+ * Writes chars from a <code>char[]</code> to a <code>Writer</code>
+ * using the default character encoding of the platform.
+ *
+ * @param data the char array to write, do not modify during output,
+ * null ignored
+ * @param output the <code>Writer</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(char[] data, Writer output) throws IOException {
+ if (data != null) {
+ output.write(data);
+ }
+ }
+
+ /**
+ * Writes chars from a <code>char[]</code> to bytes on an
+ * <code>OutputStream</code>.
+ * <p>
+ * This method uses {@link String#String(char[])} and
+ * {@link String#getBytes()}.
+ *
+ * @param data the char array to write, do not modify during output,
+ * null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(char[] data, OutputStream output)
+ throws IOException {
+ if (data != null) {
+ output.write(new String(data).getBytes());
+ }
+ }
+
+ /**
+ * Writes chars from a <code>char[]</code> to bytes on an
+ * <code>OutputStream</code> using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method uses {@link String#String(char[])} and
+ * {@link String#getBytes(String)}.
+ *
+ * @param data the char array to write, do not modify during output,
+ * null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(char[] data, OutputStream output, String encoding)
+ throws IOException {
+ if (data != null) {
+ if (encoding == null) {
+ write(data, output);
+ } else {
+ output.write(new String(data).getBytes(encoding));
+ }
+ }
+ }
+
+ // write String
+ //-----------------------------------------------------------------------
+ /**
+ * Writes chars from a <code>String</code> to a <code>Writer</code>.
+ *
+ * @param data the <code>String</code> to write, null ignored
+ * @param output the <code>Writer</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(String data, Writer output) throws IOException {
+ if (data != null) {
+ output.write(data);
+ }
+ }
+
+ /**
+ * Writes chars from a <code>String</code> to bytes on an
+ * <code>OutputStream</code> using the default character encoding of the
+ * platform.
+ * <p>
+ * This method uses {@link String#getBytes()}.
+ *
+ * @param data the <code>String</code> to write, null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(String data, OutputStream output)
+ throws IOException {
+ if (data != null) {
+ output.write(data.getBytes());
+ }
+ }
+
+ /**
+ * Writes chars from a <code>String</code> to bytes on an
+ * <code>OutputStream</code> using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method uses {@link String#getBytes(String)}.
+ *
+ * @param data the <code>String</code> to write, null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(String data, OutputStream output, String encoding)
+ throws IOException {
+ if (data != null) {
+ if (encoding == null) {
+ write(data, output);
+ } else {
+ output.write(data.getBytes(encoding));
+ }
+ }
+ }
+
+ // write StringBuffer
+ //-----------------------------------------------------------------------
+ /**
+ * Writes chars from a <code>StringBuffer</code> to a <code>Writer</code>.
+ *
+ * @param data the <code>StringBuffer</code> to write, null ignored
+ * @param output the <code>Writer</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(StringBuffer data, Writer output)
+ throws IOException {
+ if (data != null) {
+ output.write(data.toString());
+ }
+ }
+
+ /**
+ * Writes chars from a <code>StringBuffer</code> to bytes on an
+ * <code>OutputStream</code> using the default character encoding of the
+ * platform.
+ * <p>
+ * This method uses {@link String#getBytes()}.
+ *
+ * @param data the <code>StringBuffer</code> to write, null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(StringBuffer data, OutputStream output)
+ throws IOException {
+ if (data != null) {
+ output.write(data.toString().getBytes());
+ }
+ }
+
+ /**
+ * Writes chars from a <code>StringBuffer</code> to bytes on an
+ * <code>OutputStream</code> using the specified character encoding.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method uses {@link String#getBytes(String)}.
+ *
+ * @param data the <code>StringBuffer</code> to write, null ignored
+ * @param output the <code>OutputStream</code> to write to
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void write(StringBuffer data, OutputStream output,
+ String encoding) throws IOException {
+ if (data != null) {
+ if (encoding == null) {
+ write(data, output);
+ } else {
+ output.write(data.toString().getBytes(encoding));
+ }
+ }
+ }
+
+ // writeLines
+ //-----------------------------------------------------------------------
+ /**
+ * Writes the <code>toString()</code> value of each item in a collection to
+ * an <code>OutputStream</code> line by line, using the default character
+ * encoding of the platform and the specified line ending.
+ *
+ * @param lines the lines to write, null entries produce blank lines
+ * @param lineEnding the line separator to use, null is system default
+ * @param output the <code>OutputStream</code> to write to, not null, not closed
+ * @throws NullPointerException if the output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void writeLines(Collection<Object> lines, String lineEnding,
+ OutputStream output) throws IOException {
+ if (lines == null) {
+ return;
+ }
+ if (lineEnding == null) {
+ lineEnding = LINE_SEPARATOR;
+ }
+ for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) {
+ Object line = it.next();
+ if (line != null) {
+ output.write(line.toString().getBytes());
+ }
+ output.write(lineEnding.getBytes());
+ }
+ }
+
+ /**
+ * Writes the <code>toString()</code> value of each item in a collection to
+ * an <code>OutputStream</code> line by line, using the specified character
+ * encoding and the specified line ending.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ *
+ * @param lines the lines to write, null entries produce blank lines
+ * @param lineEnding the line separator to use, null is system default
+ * @param output the <code>OutputStream</code> to write to, not null, not closed
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if the output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void writeLines(Collection<Object> lines, String lineEnding,
+ OutputStream output, String encoding) throws IOException {
+ if (encoding == null) {
+ writeLines(lines, lineEnding, output);
+ } else {
+ if (lines == null) {
+ return;
+ }
+ if (lineEnding == null) {
+ lineEnding = LINE_SEPARATOR;
+ }
+ for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) {
+ Object line = it.next();
+ if (line != null) {
+ output.write(line.toString().getBytes(encoding));
+ }
+ output.write(lineEnding.getBytes(encoding));
+ }
+ }
+ }
+
+ /**
+ * Writes the <code>toString()</code> value of each item in a collection to
+ * a <code>Writer</code> line by line, using the specified line ending.
+ *
+ * @param lines the lines to write, null entries produce blank lines
+ * @param lineEnding the line separator to use, null is system default
+ * @param writer the <code>Writer</code> to write to, not null, not closed
+ * @throws NullPointerException if the input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void writeLines(Collection<Object> lines, String lineEnding,
+ Writer writer) throws IOException {
+ if (lines == null) {
+ return;
+ }
+ if (lineEnding == null) {
+ lineEnding = LINE_SEPARATOR;
+ }
+ for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) {
+ Object line = it.next();
+ if (line != null) {
+ writer.write(line.toString());
+ }
+ writer.write(lineEnding);
+ }
+ }
+
+ // copy from InputStream
+ //-----------------------------------------------------------------------
+ /**
+ * Copy bytes from an <code>InputStream</code> to an
+ * <code>OutputStream</code>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ * <p>
+ * Large streams (over 2GB) will return a bytes copied value of
+ * <code>-1</code> after the copy has completed since the correct
+ * number of bytes cannot be returned as an int. For large streams
+ * use the <code>copyLarge(InputStream, OutputStream)</code> method.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @param output the <code>OutputStream</code> to write to
+ * @return the number of bytes copied
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @throws ArithmeticException if the byte count is too large
+ * @since Commons IO 1.1
+ */
+ public static int copy(InputStream input, OutputStream output) throws IOException {
+ long count = copyLarge(input, output);
+ if (count > Integer.MAX_VALUE) {
+ return -1;
+ }
+ return (int) count;
+ }
+
+ /**
+ * Copy bytes from a large (over 2GB) <code>InputStream</code> to an
+ * <code>OutputStream</code>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @param output the <code>OutputStream</code> to write to
+ * @return the number of bytes copied
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.3
+ */
+ public static long copyLarge(InputStream input, OutputStream output)
+ throws IOException {
+ byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ long count = 0;
+ int n = 0;
+ while (-1 != (n = input.read(buffer))) {
+ output.write(buffer, 0, n);
+ count += n;
+ }
+ return count;
+ }
+
+ /**
+ * Copy bytes from an <code>InputStream</code> to chars on a
+ * <code>Writer</code> using the default character encoding of the platform.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ * <p>
+ * This method uses {@link InputStreamReader}.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @param output the <code>Writer</code> to write to
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void copy(InputStream input, Writer output)
+ throws IOException {
+ InputStreamReader in = new InputStreamReader(input);
+ copy(in, output);
+ }
+
+ /**
+ * Copy bytes from an <code>InputStream</code> to chars on a
+ * <code>Writer</code> using the specified character encoding.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedInputStream</code>.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * This method uses {@link InputStreamReader}.
+ *
+ * @param input the <code>InputStream</code> to read from
+ * @param output the <code>Writer</code> to write to
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void copy(InputStream input, Writer output, String encoding)
+ throws IOException {
+ if (encoding == null) {
+ copy(input, output);
+ } else {
+ InputStreamReader in = new InputStreamReader(input, encoding);
+ copy(in, output);
+ }
+ }
+
+ // copy from Reader
+ //-----------------------------------------------------------------------
+ /**
+ * Copy chars from a <code>Reader</code> to a <code>Writer</code>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ * <p>
+ * Large streams (over 2GB) will return a chars copied value of
+ * <code>-1</code> after the copy has completed since the correct
+ * number of chars cannot be returned as an int. For large streams
+ * use the <code>copyLarge(Reader, Writer)</code> method.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @param output the <code>Writer</code> to write to
+ * @return the number of characters copied
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @throws ArithmeticException if the character count is too large
+ * @since Commons IO 1.1
+ */
+ public static int copy(Reader input, Writer output) throws IOException {
+ long count = copyLarge(input, output);
+ if (count > Integer.MAX_VALUE) {
+ return -1;
+ }
+ return (int) count;
+ }
+
+ /**
+ * Copy chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @param output the <code>Writer</code> to write to
+ * @return the number of characters copied
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.3
+ */
+ public static long copyLarge(Reader input, Writer output) throws IOException {
+ char[] buffer = new char[DEFAULT_BUFFER_SIZE];
+ long count = 0;
+ int n = 0;
+ while (-1 != (n = input.read(buffer))) {
+ output.write(buffer, 0, n);
+ count += n;
+ }
+ return count;
+ }
+
+ /**
+ * Copy chars from a <code>Reader</code> to bytes on an
+ * <code>OutputStream</code> using the default character encoding of the
+ * platform, and calling flush.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ * <p>
+ * Due to the implementation of OutputStreamWriter, this method performs a
+ * flush.
+ * <p>
+ * This method uses {@link OutputStreamWriter}.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @param output the <code>OutputStream</code> to write to
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void copy(Reader input, OutputStream output)
+ throws IOException {
+ OutputStreamWriter out = new OutputStreamWriter(output);
+ copy(input, out);
+ // XXX Unless anyone is planning on rewriting OutputStreamWriter, we
+ // have to flush here.
+ out.flush();
+ }
+
+ /**
+ * Copy chars from a <code>Reader</code> to bytes on an
+ * <code>OutputStream</code> using the specified character encoding, and
+ * calling flush.
+ * <p>
+ * This method buffers the input internally, so there is no need to use a
+ * <code>BufferedReader</code>.
+ * <p>
+ * Character encoding names can be found at
+ * <a href="http://www.iana.org/assignments/character-sets">IANA</a>.
+ * <p>
+ * Due to the implementation of OutputStreamWriter, this method performs a
+ * flush.
+ * <p>
+ * This method uses {@link OutputStreamWriter}.
+ *
+ * @param input the <code>Reader</code> to read from
+ * @param output the <code>OutputStream</code> to write to
+ * @param encoding the encoding to use, null means platform default
+ * @throws NullPointerException if the input or output is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static void copy(Reader input, OutputStream output, String encoding)
+ throws IOException {
+ if (encoding == null) {
+ copy(input, output);
+ } else {
+ OutputStreamWriter out = new OutputStreamWriter(output, encoding);
+ copy(input, out);
+ // XXX Unless anyone is planning on rewriting OutputStreamWriter,
+ // we have to flush here.
+ out.flush();
+ }
+ }
+
+ // content equals
+ //-----------------------------------------------------------------------
+ /**
+ * Compare the contents of two Streams to determine if they are equal or
+ * not.
+ * <p>
+ * This method buffers the input internally using
+ * <code>BufferedInputStream</code> if they are not already buffered.
+ *
+ * @param input1 the first stream
+ * @param input2 the second stream
+ * @return true if the content of the streams are equal or they both don't
+ * exist, false otherwise
+ * @throws NullPointerException if either input is null
+ * @throws IOException if an I/O error occurs
+ */
+ public static boolean contentEquals(InputStream input1, InputStream input2)
+ throws IOException {
+ if (!(input1 instanceof BufferedInputStream)) {
+ input1 = new BufferedInputStream(input1);
+ }
+ if (!(input2 instanceof BufferedInputStream)) {
+ input2 = new BufferedInputStream(input2);
+ }
+
+ int ch = input1.read();
+ while (-1 != ch) {
+ int ch2 = input2.read();
+ if (ch != ch2) {
+ return false;
+ }
+ ch = input1.read();
+ }
+
+ int ch2 = input2.read();
+ return (ch2 == -1);
+ }
+
+ /**
+ * Compare the contents of two Readers to determine if they are equal or
+ * not.
+ * <p>
+ * This method buffers the input internally using
+ * <code>BufferedReader</code> if they are not already buffered.
+ *
+ * @param input1 the first reader
+ * @param input2 the second reader
+ * @return true if the content of the readers are equal or they both don't
+ * exist, false otherwise
+ * @throws NullPointerException if either input is null
+ * @throws IOException if an I/O error occurs
+ * @since Commons IO 1.1
+ */
+ public static boolean contentEquals(Reader input1, Reader input2)
+ throws IOException {
+ if (!(input1 instanceof BufferedReader)) {
+ input1 = new BufferedReader(input1);
+ }
+ if (!(input2 instanceof BufferedReader)) {
+ input2 = new BufferedReader(input2);
+ }
+
+ int ch = input1.read();
+ while (-1 != ch) {
+ int ch2 = input2.read();
+ if (ch != ch2) {
+ return false;
+ }
+ ch = input1.read();
+ }
+
+ int ch2 = input2.read();
+ return (ch2 == -1);
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java
new file mode 100644
index 000000000..867c43d86
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java
@@ -0,0 +1,392 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Encapsulates the values of the MIME-specific header fields
+ * (which starts with <code>Content-</code>).
+ *
+ *
+ * @version $Id: BodyDescriptor.java,v 1.4 2005/02/11 10:08:37 ntherning Exp $
+ */
+public class BodyDescriptor {
+ private static Log log = LogFactory.getLog(BodyDescriptor.class);
+
+ private String mimeType = "text/plain";
+ private String boundary = null;
+ private String charset = "us-ascii";
+ private String transferEncoding = "7bit";
+ private Map<String, String> parameters = new HashMap<String, String>();
+ private boolean contentTypeSet = false;
+ private boolean contentTransferEncSet = false;
+
+ /**
+ * Creates a new root <code>BodyDescriptor</code> instance.
+ */
+ public BodyDescriptor() {
+ this(null);
+ }
+
+ /**
+ * Creates a new <code>BodyDescriptor</code> instance.
+ *
+ * @param parent the descriptor of the parent or <code>null</code> if this
+ * is the root descriptor.
+ */
+ public BodyDescriptor(BodyDescriptor parent) {
+ if (parent != null && parent.isMimeType("multipart/digest")) {
+ mimeType = "message/rfc822";
+ } else {
+ mimeType = "text/plain";
+ }
+ }
+
+ /**
+ * Should be called for each <code>Content-</code> header field of
+ * a MIME message or part.
+ *
+ * @param name the field name.
+ * @param value the field value.
+ */
+ public void addField(String name, String value) {
+
+ name = name.trim().toLowerCase();
+
+ if (name.equals("content-transfer-encoding") && !contentTransferEncSet) {
+ contentTransferEncSet = true;
+
+ value = value.trim().toLowerCase();
+ if (value.length() > 0) {
+ transferEncoding = value;
+ }
+
+ } else if (name.equals("content-type") && !contentTypeSet) {
+ contentTypeSet = true;
+
+ value = value.trim();
+
+ /*
+ * Unfold Content-Type value
+ */
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (c == '\r' || c == '\n') {
+ continue;
+ }
+ sb.append(c);
+ }
+
+ Map<String, String> params = getHeaderParams(sb.toString());
+
+ String main = params.get("");
+ if (main != null) {
+ main = main.toLowerCase().trim();
+ int index = main.indexOf('/');
+ boolean valid = false;
+ if (index != -1) {
+ String type = main.substring(0, index).trim();
+ String subtype = main.substring(index + 1).trim();
+ if (type.length() > 0 && subtype.length() > 0) {
+ main = type + "/" + subtype;
+ valid = true;
+ }
+ }
+
+ if (!valid) {
+ main = null;
+ }
+ }
+ String b = params.get("boundary");
+
+ if (main != null
+ && ((main.startsWith("multipart/") && b != null)
+ || !main.startsWith("multipart/"))) {
+
+ mimeType = main;
+ }
+
+ if (isMultipart()) {
+ boundary = b;
+ }
+
+ String c = params.get("charset");
+ if (c != null) {
+ c = c.trim();
+ if (c.length() > 0) {
+ charset = c.toLowerCase();
+ }
+ }
+
+ /*
+ * Add all other parameters to parameters.
+ */
+ parameters.putAll(params);
+ parameters.remove("");
+ parameters.remove("boundary");
+ parameters.remove("charset");
+ }
+ }
+
+ private Map<String, String> getHeaderParams(String headerValue) {
+ Map<String, String> result = new HashMap<String, String>();
+
+ // split main value and parameters
+ String main;
+ String rest;
+ if (headerValue.indexOf(";") == -1) {
+ main = headerValue;
+ rest = null;
+ } else {
+ main = headerValue.substring(0, headerValue.indexOf(";"));
+ rest = headerValue.substring(main.length() + 1);
+ }
+
+ result.put("", main);
+ if (rest != null) {
+ char[] chars = rest.toCharArray();
+ StringBuffer paramName = new StringBuffer();
+ StringBuffer paramValue = new StringBuffer();
+
+ final byte READY_FOR_NAME = 0;
+ final byte IN_NAME = 1;
+ final byte READY_FOR_VALUE = 2;
+ final byte IN_VALUE = 3;
+ final byte IN_QUOTED_VALUE = 4;
+ final byte VALUE_DONE = 5;
+ final byte ERROR = 99;
+
+ byte state = READY_FOR_NAME;
+ boolean escaped = false;
+ for (int i = 0; i < chars.length; i++) {
+ char c = chars[i];
+
+ switch (state) {
+ case ERROR:
+ if (c == ';')
+ state = READY_FOR_NAME;
+ break;
+
+ case READY_FOR_NAME:
+ if (c == '=') {
+ log.error("Expected header param name, got '='");
+ state = ERROR;
+ break;
+ }
+
+ paramName = new StringBuffer();
+ paramValue = new StringBuffer();
+
+ state = IN_NAME;
+ // $FALL-THROUGH$
+
+ case IN_NAME:
+ if (c == '=') {
+ if (paramName.length() == 0)
+ state = ERROR;
+ else
+ state = READY_FOR_VALUE;
+ break;
+ }
+
+ // not '='... just add to name
+ paramName.append(c);
+ break;
+
+ case READY_FOR_VALUE:
+ boolean fallThrough = false;
+ switch (c) {
+ case ' ':
+ case '\t':
+ break; // ignore spaces, especially before '"'
+
+ case '"':
+ state = IN_QUOTED_VALUE;
+ break;
+
+ default:
+ state = IN_VALUE;
+ fallThrough = true;
+ break;
+ }
+ if (!fallThrough)
+ break;
+
+ // $FALL-THROUGH$
+
+ case IN_VALUE:
+ fallThrough = false;
+ switch (c) {
+ case ';':
+ case ' ':
+ case '\t':
+ result.put(
+ paramName.toString().trim().toLowerCase(),
+ paramValue.toString().trim());
+ state = VALUE_DONE;
+ fallThrough = true;
+ break;
+ default:
+ paramValue.append(c);
+ break;
+ }
+ if (!fallThrough)
+ break;
+
+ // $FALL-THROUGH$
+
+ case VALUE_DONE:
+ switch (c) {
+ case ';':
+ state = READY_FOR_NAME;
+ break;
+
+ case ' ':
+ case '\t':
+ break;
+
+ default:
+ state = ERROR;
+ break;
+ }
+ break;
+
+ case IN_QUOTED_VALUE:
+ switch (c) {
+ case '"':
+ if (!escaped) {
+ // don't trim quoted strings; the spaces could be intentional.
+ result.put(
+ paramName.toString().trim().toLowerCase(),
+ paramValue.toString());
+ state = VALUE_DONE;
+ } else {
+ escaped = false;
+ paramValue.append(c);
+ }
+ break;
+
+ case '\\':
+ if (escaped) {
+ paramValue.append('\\');
+ }
+ escaped = !escaped;
+ break;
+
+ default:
+ if (escaped) {
+ paramValue.append('\\');
+ }
+ escaped = false;
+ paramValue.append(c);
+ break;
+ }
+ break;
+
+ }
+ }
+
+ // done looping. check if anything is left over.
+ if (state == IN_VALUE) {
+ result.put(
+ paramName.toString().trim().toLowerCase(),
+ paramValue.toString().trim());
+ }
+ }
+
+ return result;
+ }
+
+
+ public boolean isMimeType(String mimeType) {
+ return this.mimeType.equals(mimeType.toLowerCase());
+ }
+
+ /**
+ * Return true if the BodyDescriptor belongs to a message
+ */
+ public boolean isMessage() {
+ return mimeType.equals("message/rfc822");
+ }
+
+ /**
+ * Return true if the BodyDescripotro belongs to a multipart
+ */
+ public boolean isMultipart() {
+ return mimeType.startsWith("multipart/");
+ }
+
+ /**
+ * Return the MimeType
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * Return the boundary
+ */
+ public String getBoundary() {
+ return boundary;
+ }
+
+ /**
+ * Return the charset
+ */
+ public String getCharset() {
+ return charset;
+ }
+
+ /**
+ * Return all parameters for the BodyDescriptor
+ */
+ public Map<String, String> getParameters() {
+ return parameters;
+ }
+
+ /**
+ * Return the TransferEncoding
+ */
+ public String getTransferEncoding() {
+ return transferEncoding;
+ }
+
+ /**
+ * Return true if it's base64 encoded
+ */
+ public boolean isBase64Encoded() {
+ return "base64".equals(transferEncoding);
+ }
+
+ /**
+ * Return true if it's quoted-printable
+ */
+ public boolean isQuotedPrintableEncoded() {
+ return "quoted-printable".equals(transferEncoding);
+ }
+
+ @Override
+ public String toString() {
+ return mimeType;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java
new file mode 100644
index 000000000..d9f3b078a
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java
@@ -0,0 +1,129 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import java.io.InputStream;
+import java.io.IOException;
+
+/**
+ * InputStream that shields its underlying input stream from
+ * being closed.
+ *
+ *
+ * @version $Id: CloseShieldInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $
+ */
+public class CloseShieldInputStream extends InputStream {
+
+ /**
+ * Underlying InputStream
+ */
+ private InputStream is;
+
+ public CloseShieldInputStream(InputStream is) {
+ this.is = is;
+ }
+
+ public InputStream getUnderlyingStream() {
+ return is;
+ }
+
+ /**
+ * @see java.io.InputStream#read()
+ */
+ public int read() throws IOException {
+ checkIfClosed();
+ return is.read();
+ }
+
+ /**
+ * @see java.io.InputStream#available()
+ */
+ public int available() throws IOException {
+ checkIfClosed();
+ return is.available();
+ }
+
+
+ /**
+ * Set the underlying InputStream to null
+ */
+ public void close() throws IOException {
+ is = null;
+ }
+
+ /**
+ * @see java.io.FilterInputStream#reset()
+ */
+ public synchronized void reset() throws IOException {
+ checkIfClosed();
+ is.reset();
+ }
+
+ /**
+ * @see java.io.FilterInputStream#markSupported()
+ */
+ public boolean markSupported() {
+ if (is == null)
+ return false;
+ return is.markSupported();
+ }
+
+ /**
+ * @see java.io.FilterInputStream#mark(int)
+ */
+ public synchronized void mark(int readlimit) {
+ if (is != null)
+ is.mark(readlimit);
+ }
+
+ /**
+ * @see java.io.FilterInputStream#skip(long)
+ */
+ public long skip(long n) throws IOException {
+ checkIfClosed();
+ return is.skip(n);
+ }
+
+ /**
+ * @see java.io.FilterInputStream#read(byte[])
+ */
+ public int read(byte b[]) throws IOException {
+ checkIfClosed();
+ return is.read(b);
+ }
+
+ /**
+ * @see java.io.FilterInputStream#read(byte[], int, int)
+ */
+ public int read(byte b[], int off, int len) throws IOException {
+ checkIfClosed();
+ return is.read(b, off, len);
+ }
+
+ /**
+ * Check if the underlying InputStream is null. If so throw an Exception
+ *
+ * @throws IOException if the underlying InputStream is null
+ */
+ private void checkIfClosed() throws IOException {
+ if (is == null)
+ throw new IOException("Stream is closed");
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java
new file mode 100644
index 000000000..b437e739e
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java
@@ -0,0 +1,177 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * <p>
+ * Receives notifications of the content of a plain RFC822 or MIME message.
+ * Implement this interface and register an instance of that implementation
+ * with a <code>MimeStreamParser</code> instance using its
+ * {@link org.apache.james.mime4j.MimeStreamParser#setContentHandler(ContentHandler)}
+ * method. The parser uses the <code>ContentHandler</code> instance to report
+ * basic message-related events like the start and end of the body of a
+ * part in a multipart MIME entity.
+ * </p>
+ * <p>
+ * Events will be generated in the order the corresponding elements occur in
+ * the message stream parsed by the parser. E.g.:
+ * <pre>
+ * startMessage()
+ * startHeader()
+ * field(...)
+ * field(...)
+ * ...
+ * endHeader()
+ * startMultipart()
+ * preamble(...)
+ * startBodyPart()
+ * startHeader()
+ * field(...)
+ * field(...)
+ * ...
+ * endHeader()
+ * body()
+ * endBodyPart()
+ * startBodyPart()
+ * startHeader()
+ * field(...)
+ * field(...)
+ * ...
+ * endHeader()
+ * body()
+ * endBodyPart()
+ * epilogue(...)
+ * endMultipart()
+ * endMessage()
+ * </pre>
+ * The above shows an example of a MIME message consisting of a multipart
+ * body containing two body parts.
+ * </p>
+ * <p>
+ * See MIME RFCs 2045-2049 for more information on the structure of MIME
+ * messages and RFC 822 and 2822 for the general structure of Internet mail
+ * messages.
+ * </p>
+ *
+ *
+ * @version $Id: ContentHandler.java,v 1.3 2004/10/02 12:41:10 ntherning Exp $
+ */
+public interface ContentHandler {
+ /**
+ * Called when a new message starts (a top level message or an embedded
+ * rfc822 message).
+ */
+ void startMessage();
+
+ /**
+ * Called when a message ends.
+ */
+ void endMessage();
+
+ /**
+ * Called when a new body part starts inside a
+ * <code>multipart/*</code> entity.
+ */
+ void startBodyPart();
+
+ /**
+ * Called when a body part ends.
+ */
+ void endBodyPart();
+
+ /**
+ * Called when a header (of a message or body part) is about to be parsed.
+ */
+ void startHeader();
+
+ /**
+ * Called for each field of a header.
+ *
+ * @param fieldData the raw contents of the field
+ * (<code>Field-Name: field value</code>). The value will not be
+ * unfolded.
+ */
+ void field(String fieldData);
+
+ /**
+ * Called when there are no more header fields in a message or body part.
+ */
+ void endHeader();
+
+ /**
+ * Called for the preamble (whatever comes before the first body part)
+ * of a <code>multipart/*</code> entity.
+ *
+ * @param is used to get the contents of the preamble.
+ * @throws IOException should be thrown on I/O errors.
+ */
+ void preamble(InputStream is) throws IOException;
+
+ /**
+ * Called for the epilogue (whatever comes after the final body part)
+ * of a <code>multipart/*</code> entity.
+ *
+ * @param is used to get the contents of the epilogue.
+ * @throws IOException should be thrown on I/O errors.
+ */
+ void epilogue(InputStream is) throws IOException;
+
+ /**
+ * Called when the body of a multipart entity is about to be parsed.
+ *
+ * @param bd encapsulates the values (either read from the
+ * message stream or, if not present, determined implictly
+ * as described in the
+ * MIME rfc:s) of the <code>Content-Type</code> and
+ * <code>Content-Transfer-Encoding</code> header fields.
+ */
+ void startMultipart(BodyDescriptor bd);
+
+ /**
+ * Called when the body of an entity has been parsed.
+ */
+ void endMultipart();
+
+ /**
+ * Called when the body of a discrete (non-multipart) entity is about to
+ * be parsed.
+ *
+ * @param bd see {@link #startMultipart(BodyDescriptor)}
+ * @param is the contents of the body. NOTE: this is the raw body contents
+ * - it will not be decoded if encoded. The <code>bd</code>
+ * parameter should be used to determine how the stream data
+ * should be decoded.
+ * @throws IOException should be thrown on I/O errors.
+ */
+ void body(BodyDescriptor bd, InputStream is) throws IOException;
+
+ /**
+ * Called when a new entity (message or body part) starts and the
+ * parser is in <code>raw</code> mode.
+ *
+ * @param is the raw contents of the entity.
+ * @throws IOException should be thrown on I/O errors.
+ * @see MimeStreamParser#setRaw(boolean)
+ */
+ void raw(InputStream is) throws IOException;
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java
new file mode 100644
index 000000000..d6ef706b2
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java
@@ -0,0 +1,139 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+
+/**
+ * InputStream which converts <code>\r</code>
+ * bytes not followed by <code>\n</code> and <code>\n</code> not
+ * preceded by <code>\r</code> to <code>\r\n</code>.
+ *
+ *
+ * @version $Id: EOLConvertingInputStream.java,v 1.4 2004/11/29 13:15:42 ntherning Exp $
+ */
+public class EOLConvertingInputStream extends InputStream {
+ /** Converts single '\r' to '\r\n' */
+ public static final int CONVERT_CR = 1;
+ /** Converts single '\n' to '\r\n' */
+ public static final int CONVERT_LF = 2;
+ /** Converts single '\r' and '\n' to '\r\n' */
+ public static final int CONVERT_BOTH = 3;
+
+ private PushbackInputStream in = null;
+ private int previous = 0;
+ private int flags = CONVERT_BOTH;
+ private int size = 0;
+ private int pos = 0;
+ private int nextTenPctPos;
+ private int tenPctSize;
+ private Callback callback;
+
+ public interface Callback {
+ public void report(int bytesRead);
+ }
+
+ /**
+ * Creates a new <code>EOLConvertingInputStream</code>
+ * instance converting bytes in the given <code>InputStream</code>.
+ * The flag <code>CONVERT_BOTH</code> is the default.
+ *
+ * @param in the <code>InputStream</code> to read from.
+ */
+ public EOLConvertingInputStream(InputStream _in) {
+ super();
+ in = new PushbackInputStream(_in, 2);
+ }
+
+ /**
+ * Creates a new <code>EOLConvertingInputStream</code>
+ * instance converting bytes in the given <code>InputStream</code>.
+ *
+ * @param _in the <code>InputStream</code> to read from.
+ * @param _size the size of the input stream (need not be exact)
+ * @param _callback a callback reporting when each 10% of stream's size is reached
+ */
+ public EOLConvertingInputStream(InputStream _in, int _size, Callback _callback) {
+ this(_in);
+ size = _size;
+ tenPctSize = size / 10;
+ nextTenPctPos = tenPctSize;
+ callback = _callback;
+ }
+
+ /**
+ * Closes the underlying stream.
+ *
+ * @throws IOException on I/O errors.
+ */
+ public void close() throws IOException {
+ in.close();
+ }
+
+ private int readByte() throws IOException {
+ int b = in.read();
+ if (b != -1) {
+ if (callback != null && pos++ == nextTenPctPos) {
+ nextTenPctPos += tenPctSize;
+ if (callback != null) {
+ callback.report(pos);
+ }
+ }
+ }
+ return b;
+ }
+
+ private void unreadByte(int c) throws IOException {
+ in.unread(c);
+ pos--;
+ }
+
+ /**
+ * @see java.io.InputStream#read()
+ */
+ public int read() throws IOException {
+ int b = readByte();
+
+ if (b == -1) {
+ pos = size;
+ return -1;
+ }
+
+ if ((flags & CONVERT_CR) != 0 && b == '\r') {
+ int c = readByte();
+ if (c != -1) {
+ unreadByte(c);
+ }
+ if (c != '\n') {
+ unreadByte('\n');
+ }
+ } else if ((flags & CONVERT_LF) != 0 && b == '\n' && previous != '\r') {
+ b = '\r';
+ unreadByte('\n');
+ }
+
+ previous = b;
+
+ return b;
+ }
+
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java
new file mode 100644
index 000000000..5eeead5f3
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.apache.james.mime4j;
+
+/**
+ * Empty stub for the apache logging library.
+ */
+public class Log {
+ private static final String LOG_TAG = "Email Log";
+
+ public Log(Class mClazz) {
+ }
+
+ public boolean isDebugEnabled() {
+ return false;
+ }
+
+ public boolean isErrorEnabled() {
+ return true;
+ }
+
+ public boolean isFatalEnabled() {
+ return true;
+ }
+
+ public boolean isInfoEnabled() {
+ return false;
+ }
+
+ public boolean isTraceEnabled() {
+ return false;
+ }
+
+ public boolean isWarnEnabled() {
+ return true;
+ }
+
+ public void trace(Object message) {
+ if (!isTraceEnabled()) return;
+ android.util.Log.v(LOG_TAG, toString(message, null));
+ }
+
+ public void trace(Object message, Throwable t) {
+ if (!isTraceEnabled()) return;
+ android.util.Log.v(LOG_TAG, toString(message, t));
+ }
+
+ public void debug(Object message) {
+ if (!isDebugEnabled()) return;
+ android.util.Log.d(LOG_TAG, toString(message, null));
+ }
+
+ public void debug(Object message, Throwable t) {
+ if (!isDebugEnabled()) return;
+ android.util.Log.d(LOG_TAG, toString(message, t));
+ }
+
+ public void info(Object message) {
+ if (!isInfoEnabled()) return;
+ android.util.Log.i(LOG_TAG, toString(message, null));
+ }
+
+ public void info(Object message, Throwable t) {
+ if (!isInfoEnabled()) return;
+ android.util.Log.i(LOG_TAG, toString(message, t));
+ }
+
+ public void warn(Object message) {
+ android.util.Log.w(LOG_TAG, toString(message, null));
+ }
+
+ public void warn(Object message, Throwable t) {
+ android.util.Log.w(LOG_TAG, toString(message, t));
+ }
+
+ public void error(Object message) {
+ android.util.Log.e(LOG_TAG, toString(message, null));
+ }
+
+ public void error(Object message, Throwable t) {
+ android.util.Log.e(LOG_TAG, toString(message, t));
+ }
+
+ public void fatal(Object message) {
+ android.util.Log.e(LOG_TAG, toString(message, null));
+ }
+
+ public void fatal(Object message, Throwable t) {
+ android.util.Log.e(LOG_TAG, toString(message, t));
+ }
+
+ private static String toString(Object o, Throwable t) {
+ String m = (o == null) ? "(null)" : o.toString();
+ if (t == null) {
+ return m;
+ } else {
+ return m + " " + t.getMessage();
+ }
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java
new file mode 100644
index 000000000..ed6e3de3d
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java
@@ -0,0 +1,29 @@
+/*
+ * 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 org.apache.james.mime4j;
+
+/**
+ * Empty stub for the apache logging library.
+ */
+public final class LogFactory {
+ private LogFactory() {
+ }
+
+ public static Log getLog(Class clazz) {
+ return new Log(clazz);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java
new file mode 100644
index 000000000..c6d6f248a
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java
@@ -0,0 +1,184 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
+
+/**
+ * Stream that constrains itself to a single MIME body part.
+ * After the stream ends (i.e. read() returns -1) {@link #hasMoreParts()}
+ * can be used to determine if a final boundary has been seen or not.
+ * If {@link #parentEOF()} is <code>true</code> an unexpected end of stream
+ * has been detected in the parent stream.
+ *
+ *
+ *
+ * @version $Id: MimeBoundaryInputStream.java,v 1.2 2004/11/29 13:15:42 ntherning Exp $
+ */
+public class MimeBoundaryInputStream extends InputStream {
+
+ private PushbackInputStream s = null;
+ private byte[] boundary = null;
+ private boolean first = true;
+ private boolean eof = false;
+ private boolean parenteof = false;
+ private boolean moreParts = true;
+
+ /**
+ * Creates a new MimeBoundaryInputStream.
+ * @param s The underlying stream.
+ * @param boundary Boundary string (not including leading hyphens).
+ */
+ public MimeBoundaryInputStream(InputStream s, String boundary)
+ throws IOException {
+
+ this.s = new PushbackInputStream(s, boundary.length() + 4);
+
+ boundary = "--" + boundary;
+ this.boundary = new byte[boundary.length()];
+ for (int i = 0; i < this.boundary.length; i++) {
+ this.boundary[i] = (byte) boundary.charAt(i);
+ }
+
+ /*
+ * By reading one byte we will update moreParts to be as expected
+ * before any bytes have been read.
+ */
+ int b = read();
+ if (b != -1) {
+ this.s.unread(b);
+ }
+ }
+
+ /**
+ * Closes the underlying stream.
+ *
+ * @throws IOException on I/O errors.
+ */
+ public void close() throws IOException {
+ s.close();
+ }
+
+ /**
+ * Determines if the underlying stream has more parts (this stream has
+ * not seen an end boundary).
+ *
+ * @return <code>true</code> if there are more parts in the underlying
+ * stream, <code>false</code> otherwise.
+ */
+ public boolean hasMoreParts() {
+ return moreParts;
+ }
+
+ /**
+ * Determines if the parent stream has reached EOF
+ *
+ * @return <code>true</code> if EOF has been reached for the parent stream,
+ * <code>false</code> otherwise.
+ */
+ public boolean parentEOF() {
+ return parenteof;
+ }
+
+ /**
+ * Consumes all unread bytes of this stream. After a call to this method
+ * this stream will have reached EOF.
+ *
+ * @throws IOException on I/O errors.
+ */
+ public void consume() throws IOException {
+ while (read() != -1) {
+ }
+ }
+
+ /**
+ * @see java.io.InputStream#read()
+ */
+ public int read() throws IOException {
+ if (eof) {
+ return -1;
+ }
+
+ if (first) {
+ first = false;
+ if (matchBoundary()) {
+ return -1;
+ }
+ }
+
+ int b1 = s.read();
+ int b2 = s.read();
+
+ if (b1 == '\r' && b2 == '\n') {
+ if (matchBoundary()) {
+ return -1;
+ }
+ }
+
+ if (b2 != -1) {
+ s.unread(b2);
+ }
+
+ parenteof = b1 == -1;
+ eof = parenteof;
+
+ return b1;
+ }
+
+ private boolean matchBoundary() throws IOException {
+
+ for (int i = 0; i < boundary.length; i++) {
+ int b = s.read();
+ if (b != boundary[i]) {
+ if (b != -1) {
+ s.unread(b);
+ }
+ for (int j = i - 1; j >= 0; j--) {
+ s.unread(boundary[j]);
+ }
+ return false;
+ }
+ }
+
+ /*
+ * We have a match. Is it an end boundary?
+ */
+ int prev = s.read();
+ int curr = s.read();
+ moreParts = !(prev == '-' && curr == '-');
+ do {
+ if (curr == '\n' && prev == '\r') {
+ break;
+ }
+ prev = curr;
+ } while ((curr = s.read()) != -1);
+
+ if (curr == -1) {
+ moreParts = false;
+ parenteof = true;
+ }
+
+ eof = true;
+
+ return true;
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java
new file mode 100644
index 000000000..a8aad5a38
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java
@@ -0,0 +1,324 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import org.apache.james.mime4j.decoder.Base64InputStream;
+import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.BitSet;
+import java.util.LinkedList;
+
+/**
+ * <p>
+ * Parses MIME (or RFC822) message streams of bytes or characters and reports
+ * parsing events to a <code>ContentHandler</code> instance.
+ * </p>
+ * <p>
+ * Typical usage:<br/>
+ * <pre>
+ * ContentHandler handler = new MyHandler();
+ * MimeStreamParser parser = new MimeStreamParser();
+ * parser.setContentHandler(handler);
+ * parser.parse(new BufferedInputStream(new FileInputStream("mime.msg")));
+ * </pre>
+ * <strong>NOTE:</strong> All lines must end with CRLF
+ * (<code>\r\n</code>). If you are unsure of the line endings in your stream
+ * you should wrap it in a {@link org.apache.james.mime4j.EOLConvertingInputStream} instance.
+ *
+ *
+ * @version $Id: MimeStreamParser.java,v 1.8 2005/02/11 10:12:02 ntherning Exp $
+ */
+public class MimeStreamParser {
+ private static final Log log = LogFactory.getLog(MimeStreamParser.class);
+
+ private static BitSet fieldChars = null;
+
+ private RootInputStream rootStream = null;
+ private LinkedList<BodyDescriptor> bodyDescriptors = new LinkedList<BodyDescriptor>();
+ private ContentHandler handler = null;
+ private boolean raw = false;
+ private boolean prematureEof = false;
+
+ static {
+ fieldChars = new BitSet();
+ for (int i = 0x21; i <= 0x39; i++) {
+ fieldChars.set(i);
+ }
+ for (int i = 0x3b; i <= 0x7e; i++) {
+ fieldChars.set(i);
+ }
+ }
+
+ /**
+ * Creates a new <code>MimeStreamParser</code> instance.
+ */
+ public MimeStreamParser() {
+ }
+
+ /**
+ * Parses a stream of bytes containing a MIME message.
+ *
+ * @param is the stream to parse.
+ * @throws IOException on I/O errors.
+ */
+ public void parse(InputStream is) throws IOException {
+ rootStream = new RootInputStream(is);
+ parseMessage(rootStream);
+ }
+
+ /**
+ * Determines if this parser is currently in raw mode.
+ *
+ * @return <code>true</code> if in raw mode, <code>false</code>
+ * otherwise.
+ * @see #setRaw(boolean)
+ */
+ public boolean isRaw() {
+ return raw;
+ }
+
+ /**
+ * Enables or disables raw mode. In raw mode all future entities
+ * (messages or body parts) in the stream will be reported to the
+ * {@link ContentHandler#raw(InputStream)} handler method only.
+ * The stream will contain the entire unparsed entity contents
+ * including header fields and whatever is in the body.
+ *
+ * @param raw <code>true</code> enables raw mode, <code>false</code>
+ * disables it.
+ */
+ public void setRaw(boolean raw) {
+ this.raw = raw;
+ }
+
+ /**
+ * Finishes the parsing and stops reading lines.
+ * NOTE: No more lines will be parsed but the parser
+ * will still call
+ * {@link ContentHandler#endMultipart()},
+ * {@link ContentHandler#endBodyPart()},
+ * {@link ContentHandler#endMessage()}, etc to match previous calls
+ * to
+ * {@link ContentHandler#startMultipart(BodyDescriptor)},
+ * {@link ContentHandler#startBodyPart()},
+ * {@link ContentHandler#startMessage()}, etc.
+ */
+ public void stop() {
+ rootStream.truncate();
+ }
+
+ /**
+ * Parses an entity which consists of a header followed by a body containing
+ * arbitrary data, body parts or an embedded message.
+ *
+ * @param is the stream to parse.
+ * @throws IOException on I/O errors.
+ */
+ private void parseEntity(InputStream is) throws IOException {
+ BodyDescriptor bd = parseHeader(is);
+
+ if (bd.isMultipart()) {
+ bodyDescriptors.addFirst(bd);
+
+ handler.startMultipart(bd);
+
+ MimeBoundaryInputStream tempIs =
+ new MimeBoundaryInputStream(is, bd.getBoundary());
+ handler.preamble(new CloseShieldInputStream(tempIs));
+ tempIs.consume();
+
+ while (tempIs.hasMoreParts()) {
+ tempIs = new MimeBoundaryInputStream(is, bd.getBoundary());
+ parseBodyPart(tempIs);
+ tempIs.consume();
+ if (tempIs.parentEOF()) {
+ prematureEof = true;
+// if (log.isWarnEnabled()) {
+// log.warn("Line " + rootStream.getLineNumber()
+// + ": Body part ended prematurely. "
+// + "Higher level boundary detected or "
+// + "EOF reached.");
+// }
+ break;
+ }
+ }
+
+ handler.epilogue(new CloseShieldInputStream(is));
+
+ handler.endMultipart();
+
+ bodyDescriptors.removeFirst();
+
+ } else if (bd.isMessage()) {
+ if (bd.isBase64Encoded()) {
+ log.warn("base64 encoded message/rfc822 detected");
+ is = new EOLConvertingInputStream(
+ new Base64InputStream(is));
+ } else if (bd.isQuotedPrintableEncoded()) {
+ log.warn("quoted-printable encoded message/rfc822 detected");
+ is = new EOLConvertingInputStream(
+ new QuotedPrintableInputStream(is));
+ }
+ bodyDescriptors.addFirst(bd);
+ parseMessage(is);
+ bodyDescriptors.removeFirst();
+ } else {
+ handler.body(bd, new CloseShieldInputStream(is));
+ }
+
+ /*
+ * Make sure the stream has been consumed.
+ */
+ while (is.read() != -1) {
+ }
+ }
+
+ private void parseMessage(InputStream is) throws IOException {
+ if (raw) {
+ handler.raw(new CloseShieldInputStream(is));
+ } else {
+ handler.startMessage();
+ parseEntity(is);
+ handler.endMessage();
+ }
+ }
+
+ public boolean getPrematureEof() {
+ return prematureEof;
+ }
+
+ private void parseBodyPart(InputStream is) throws IOException {
+ if (raw) {
+ handler.raw(new CloseShieldInputStream(is));
+ } else {
+ handler.startBodyPart();
+ parseEntity(is);
+ handler.endBodyPart();
+ }
+ }
+
+ /**
+ * Parses a header.
+ *
+ * @param is the stream to parse.
+ * @return a <code>BodyDescriptor</code> describing the body following
+ * the header.
+ */
+ private BodyDescriptor parseHeader(InputStream is) throws IOException {
+ BodyDescriptor bd = new BodyDescriptor(bodyDescriptors.isEmpty()
+ ? null : (BodyDescriptor) bodyDescriptors.getFirst());
+
+ handler.startHeader();
+
+ int lineNumber = rootStream.getLineNumber();
+
+ StringBuffer sb = new StringBuffer();
+ int curr = 0;
+ int prev = 0;
+ while ((curr = is.read()) != -1) {
+ if (curr == '\n' && (prev == '\n' || prev == 0)) {
+ /*
+ * [\r]\n[\r]\n or an immediate \r\n have been seen.
+ */
+ sb.deleteCharAt(sb.length() - 1);
+ break;
+ }
+ sb.append((char) curr);
+ prev = curr == '\r' ? prev : curr;
+ }
+
+// if (curr == -1 && log.isWarnEnabled()) {
+// log.warn("Line " + rootStream.getLineNumber()
+// + ": Unexpected end of headers detected. "
+// + "Boundary detected in header or EOF reached.");
+// }
+
+ int start = 0;
+ int pos = 0;
+ int startLineNumber = lineNumber;
+ while (pos < sb.length()) {
+ while (pos < sb.length() && sb.charAt(pos) != '\r') {
+ pos++;
+ }
+ if (pos < sb.length() - 1 && sb.charAt(pos + 1) != '\n') {
+ pos++;
+ continue;
+ }
+
+ if (pos >= sb.length() - 2 || fieldChars.get(sb.charAt(pos + 2))) {
+
+ /*
+ * field should be the complete field data excluding the
+ * trailing \r\n.
+ */
+ String field = sb.substring(start, pos);
+ start = pos + 2;
+
+ /*
+ * Check for a valid field.
+ */
+ int index = field.indexOf(':');
+ boolean valid = false;
+ if (index != -1 && fieldChars.get(field.charAt(0))) {
+ valid = true;
+ String fieldName = field.substring(0, index).trim();
+ for (int i = 0; i < fieldName.length(); i++) {
+ if (!fieldChars.get(fieldName.charAt(i))) {
+ valid = false;
+ break;
+ }
+ }
+
+ if (valid) {
+ handler.field(field);
+ bd.addField(fieldName, field.substring(index + 1));
+ }
+ }
+
+ if (!valid && log.isWarnEnabled()) {
+ log.warn("Line " + startLineNumber
+ + ": Ignoring invalid field: '" + field.trim() + "'");
+ }
+
+ startLineNumber = lineNumber;
+ }
+
+ pos += 2;
+ lineNumber++;
+ }
+
+ handler.endHeader();
+
+ return bd;
+ }
+
+ /**
+ * Sets the <code>ContentHandler</code> to use when reporting
+ * parsing events.
+ *
+ * @param h the <code>ContentHandler</code>.
+ */
+ public void setContentHandler(ContentHandler h) {
+ this.handler = h;
+ }
+
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java
new file mode 100644
index 000000000..cc8b2411c
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java
@@ -0,0 +1,111 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * <code>InputStream</code> used by the parser to wrap the original user
+ * supplied stream. This stream keeps track of the current line number and
+ * can also be truncated. When truncated the stream will appear to have
+ * reached end of file. This is used by the parser's
+ * {@link org.apache.james.mime4j.MimeStreamParser#stop()} method.
+ *
+ *
+ * @version $Id: RootInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $
+ */
+class RootInputStream extends InputStream {
+ private InputStream is = null;
+ private int lineNumber = 1;
+ private int prev = -1;
+ private boolean truncated = false;
+
+ /**
+ * Creates a new <code>RootInputStream</code>.
+ *
+ * @param in the stream to read from.
+ */
+ public RootInputStream(InputStream is) {
+ this.is = is;
+ }
+
+ /**
+ * Gets the current line number starting at 1
+ * (the number of <code>\r\n</code> read so far plus 1).
+ *
+ * @return the current line number.
+ */
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ /**
+ * Truncates this <code>InputStream</code>. After this call any
+ * call to {@link #read()}, {@link #read(byte[]) or
+ * {@link #read(byte[], int, int)} will return
+ * -1 as if end-of-file had been reached.
+ */
+ public void truncate() {
+ this.truncated = true;
+ }
+
+ /**
+ * @see java.io.InputStream#read()
+ */
+ public int read() throws IOException {
+ if (truncated) {
+ return -1;
+ }
+
+ int b = is.read();
+ if (prev == '\r' && b == '\n') {
+ lineNumber++;
+ }
+ prev = b;
+ return b;
+ }
+
+ /**
+ *
+ * @see java.io.InputStream#read(byte[], int, int)
+ */
+ public int read(byte[] b, int off, int len) throws IOException {
+ if (truncated) {
+ return -1;
+ }
+
+ int n = is.read(b, off, len);
+ for (int i = off; i < off + n; i++) {
+ if (prev == '\r' && b[i] == '\n') {
+ lineNumber++;
+ }
+ prev = b[i];
+ }
+ return n;
+ }
+
+ /**
+ * @see java.io.InputStream#read(byte[])
+ */
+ public int read(byte[] b) throws IOException {
+ return read(b, 0, b.length);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java
new file mode 100644
index 000000000..6841bc998
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java
@@ -0,0 +1,630 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.codec;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.BitSet;
+import java.util.Locale;
+
+import org.apache.james.mime4j.util.CharsetUtil;
+
+/**
+ * ANDROID: THIS CLASS IS COPIED FROM A NEWER VERSION OF MIME4J
+ */
+
+/**
+ * Static methods for encoding header field values. This includes encoded-words
+ * as defined in <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a>
+ * or display-names of an e-mail address, for example.
+ *
+ */
+public class EncoderUtil {
+
+ // This array is a lookup table that translates 6-bit positive integer index
+ // values into their "Base64 Alphabet" equivalents as specified in Table 1
+ // of RFC 2045.
+ // ANDROID: THIS TABLE IS COPIED FROM BASE64OUTPUTSTREAM
+ static final byte[] BASE64_TABLE = { 'A', 'B', 'C', 'D', 'E', 'F',
+ 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
+ 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
+ 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
+ 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9', '+', '/' };
+
+ // Byte used to pad output.
+ private static final byte BASE64_PAD = '=';
+
+ private static final BitSet Q_REGULAR_CHARS = initChars("=_?");
+
+ private static final BitSet Q_RESTRICTED_CHARS = initChars("=_?\"#$%&'(),.:;<>@[\\]^`{|}~");
+
+ private static final int MAX_USED_CHARACTERS = 50;
+
+ private static final String ENC_WORD_PREFIX = "=?";
+ private static final String ENC_WORD_SUFFIX = "?=";
+
+ private static final int ENCODED_WORD_MAX_LENGTH = 75; // RFC 2047
+
+ private static final BitSet TOKEN_CHARS = initChars("()<>@,;:\\\"/[]?=");
+
+ private static final BitSet ATEXT_CHARS = initChars("()<>@.,;:\\\"[]");
+
+ private static BitSet initChars(String specials) {
+ BitSet bs = new BitSet(128);
+ for (char ch = 33; ch < 127; ch++) {
+ if (specials.indexOf(ch) == -1) {
+ bs.set(ch);
+ }
+ }
+ return bs;
+ }
+
+ /**
+ * Selects one of the two encodings specified in RFC 2047.
+ */
+ public enum Encoding {
+ /** The B encoding (identical to base64 defined in RFC 2045). */
+ B,
+ /** The Q encoding (similar to quoted-printable defined in RFC 2045). */
+ Q
+ }
+
+ /**
+ * Indicates the intended usage of an encoded word.
+ */
+ public enum Usage {
+ /**
+ * Encoded word is used to replace a 'text' token in any Subject or
+ * Comments header field.
+ */
+ TEXT_TOKEN,
+ /**
+ * Encoded word is used to replace a 'word' entity within a 'phrase',
+ * for example, one that precedes an address in a From, To, or Cc
+ * header.
+ */
+ WORD_ENTITY
+ }
+
+ private EncoderUtil() {
+ }
+
+ /**
+ * Encodes the display-name portion of an address. See <a
+ * href='http://www.faqs.org/rfcs/rfc5322.html'>RFC 5322</a> section 3.4
+ * and <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> section
+ * 5.3. The specified string should not be folded.
+ *
+ * @param displayName
+ * display-name to encode.
+ * @return encoded display-name.
+ */
+ public static String encodeAddressDisplayName(String displayName) {
+ // display-name = phrase
+ // phrase = 1*( encoded-word / word )
+ // word = atom / quoted-string
+ // atom = [CFWS] 1*atext [CFWS]
+ // CFWS = comment or folding white space
+
+ if (isAtomPhrase(displayName)) {
+ return displayName;
+ } else if (hasToBeEncoded(displayName, 0)) {
+ return encodeEncodedWord(displayName, Usage.WORD_ENTITY);
+ } else {
+ return quote(displayName);
+ }
+ }
+
+ /**
+ * Encodes the local part of an address specification as described in RFC
+ * 5322 section 3.4.1. Leading and trailing CFWS should have been removed
+ * before calling this method. The specified string should not contain any
+ * illegal (control or non-ASCII) characters.
+ *
+ * @param localPart
+ * the local part to encode
+ * @return the encoded local part.
+ */
+ public static String encodeAddressLocalPart(String localPart) {
+ // local-part = dot-atom / quoted-string
+ // dot-atom = [CFWS] dot-atom-text [CFWS]
+ // CFWS = comment or folding white space
+
+ if (isDotAtomText(localPart)) {
+ return localPart;
+ } else {
+ return quote(localPart);
+ }
+ }
+
+ /**
+ * Encodes the specified strings into a header parameter as described in RFC
+ * 2045 section 5.1 and RFC 2183 section 2. The specified strings should not
+ * contain any illegal (control or non-ASCII) characters.
+ *
+ * @param name
+ * parameter name.
+ * @param value
+ * parameter value.
+ * @return encoded result.
+ */
+ public static String encodeHeaderParameter(String name, String value) {
+ name = name.toLowerCase(Locale.US);
+
+ // value := token / quoted-string
+ if (isToken(value)) {
+ return name + "=" + value;
+ } else {
+ return name + "=" + quote(value);
+ }
+ }
+
+ /**
+ * Shortcut method that encodes the specified text into an encoded-word if
+ * the text has to be encoded.
+ *
+ * @param text
+ * text to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @param usedCharacters
+ * number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
+ * @return the specified text if encoding is not necessary or an encoded
+ * word or a sequence of encoded words otherwise.
+ */
+ public static String encodeIfNecessary(String text, Usage usage,
+ int usedCharacters) {
+ if (hasToBeEncoded(text, usedCharacters))
+ return encodeEncodedWord(text, usage, usedCharacters);
+ else
+ return text;
+ }
+
+ /**
+ * Determines if the specified string has to encoded into an encoded-word.
+ * Returns <code>true</code> if the text contains characters that don't
+ * fall into the printable ASCII character set or if the text contains a
+ * 'word' (sequence of non-whitespace characters) longer than 77 characters
+ * (including characters already used up in the line).
+ *
+ * @param text
+ * text to analyze.
+ * @param usedCharacters
+ * number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
+ * @return <code>true</code> if the specified text has to be encoded into
+ * an encoded-word, <code>false</code> otherwise.
+ */
+ public static boolean hasToBeEncoded(String text, int usedCharacters) {
+ if (text == null)
+ throw new IllegalArgumentException();
+ if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS)
+ throw new IllegalArgumentException();
+
+ int nonWhiteSpaceCount = usedCharacters;
+
+ for (int idx = 0; idx < text.length(); idx++) {
+ char ch = text.charAt(idx);
+ if (ch == '\t' || ch == ' ') {
+ nonWhiteSpaceCount = 0;
+ } else {
+ nonWhiteSpaceCount++;
+ if (nonWhiteSpaceCount > 77) {
+ // Line cannot be folded into multiple lines with no more
+ // than 78 characters each. Encoding as encoded-words makes
+ // that possible. One character has to be reserved for
+ // folding white space; that leaves 77 characters.
+ return true;
+ }
+
+ if (ch < 32 || ch >= 127) {
+ // non-printable ascii character has to be encoded
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Encodes the specified text into an encoded word or a sequence of encoded
+ * words separated by space. The text is separated into a sequence of
+ * encoded words if it does not fit in a single one.
+ * <p>
+ * The charset to encode the specified text into a byte array and the
+ * encoding to use for the encoded-word are detected automatically.
+ * <p>
+ * This method assumes that zero characters have already been used up in the
+ * current line.
+ *
+ * @param text
+ * text to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @return the encoded word (or sequence of encoded words if the given text
+ * does not fit in a single encoded word).
+ * @see #hasToBeEncoded(String, int)
+ */
+ public static String encodeEncodedWord(String text, Usage usage) {
+ return encodeEncodedWord(text, usage, 0, null, null);
+ }
+
+ /**
+ * Encodes the specified text into an encoded word or a sequence of encoded
+ * words separated by space. The text is separated into a sequence of
+ * encoded words if it does not fit in a single one.
+ * <p>
+ * The charset to encode the specified text into a byte array and the
+ * encoding to use for the encoded-word are detected automatically.
+ *
+ * @param text
+ * text to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @param usedCharacters
+ * number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
+ * @return the encoded word (or sequence of encoded words if the given text
+ * does not fit in a single encoded word).
+ * @see #hasToBeEncoded(String, int)
+ */
+ public static String encodeEncodedWord(String text, Usage usage,
+ int usedCharacters) {
+ return encodeEncodedWord(text, usage, usedCharacters, null, null);
+ }
+
+ /**
+ * Encodes the specified text into an encoded word or a sequence of encoded
+ * words separated by space. The text is separated into a sequence of
+ * encoded words if it does not fit in a single one.
+ *
+ * @param text
+ * text to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @param usedCharacters
+ * number of characters already used up (<code>0 <= usedCharacters <= 50</code>).
+ * @param charset
+ * the Java charset that should be used to encode the specified
+ * string into a byte array. A suitable charset is detected
+ * automatically if this parameter is <code>null</code>.
+ * @param encoding
+ * the encoding to use for the encoded-word (either B or Q). A
+ * suitable encoding is automatically chosen if this parameter is
+ * <code>null</code>.
+ * @return the encoded word (or sequence of encoded words if the given text
+ * does not fit in a single encoded word).
+ * @see #hasToBeEncoded(String, int)
+ */
+ public static String encodeEncodedWord(String text, Usage usage,
+ int usedCharacters, Charset charset, Encoding encoding) {
+ if (text == null)
+ throw new IllegalArgumentException();
+ if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS)
+ throw new IllegalArgumentException();
+
+ if (charset == null)
+ charset = determineCharset(text);
+
+ String mimeCharset = CharsetUtil.toMimeCharset(charset.name());
+ if (mimeCharset == null) {
+ // cannot happen if charset was originally null
+ throw new IllegalArgumentException("Unsupported charset");
+ }
+
+ byte[] bytes = encode(text, charset);
+
+ if (encoding == null)
+ encoding = determineEncoding(bytes, usage);
+
+ if (encoding == Encoding.B) {
+ String prefix = ENC_WORD_PREFIX + mimeCharset + "?B?";
+ return encodeB(prefix, text, usedCharacters, charset, bytes);
+ } else {
+ String prefix = ENC_WORD_PREFIX + mimeCharset + "?Q?";
+ return encodeQ(prefix, text, usage, usedCharacters, charset, bytes);
+ }
+ }
+
+ /**
+ * Encodes the specified byte array using the B encoding defined in RFC
+ * 2047.
+ *
+ * @param bytes
+ * byte array to encode.
+ * @return encoded string.
+ */
+ public static String encodeB(byte[] bytes) {
+ StringBuilder sb = new StringBuilder();
+
+ int idx = 0;
+ final int end = bytes.length;
+ for (; idx < end - 2; idx += 3) {
+ int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8
+ | bytes[idx + 2] & 0xff;
+ sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data & 0x3f]);
+ }
+
+ if (idx == end - 2) {
+ int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8;
+ sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]);
+ sb.append((char) BASE64_PAD);
+
+ } else if (idx == end - 1) {
+ int data = (bytes[idx] & 0xff) << 16;
+ sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]);
+ sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]);
+ sb.append((char) BASE64_PAD);
+ sb.append((char) BASE64_PAD);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Encodes the specified byte array using the Q encoding defined in RFC
+ * 2047.
+ *
+ * @param bytes
+ * byte array to encode.
+ * @param usage
+ * whether the encoded-word is to be used to replace a text token
+ * or a word entity (see RFC 822).
+ * @return encoded string.
+ */
+ public static String encodeQ(byte[] bytes, Usage usage) {
+ BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
+ : Q_RESTRICTED_CHARS;
+
+ StringBuilder sb = new StringBuilder();
+
+ final int end = bytes.length;
+ for (int idx = 0; idx < end; idx++) {
+ int v = bytes[idx] & 0xff;
+ if (v == 32) {
+ sb.append('_');
+ } else if (!qChars.get(v)) {
+ sb.append('=');
+ sb.append(hexDigit(v >>> 4));
+ sb.append(hexDigit(v & 0xf));
+ } else {
+ sb.append((char) v);
+ }
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * Tests whether the specified string is a token as defined in RFC 2045
+ * section 5.1.
+ *
+ * @param str
+ * string to test.
+ * @return <code>true</code> if the specified string is a RFC 2045 token,
+ * <code>false</code> otherwise.
+ */
+ public static boolean isToken(String str) {
+ // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials>
+ // tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" /
+ // <"> / "/" / "[" / "]" / "?" / "="
+ // CTL := 0.- 31., 127.
+
+ final int length = str.length();
+ if (length == 0)
+ return false;
+
+ for (int idx = 0; idx < length; idx++) {
+ char ch = str.charAt(idx);
+ if (!TOKEN_CHARS.get(ch))
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isAtomPhrase(String str) {
+ // atom = [CFWS] 1*atext [CFWS]
+
+ boolean containsAText = false;
+
+ final int length = str.length();
+ for (int idx = 0; idx < length; idx++) {
+ char ch = str.charAt(idx);
+ if (ATEXT_CHARS.get(ch)) {
+ containsAText = true;
+ } else if (!CharsetUtil.isWhitespace(ch)) {
+ return false;
+ }
+ }
+
+ return containsAText;
+ }
+
+ // RFC 5322 section 3.2.3
+ private static boolean isDotAtomText(String str) {
+ // dot-atom-text = 1*atext *("." 1*atext)
+ // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" /
+ // "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~"
+
+ char prev = '.';
+
+ final int length = str.length();
+ if (length == 0)
+ return false;
+
+ for (int idx = 0; idx < length; idx++) {
+ char ch = str.charAt(idx);
+
+ if (ch == '.') {
+ if (prev == '.' || idx == length - 1)
+ return false;
+ } else {
+ if (!ATEXT_CHARS.get(ch))
+ return false;
+ }
+
+ prev = ch;
+ }
+
+ return true;
+ }
+
+ // RFC 5322 section 3.2.4
+ private static String quote(String str) {
+ // quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS]
+ // qcontent = qtext / quoted-pair
+ // qtext = %d33 / %d35-91 / %d93-126
+ // quoted-pair = ("\" (VCHAR / WSP))
+ // VCHAR = %x21-7E
+ // DQUOTE = %x22
+
+ String escaped = str.replaceAll("[\\\\\"]", "\\\\$0");
+ return "\"" + escaped + "\"";
+ }
+
+ private static String encodeB(String prefix, String text,
+ int usedCharacters, Charset charset, byte[] bytes) {
+ int encodedLength = bEncodedLength(bytes);
+
+ int totalLength = prefix.length() + encodedLength
+ + ENC_WORD_SUFFIX.length();
+ if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) {
+ return prefix + encodeB(bytes) + ENC_WORD_SUFFIX;
+ } else {
+ int splitOffset = text.offsetByCodePoints(text.length() / 2, -1);
+
+ String part1 = text.substring(0, splitOffset);
+ byte[] bytes1 = encode(part1, charset);
+ String word1 = encodeB(prefix, part1, usedCharacters, charset,
+ bytes1);
+
+ String part2 = text.substring(splitOffset);
+ byte[] bytes2 = encode(part2, charset);
+ String word2 = encodeB(prefix, part2, 0, charset, bytes2);
+
+ return word1 + " " + word2;
+ }
+ }
+
+ private static int bEncodedLength(byte[] bytes) {
+ return (bytes.length + 2) / 3 * 4;
+ }
+
+ private static String encodeQ(String prefix, String text, Usage usage,
+ int usedCharacters, Charset charset, byte[] bytes) {
+ int encodedLength = qEncodedLength(bytes, usage);
+
+ int totalLength = prefix.length() + encodedLength
+ + ENC_WORD_SUFFIX.length();
+ if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) {
+ return prefix + encodeQ(bytes, usage) + ENC_WORD_SUFFIX;
+ } else {
+ int splitOffset = text.offsetByCodePoints(text.length() / 2, -1);
+
+ String part1 = text.substring(0, splitOffset);
+ byte[] bytes1 = encode(part1, charset);
+ String word1 = encodeQ(prefix, part1, usage, usedCharacters,
+ charset, bytes1);
+
+ String part2 = text.substring(splitOffset);
+ byte[] bytes2 = encode(part2, charset);
+ String word2 = encodeQ(prefix, part2, usage, 0, charset, bytes2);
+
+ return word1 + " " + word2;
+ }
+ }
+
+ private static int qEncodedLength(byte[] bytes, Usage usage) {
+ BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
+ : Q_RESTRICTED_CHARS;
+
+ int count = 0;
+
+ for (int idx = 0; idx < bytes.length; idx++) {
+ int v = bytes[idx] & 0xff;
+ if (v == 32) {
+ count++;
+ } else if (!qChars.get(v)) {
+ count += 3;
+ } else {
+ count++;
+ }
+ }
+
+ return count;
+ }
+
+ private static byte[] encode(String text, Charset charset) {
+ ByteBuffer buffer = charset.encode(text);
+ byte[] bytes = new byte[buffer.limit()];
+ buffer.get(bytes);
+ return bytes;
+ }
+
+ private static Charset determineCharset(String text) {
+ // it is an important property of iso-8859-1 that it directly maps
+ // unicode code points 0000 to 00ff to byte values 00 to ff.
+ boolean ascii = true;
+ final int len = text.length();
+ for (int index = 0; index < len; index++) {
+ char ch = text.charAt(index);
+ if (ch > 0xff) {
+ return CharsetUtil.UTF_8;
+ }
+ if (ch > 0x7f) {
+ ascii = false;
+ }
+ }
+ return ascii ? CharsetUtil.US_ASCII : CharsetUtil.ISO_8859_1;
+ }
+
+ private static Encoding determineEncoding(byte[] bytes, Usage usage) {
+ if (bytes.length == 0)
+ return Encoding.Q;
+
+ BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS
+ : Q_RESTRICTED_CHARS;
+
+ int qEncoded = 0;
+ for (int i = 0; i < bytes.length; i++) {
+ int v = bytes[i] & 0xff;
+ if (v != 32 && !qChars.get(v)) {
+ qEncoded++;
+ }
+ }
+
+ int percentage = qEncoded * 100 / bytes.length;
+ return percentage > 30 ? Encoding.B : Encoding.Q;
+ }
+
+ private static char hexDigit(int i) {
+ return i < 10 ? (char) (i + '0') : (char) (i - 10 + 'A');
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java
new file mode 100644
index 000000000..77f5d7d4a
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java
@@ -0,0 +1,151 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+/**
+ * Modified to improve efficiency by Android 21-Aug-2009
+ */
+
+package org.apache.james.mime4j.decoder;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Performs Base-64 decoding on an underlying stream.
+ *
+ *
+ * @version $Id: Base64InputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
+ */
+public class Base64InputStream extends InputStream {
+ private final InputStream s;
+ private int outCount = 0;
+ private int outIndex = 0;
+ private final int[] outputBuffer = new int[3];
+ private final byte[] inputBuffer = new byte[4];
+ private boolean done = false;
+
+ public Base64InputStream(InputStream s) {
+ this.s = s;
+ }
+
+ /**
+ * Closes the underlying stream.
+ *
+ * @throws IOException on I/O errors.
+ */
+ @Override
+ public void close() throws IOException {
+ s.close();
+ }
+
+ @Override
+ public int read() throws IOException {
+ if (outIndex == outCount) {
+ fillBuffer();
+ if (outIndex == outCount) {
+ return -1;
+ }
+ }
+
+ return outputBuffer[outIndex++];
+ }
+
+ /**
+ * Retrieve data from the underlying stream, decode it,
+ * and put the results in the byteq.
+ * @throws IOException
+ */
+ private void fillBuffer() throws IOException {
+ outCount = 0;
+ outIndex = 0;
+ int inCount = 0;
+
+ int i;
+ // "done" is needed for the two successive '=' at the end
+ while (!done) {
+ switch (i = s.read()) {
+ case -1:
+ // No more input - just return, let outputBuffer drain out, and be done
+ return;
+ case '=':
+ // once we meet the first '=', avoid reading the second '='
+ done = true;
+ decodeAndEnqueue(inCount);
+ return;
+ default:
+ byte sX = TRANSLATION[i];
+ if (sX < 0) continue;
+ inputBuffer[inCount++] = sX;
+ if (inCount == 4) {
+ decodeAndEnqueue(inCount);
+ return;
+ }
+ break;
+ }
+ }
+ }
+
+ private void decodeAndEnqueue(int len) {
+ int accum = 0;
+ accum |= inputBuffer[0] << 18;
+ accum |= inputBuffer[1] << 12;
+ accum |= inputBuffer[2] << 6;
+ accum |= inputBuffer[3];
+
+ // There's a bit of duplicated code here because we want to have straight-through operation
+ // for the most common case of len==4
+ if (len == 4) {
+ outputBuffer[0] = (accum >> 16) & 0xFF;
+ outputBuffer[1] = (accum >> 8) & 0xFF;
+ outputBuffer[2] = (accum) & 0xFF;
+ outCount = 3;
+ return;
+ } else if (len == 3) {
+ outputBuffer[0] = (accum >> 16) & 0xFF;
+ outputBuffer[1] = (accum >> 8) & 0xFF;
+ outCount = 2;
+ return;
+ } else { // len == 2
+ outputBuffer[0] = (accum >> 16) & 0xFF;
+ outCount = 1;
+ return;
+ }
+ }
+
+ private static byte[] TRANSLATION = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 0x30 */
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xA0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xB0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xC0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xD0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xE0 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* 0xF0 */
+ };
+
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java
new file mode 100644
index 000000000..6d7ccef52
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java
@@ -0,0 +1,62 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.decoder;
+
+import java.util.Iterator;
+
+public class ByteQueue {
+
+ private UnboundedFifoByteBuffer buf;
+ private int initialCapacity = -1;
+
+ public ByteQueue() {
+ buf = new UnboundedFifoByteBuffer();
+ }
+
+ public ByteQueue(int initialCapacity) {
+ buf = new UnboundedFifoByteBuffer(initialCapacity);
+ this.initialCapacity = initialCapacity;
+ }
+
+ public void enqueue(byte b) {
+ buf.add(b);
+ }
+
+ public byte dequeue() {
+ return buf.remove();
+ }
+
+ public int count() {
+ return buf.size();
+ }
+
+ public void clear() {
+ if (initialCapacity != -1)
+ buf = new UnboundedFifoByteBuffer(initialCapacity);
+ else
+ buf = new UnboundedFifoByteBuffer();
+ }
+
+ public Iterator iterator() {
+ return buf.iterator();
+ }
+
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java
new file mode 100644
index 000000000..48fe07dee
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java
@@ -0,0 +1,284 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.decoder;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+import org.apache.james.mime4j.util.CharsetUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Static methods for decoding strings, byte arrays and encoded words.
+ *
+ *
+ * @version $Id: DecoderUtil.java,v 1.3 2005/02/07 15:33:59 ntherning Exp $
+ */
+public class DecoderUtil {
+ private static Log log = LogFactory.getLog(DecoderUtil.class);
+
+ /**
+ * Decodes a string containing quoted-printable encoded data.
+ *
+ * @param s the string to decode.
+ * @return the decoded bytes.
+ */
+ public static byte[] decodeBaseQuotedPrintable(String s) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ try {
+ byte[] bytes = s.getBytes("US-ASCII");
+
+ QuotedPrintableInputStream is = new QuotedPrintableInputStream(
+ new ByteArrayInputStream(bytes));
+
+ int b = 0;
+ while ((b = is.read()) != -1) {
+ baos.write(b);
+ }
+ } catch (IOException e) {
+ /*
+ * This should never happen!
+ */
+ log.error(e);
+ }
+
+ return baos.toByteArray();
+ }
+
+ /**
+ * Decodes a string containing base64 encoded data.
+ *
+ * @param s the string to decode.
+ * @return the decoded bytes.
+ */
+ public static byte[] decodeBase64(String s) {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+ try {
+ byte[] bytes = s.getBytes("US-ASCII");
+
+ Base64InputStream is = new Base64InputStream(
+ new ByteArrayInputStream(bytes));
+
+ int b = 0;
+ while ((b = is.read()) != -1) {
+ baos.write(b);
+ }
+ } catch (IOException e) {
+ /*
+ * This should never happen!
+ */
+ log.error(e);
+ }
+
+ return baos.toByteArray();
+ }
+
+ /**
+ * Decodes an encoded word encoded with the 'B' encoding (described in
+ * RFC 2047) found in a header field body.
+ *
+ * @param encodedWord the encoded word to decode.
+ * @param charset the Java charset to use.
+ * @return the decoded string.
+ * @throws UnsupportedEncodingException if the given Java charset isn't
+ * supported.
+ */
+ public static String decodeB(String encodedWord, String charset)
+ throws UnsupportedEncodingException {
+
+ return new String(decodeBase64(encodedWord), charset);
+ }
+
+ /**
+ * Decodes an encoded word encoded with the 'Q' encoding (described in
+ * RFC 2047) found in a header field body.
+ *
+ * @param encodedWord the encoded word to decode.
+ * @param charset the Java charset to use.
+ * @return the decoded string.
+ * @throws UnsupportedEncodingException if the given Java charset isn't
+ * supported.
+ */
+ public static String decodeQ(String encodedWord, String charset)
+ throws UnsupportedEncodingException {
+
+ /*
+ * Replace _ with =20
+ */
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < encodedWord.length(); i++) {
+ char c = encodedWord.charAt(i);
+ if (c == '_') {
+ sb.append("=20");
+ } else {
+ sb.append(c);
+ }
+ }
+
+ return new String(decodeBaseQuotedPrintable(sb.toString()), charset);
+ }
+
+ /**
+ * Decodes a string containing encoded words as defined by RFC 2047.
+ * Encoded words in have the form
+ * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for
+ * quoted-printable and 'B' or 'b' for Base64.
+ *
+ * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J
+ *
+ * @param body the string to decode.
+ * @return the decoded string.
+ */
+ public static String decodeEncodedWords(String body) {
+
+ // ANDROID: Most strings will not include "=?" so a quick test can prevent unneeded
+ // object creation. This could also be handled via lazy creation of the StringBuilder.
+ if (body.indexOf("=?") == -1) {
+ return body;
+ }
+
+ int previousEnd = 0;
+ boolean previousWasEncoded = false;
+
+ StringBuilder sb = new StringBuilder();
+
+ while (true) {
+ int begin = body.indexOf("=?", previousEnd);
+
+ // ANDROID: The mime4j original version has an error here. It gets confused if
+ // the encoded string begins with an '=' (just after "?Q?"). This patch seeks forward
+ // to find the two '?' in the "header", before looking for the final "?=".
+ if (begin == -1) {
+ break;
+ }
+ int qm1 = body.indexOf('?', begin + 2);
+ if (qm1 == -1) {
+ break;
+ }
+ int qm2 = body.indexOf('?', qm1 + 1);
+ if (qm2 == -1) {
+ break;
+ }
+ int end = body.indexOf("?=", qm2 + 1);
+ if (end == -1) {
+ break;
+ }
+ end += 2;
+
+ String sep = body.substring(previousEnd, begin);
+
+ String decoded = decodeEncodedWord(body, begin, end);
+ if (decoded == null) {
+ sb.append(sep);
+ sb.append(body.substring(begin, end));
+ } else {
+ if (!previousWasEncoded || !CharsetUtil.isWhitespace(sep)) {
+ sb.append(sep);
+ }
+ sb.append(decoded);
+ }
+
+ previousEnd = end;
+ previousWasEncoded = decoded != null;
+ }
+
+ if (previousEnd == 0)
+ return body;
+
+ sb.append(body.substring(previousEnd));
+ return sb.toString();
+ }
+
+ // return null on error. Begin is index of '=?' in body.
+ public static String decodeEncodedWord(String body, int begin, int end) {
+ // Skip the '?=' chars in body and scan forward from there for next '?'
+ int qm1 = body.indexOf('?', begin + 2);
+ if (qm1 == -1 || qm1 == end - 2)
+ return null;
+
+ int qm2 = body.indexOf('?', qm1 + 1);
+ if (qm2 == -1 || qm2 == end - 2)
+ return null;
+
+ String mimeCharset = body.substring(begin + 2, qm1);
+ String encoding = body.substring(qm1 + 1, qm2);
+ String encodedText = body.substring(qm2 + 1, end - 2);
+
+ String charset = CharsetUtil.toJavaCharset(mimeCharset);
+ if (charset == null) {
+ if (log.isWarnEnabled()) {
+ log.warn("MIME charset '" + mimeCharset + "' in encoded word '"
+ + body.substring(begin, end) + "' doesn't have a "
+ + "corresponding Java charset");
+ }
+ return null;
+ } else if (!CharsetUtil.isDecodingSupported(charset)) {
+ if (log.isWarnEnabled()) {
+ log.warn("Current JDK doesn't support decoding of charset '"
+ + charset + "' (MIME charset '" + mimeCharset
+ + "' in encoded word '" + body.substring(begin, end)
+ + "')");
+ }
+ return null;
+ }
+
+ if (encodedText.length() == 0) {
+ if (log.isWarnEnabled()) {
+ log.warn("Missing encoded text in encoded word: '"
+ + body.substring(begin, end) + "'");
+ }
+ return null;
+ }
+
+ try {
+ if (encoding.equalsIgnoreCase("Q")) {
+ return DecoderUtil.decodeQ(encodedText, charset);
+ } else if (encoding.equalsIgnoreCase("B")) {
+ return DecoderUtil.decodeB(encodedText, charset);
+ } else {
+ if (log.isWarnEnabled()) {
+ log.warn("Warning: Unknown encoding in encoded word '"
+ + body.substring(begin, end) + "'");
+ }
+ return null;
+ }
+ } catch (UnsupportedEncodingException e) {
+ // should not happen because of isDecodingSupported check above
+ if (log.isWarnEnabled()) {
+ log.warn("Unsupported encoding in encoded word '"
+ + body.substring(begin, end) + "'", e);
+ }
+ return null;
+ } catch (RuntimeException e) {
+ if (log.isWarnEnabled()) {
+ log.warn("Could not decode encoded word '"
+ + body.substring(begin, end) + "'", e);
+ }
+ return null;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
new file mode 100644
index 000000000..e43f398f9
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java
@@ -0,0 +1,229 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.decoder;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+
+/**
+ * Performs Quoted-Printable decoding on an underlying stream.
+ *
+ *
+ *
+ * @version $Id: QuotedPrintableInputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $
+ */
+public class QuotedPrintableInputStream extends InputStream {
+ private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class);
+
+ private InputStream stream;
+ ByteQueue byteq = new ByteQueue();
+ ByteQueue pushbackq = new ByteQueue();
+ private byte state = 0;
+
+ public QuotedPrintableInputStream(InputStream stream) {
+ this.stream = stream;
+ }
+
+ /**
+ * Closes the underlying stream.
+ *
+ * @throws IOException on I/O errors.
+ */
+ public void close() throws IOException {
+ stream.close();
+ }
+
+ public int read() throws IOException {
+ fillBuffer();
+ if (byteq.count() == 0)
+ return -1;
+ else {
+ byte val = byteq.dequeue();
+ if (val >= 0)
+ return val;
+ else
+ return val & 0xFF;
+ }
+ }
+
+ /**
+ * Pulls bytes out of the underlying stream and places them in the
+ * pushback queue. This is necessary (vs. reading from the
+ * underlying stream directly) to detect and filter out "transport
+ * padding" whitespace, i.e., all whitespace that appears immediately
+ * before a CRLF.
+ *
+ * @throws IOException Underlying stream threw IOException.
+ */
+ private void populatePushbackQueue() throws IOException {
+ //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!");
+
+ if (pushbackq.count() != 0)
+ return;
+
+ while (true) {
+ int i = stream.read();
+ switch (i) {
+ case -1:
+ // stream is done
+ pushbackq.clear(); // discard any whitespace preceding EOF
+ return;
+ case ' ':
+ case '\t':
+ pushbackq.enqueue((byte)i);
+ break;
+ case '\r':
+ case '\n':
+ pushbackq.clear(); // discard any whitespace preceding EOL
+ pushbackq.enqueue((byte)i);
+ return;
+ default:
+ pushbackq.enqueue((byte)i);
+ return;
+ }
+ }
+ }
+
+ /**
+ * Causes the pushback queue to get populated if it is empty, then
+ * consumes and decodes bytes out of it until one or more bytes are
+ * in the byte queue. This decoding step performs the actual QP
+ * decoding.
+ *
+ * @throws IOException Underlying stream threw IOException.
+ */
+ private void fillBuffer() throws IOException {
+ byte msdChar = 0; // first digit of escaped num
+ while (byteq.count() == 0) {
+ if (pushbackq.count() == 0) {
+ populatePushbackQueue();
+ if (pushbackq.count() == 0)
+ return;
+ }
+
+ byte b = (byte)pushbackq.dequeue();
+
+ switch (state) {
+ case 0: // start state, no bytes pending
+ if (b != '=') {
+ byteq.enqueue(b);
+ break; // state remains 0
+ } else {
+ state = 1;
+ break;
+ }
+ case 1: // encountered "=" so far
+ if (b == '\r') {
+ state = 2;
+ break;
+ } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
+ state = 3;
+ msdChar = b; // save until next digit encountered
+ break;
+ } else if (b == '=') {
+ /*
+ * Special case when == is encountered.
+ * Emit one = and stay in this state.
+ */
+ if (log.isWarnEnabled()) {
+ log.warn("Malformed MIME; got ==");
+ }
+ byteq.enqueue((byte)'=');
+ break;
+ } else {
+ if (log.isWarnEnabled()) {
+ log.warn("Malformed MIME; expected \\r or "
+ + "[0-9A-Z], got " + b);
+ }
+ state = 0;
+ byteq.enqueue((byte)'=');
+ byteq.enqueue(b);
+ break;
+ }
+ case 2: // encountered "=\r" so far
+ if (b == '\n') {
+ state = 0;
+ break;
+ } else {
+ if (log.isWarnEnabled()) {
+ log.warn("Malformed MIME; expected "
+ + (int)'\n' + ", got " + b);
+ }
+ state = 0;
+ byteq.enqueue((byte)'=');
+ byteq.enqueue((byte)'\r');
+ byteq.enqueue(b);
+ break;
+ }
+ case 3: // encountered =<digit> so far; expecting another <digit> to complete the octet
+ if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) {
+ byte msd = asciiCharToNumericValue(msdChar);
+ byte low = asciiCharToNumericValue(b);
+ state = 0;
+ byteq.enqueue((byte)((msd << 4) | low));
+ break;
+ } else {
+ if (log.isWarnEnabled()) {
+ log.warn("Malformed MIME; expected "
+ + "[0-9A-Z], got " + b);
+ }
+ state = 0;
+ byteq.enqueue((byte)'=');
+ byteq.enqueue(msdChar);
+ byteq.enqueue(b);
+ break;
+ }
+ default: // should never happen
+ log.error("Illegal state: " + state);
+ state = 0;
+ byteq.enqueue(b);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Converts '0' => 0, 'A' => 10, etc.
+ * @param c ASCII character value.
+ * @return Numeric value of hexadecimal character.
+ */
+ private byte asciiCharToNumericValue(byte c) {
+ if (c >= '0' && c <= '9') {
+ return (byte)(c - '0');
+ } else if (c >= 'A' && c <= 'Z') {
+ return (byte)(0xA + (c - 'A'));
+ } else if (c >= 'a' && c <= 'z') {
+ return (byte)(0xA + (c - 'a'));
+ } else {
+ /*
+ * This should never happen since all calls to this method
+ * are preceded by a check that c is in [0-9A-Za-z]
+ */
+ throw new IllegalArgumentException((char) c
+ + " is not a hexadecimal digit");
+ }
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
new file mode 100644
index 000000000..f01194fd1
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java
@@ -0,0 +1,272 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.decoder;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * UnboundedFifoByteBuffer is a very efficient buffer implementation.
+ * According to performance testing, it exhibits a constant access time, but it
+ * also outperforms ArrayList when used for the same purpose.
+ * <p>
+ * The removal order of an <code>UnboundedFifoByteBuffer</code> is based on the insertion
+ * order; elements are removed in the same order in which they were added.
+ * The iteration order is the same as the removal order.
+ * <p>
+ * The {@link #remove()} and {@link #get()} operations perform in constant time.
+ * The {@link #add(Object)} operation performs in amortized constant time. All
+ * other operations perform in linear time or worse.
+ * <p>
+ * Note that this implementation is not synchronized. The following can be
+ * used to provide synchronized access to your <code>UnboundedFifoByteBuffer</code>:
+ * <pre>
+ * Buffer fifo = BufferUtils.synchronizedBuffer(new UnboundedFifoByteBuffer());
+ * </pre>
+ * <p>
+ * This buffer prevents null objects from being added.
+ *
+ * @since Commons Collections 3.0 (previously in main package v2.1)
+ * @version $Revision: 1.1 $ $Date: 2004/08/24 06:52:02 $
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+class UnboundedFifoByteBuffer {
+
+ protected byte[] buffer;
+ protected int head;
+ protected int tail;
+
+ /**
+ * Constructs an UnboundedFifoByteBuffer with the default number of elements.
+ * It is exactly the same as performing the following:
+ *
+ * <pre>
+ * new UnboundedFifoByteBuffer(32);
+ * </pre>
+ */
+ public UnboundedFifoByteBuffer() {
+ this(32);
+ }
+
+ /**
+ * Constructs an UnboundedFifoByteBuffer with the specified number of elements.
+ * The integer must be a positive integer.
+ *
+ * @param initialSize the initial size of the buffer
+ * @throws IllegalArgumentException if the size is less than 1
+ */
+ public UnboundedFifoByteBuffer(int initialSize) {
+ if (initialSize <= 0) {
+ throw new IllegalArgumentException("The size must be greater than 0");
+ }
+ buffer = new byte[initialSize + 1];
+ head = 0;
+ tail = 0;
+ }
+
+ /**
+ * Returns the number of elements stored in the buffer.
+ *
+ * @return this buffer's size
+ */
+ public int size() {
+ int size = 0;
+
+ if (tail < head) {
+ size = buffer.length - head + tail;
+ } else {
+ size = tail - head;
+ }
+
+ return size;
+ }
+
+ /**
+ * Returns true if this buffer is empty; false otherwise.
+ *
+ * @return true if this buffer is empty
+ */
+ public boolean isEmpty() {
+ return (size() == 0);
+ }
+
+ /**
+ * Adds the given element to this buffer.
+ *
+ * @param b the byte to add
+ * @return true, always
+ */
+ public boolean add(final byte b) {
+
+ if (size() + 1 >= buffer.length) {
+ byte[] tmp = new byte[((buffer.length - 1) * 2) + 1];
+
+ int j = 0;
+ for (int i = head; i != tail;) {
+ tmp[j] = buffer[i];
+ buffer[i] = 0;
+
+ j++;
+ i++;
+ if (i == buffer.length) {
+ i = 0;
+ }
+ }
+
+ buffer = tmp;
+ head = 0;
+ tail = j;
+ }
+
+ buffer[tail] = b;
+ tail++;
+ if (tail >= buffer.length) {
+ tail = 0;
+ }
+ return true;
+ }
+
+ /**
+ * Returns the next object in the buffer.
+ *
+ * @return the next object in the buffer
+ * @throws BufferUnderflowException if this buffer is empty
+ */
+ public byte get() {
+ if (isEmpty()) {
+ throw new IllegalStateException("The buffer is already empty");
+ }
+
+ return buffer[head];
+ }
+
+ /**
+ * Removes the next object from the buffer
+ *
+ * @return the removed object
+ * @throws BufferUnderflowException if this buffer is empty
+ */
+ public byte remove() {
+ if (isEmpty()) {
+ throw new IllegalStateException("The buffer is already empty");
+ }
+
+ byte element = buffer[head];
+
+ head++;
+ if (head >= buffer.length) {
+ head = 0;
+ }
+
+ return element;
+ }
+
+ /**
+ * Increments the internal index.
+ *
+ * @param index the index to increment
+ * @return the updated index
+ */
+ private int increment(int index) {
+ index++;
+ if (index >= buffer.length) {
+ index = 0;
+ }
+ return index;
+ }
+
+ /**
+ * Decrements the internal index.
+ *
+ * @param index the index to decrement
+ * @return the updated index
+ */
+ private int decrement(int index) {
+ index--;
+ if (index < 0) {
+ index = buffer.length - 1;
+ }
+ return index;
+ }
+
+ /**
+ * Returns an iterator over this buffer's elements.
+ *
+ * @return an iterator over this buffer's elements
+ */
+ public Iterator iterator() {
+ return new Iterator() {
+
+ private int index = head;
+ private int lastReturnedIndex = -1;
+
+ public boolean hasNext() {
+ return index != tail;
+
+ }
+
+ public Object next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+ lastReturnedIndex = index;
+ index = increment(index);
+ return new Byte(buffer[lastReturnedIndex]);
+ }
+
+ public void remove() {
+ if (lastReturnedIndex == -1) {
+ throw new IllegalStateException();
+ }
+
+ // First element can be removed quickly
+ if (lastReturnedIndex == head) {
+ UnboundedFifoByteBuffer.this.remove();
+ lastReturnedIndex = -1;
+ return;
+ }
+
+ // Other elements require us to shift the subsequent elements
+ int i = lastReturnedIndex + 1;
+ while (i != tail) {
+ if (i >= buffer.length) {
+ buffer[i - 1] = buffer[0];
+ i = 0;
+ } else {
+ buffer[i - 1] = buffer[i];
+ i++;
+ }
+ }
+
+ lastReturnedIndex = -1;
+ tail = decrement(tail);
+ buffer[tail] = 0;
+ index = decrement(index);
+ }
+
+ };
+ }
+
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java
new file mode 100644
index 000000000..df9f39835
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java
@@ -0,0 +1,65 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+import org.apache.james.mime4j.field.address.AddressList;
+import org.apache.james.mime4j.field.address.parser.ParseException;
+
+public class AddressListField extends Field {
+ private AddressList addressList;
+ private ParseException parseException;
+
+ protected AddressListField(String name, String body, String raw, AddressList addressList, ParseException parseException) {
+ super(name, body, raw);
+ this.addressList = addressList;
+ this.parseException = parseException;
+ }
+
+ public AddressList getAddressList() {
+ return addressList;
+ }
+
+ public ParseException getParseException() {
+ return parseException;
+ }
+
+ public static class Parser implements FieldParser {
+ private static Log log = LogFactory.getLog(Parser.class);
+
+ public Field parse(final String name, final String body, final String raw) {
+ AddressList addressList = null;
+ ParseException parseException = null;
+ try {
+ addressList = AddressList.parse(body);
+ }
+ catch (ParseException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Parsing value '" + body + "': "+ e.getMessage());
+ }
+ parseException = e;
+ }
+ return new AddressListField(name, body, raw, addressList, parseException);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java
new file mode 100644
index 000000000..73d8d2339
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java
@@ -0,0 +1,88 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+
+
+/**
+ * Represents a <code>Content-Transfer-Encoding</code> field.
+ *
+ *
+ * @version $Id: ContentTransferEncodingField.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $
+ */
+public class ContentTransferEncodingField extends Field {
+ /**
+ * The <code>7bit</code> encoding.
+ */
+ public static final String ENC_7BIT = "7bit";
+ /**
+ * The <code>8bit</code> encoding.
+ */
+ public static final String ENC_8BIT = "8bit";
+ /**
+ * The <code>binary</code> encoding.
+ */
+ public static final String ENC_BINARY = "binary";
+ /**
+ * The <code>quoted-printable</code> encoding.
+ */
+ public static final String ENC_QUOTED_PRINTABLE = "quoted-printable";
+ /**
+ * The <code>base64</code> encoding.
+ */
+ public static final String ENC_BASE64 = "base64";
+
+ private String encoding;
+
+ protected ContentTransferEncodingField(String name, String body, String raw, String encoding) {
+ super(name, body, raw);
+ this.encoding = encoding;
+ }
+
+ /**
+ * Gets the encoding defined in this field.
+ *
+ * @return the encoding or an empty string if not set.
+ */
+ public String getEncoding() {
+ return encoding;
+ }
+
+ /**
+ * Gets the encoding of the given field if. Returns the default
+ * <code>7bit</code> if not set or if
+ * <code>f</code> is <code>null</code>.
+ *
+ * @return the encoding.
+ */
+ public static String getEncoding(ContentTransferEncodingField f) {
+ if (f != null && f.getEncoding().length() != 0) {
+ return f.getEncoding();
+ }
+ return ENC_7BIT;
+ }
+
+ public static class Parser implements FieldParser {
+ public Field parse(final String name, final String body, final String raw) {
+ final String encoding = body.trim().toLowerCase();
+ return new ContentTransferEncodingField(name, body, raw, encoding);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java
new file mode 100644
index 000000000..ad9f7f9ac
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java
@@ -0,0 +1,259 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+import org.apache.james.mime4j.field.contenttype.parser.ContentTypeParser;
+import org.apache.james.mime4j.field.contenttype.parser.ParseException;
+import org.apache.james.mime4j.field.contenttype.parser.TokenMgrError;
+
+/**
+ * Represents a <code>Content-Type</code> field.
+ *
+ * <p>TODO: Remove dependency on Java 1.4 regexps</p>
+ *
+ *
+ * @version $Id: ContentTypeField.java,v 1.6 2005/01/27 14:16:31 ntherning Exp $
+ */
+public class ContentTypeField extends Field {
+
+ /**
+ * The prefix of all <code>multipart</code> MIME types.
+ */
+ public static final String TYPE_MULTIPART_PREFIX = "multipart/";
+ /**
+ * The <code>multipart/digest</code> MIME type.
+ */
+ public static final String TYPE_MULTIPART_DIGEST = "multipart/digest";
+ /**
+ * The <code>text/plain</code> MIME type.
+ */
+ public static final String TYPE_TEXT_PLAIN = "text/plain";
+ /**
+ * The <code>message/rfc822</code> MIME type.
+ */
+ public static final String TYPE_MESSAGE_RFC822 = "message/rfc822";
+ /**
+ * The name of the <code>boundary</code> parameter.
+ */
+ public static final String PARAM_BOUNDARY = "boundary";
+ /**
+ * The name of the <code>charset</code> parameter.
+ */
+ public static final String PARAM_CHARSET = "charset";
+
+ private String mimeType = "";
+ private Map<String, String> parameters = null;
+ private ParseException parseException;
+
+ protected ContentTypeField(String name, String body, String raw, String mimeType, Map<String, String> parameters, ParseException parseException) {
+ super(name, body, raw);
+ this.mimeType = mimeType;
+ this.parameters = parameters;
+ this.parseException = parseException;
+ }
+
+ /**
+ * Gets the exception that was raised during parsing of
+ * the field value, if any; otherwise, null.
+ */
+ public ParseException getParseException() {
+ return parseException;
+ }
+
+ /**
+ * Gets the MIME type defined in this Content-Type field.
+ *
+ * @return the MIME type or an empty string if not set.
+ */
+ public String getMimeType() {
+ return mimeType;
+ }
+
+ /**
+ * Gets the MIME type defined in the child's
+ * Content-Type field or derives a MIME type from the parent
+ * if child is <code>null</code> or hasn't got a MIME type value set.
+ * If child's MIME type is multipart but no boundary
+ * has been set the MIME type of child will be derived from
+ * the parent.
+ *
+ * @param child the child.
+ * @param parent the parent.
+ * @return the MIME type.
+ */
+ public static String getMimeType(ContentTypeField child,
+ ContentTypeField parent) {
+
+ if (child == null || child.getMimeType().length() == 0
+ || child.isMultipart() && child.getBoundary() == null) {
+
+ if (parent != null && parent.isMimeType(TYPE_MULTIPART_DIGEST)) {
+ return TYPE_MESSAGE_RFC822;
+ } else {
+ return TYPE_TEXT_PLAIN;
+ }
+ }
+
+ return child.getMimeType();
+ }
+
+ /**
+ * Gets the value of a parameter. Parameter names are case-insensitive.
+ *
+ * @param name the name of the parameter to get.
+ * @return the parameter value or <code>null</code> if not set.
+ */
+ public String getParameter(String name) {
+ return parameters != null
+ ? parameters.get(name.toLowerCase())
+ : null;
+ }
+
+ /**
+ * Gets all parameters.
+ *
+ * @return the parameters.
+ */
+ public Map<String, String> getParameters() {
+ if (parameters != null) {
+ return Collections.unmodifiableMap(parameters);
+ }
+ return Collections.emptyMap();
+ }
+
+ /**
+ * Gets the value of the <code>boundary</code> parameter if set.
+ *
+ * @return the <code>boundary</code> parameter value or <code>null</code>
+ * if not set.
+ */
+ public String getBoundary() {
+ return getParameter(PARAM_BOUNDARY);
+ }
+
+ /**
+ * Gets the value of the <code>charset</code> parameter if set.
+ *
+ * @return the <code>charset</code> parameter value or <code>null</code>
+ * if not set.
+ */
+ public String getCharset() {
+ return getParameter(PARAM_CHARSET);
+ }
+
+ /**
+ * Gets the value of the <code>charset</code> parameter if set for the
+ * given field. Returns the default <code>us-ascii</code> if not set or if
+ * <code>f</code> is <code>null</code>.
+ *
+ * @return the <code>charset</code> parameter value.
+ */
+ public static String getCharset(ContentTypeField f) {
+ if (f != null) {
+ if (f.getCharset() != null && f.getCharset().length() > 0) {
+ return f.getCharset();
+ }
+ }
+ return "us-ascii";
+ }
+
+ /**
+ * Determines if the MIME type of this field matches the given one.
+ *
+ * @param mimeType the MIME type to match against.
+ * @return <code>true</code> if the MIME type of this field matches,
+ * <code>false</code> otherwise.
+ */
+ public boolean isMimeType(String mimeType) {
+ return this.mimeType.equalsIgnoreCase(mimeType);
+ }
+
+ /**
+ * Determines if the MIME type of this field is <code>multipart/*</code>.
+ *
+ * @return <code>true</code> if this field is has a <code>multipart/*</code>
+ * MIME type, <code>false</code> otherwise.
+ */
+ public boolean isMultipart() {
+ return mimeType.startsWith(TYPE_MULTIPART_PREFIX);
+ }
+
+ public static class Parser implements FieldParser {
+ private static Log log = LogFactory.getLog(Parser.class);
+
+ public Field parse(final String name, final String body, final String raw) {
+ ParseException parseException = null;
+ String mimeType = "";
+ Map<String, String> parameters = null;
+
+ ContentTypeParser parser = new ContentTypeParser(new StringReader(body));
+ try {
+ parser.parseAll();
+ }
+ catch (ParseException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Parsing value '" + body + "': "+ e.getMessage());
+ }
+ parseException = e;
+ }
+ catch (TokenMgrError e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Parsing value '" + body + "': "+ e.getMessage());
+ }
+ parseException = new ParseException(e.getMessage());
+ }
+
+ try {
+ final String type = parser.getType();
+ final String subType = parser.getSubType();
+
+ if (type != null && subType != null) {
+ mimeType = (type + "/" + parser.getSubType()).toLowerCase();
+
+ ArrayList<String> paramNames = parser.getParamNames();
+ ArrayList<String> paramValues = parser.getParamValues();
+
+ if (paramNames != null && paramValues != null) {
+ for (int i = 0; i < paramNames.size() && i < paramValues.size(); i++) {
+ if (parameters == null)
+ parameters = new HashMap<String, String>((int)(paramNames.size() * 1.3 + 1));
+ String paramName = paramNames.get(i).toLowerCase();
+ String paramValue = paramValues.get(i);
+ parameters.put(paramName, paramValue);
+ }
+ }
+ }
+ }
+ catch (NullPointerException npe) {
+ }
+ return new ContentTypeField(name, body, raw, mimeType, parameters, parseException);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java
new file mode 100644
index 000000000..1e6c8e250
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java
@@ -0,0 +1,73 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+//BEGIN android-changed: Stubbing out logging
+
+import com.android.voicemailomtp.mail.utils.LogUtils;
+
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END
+import org.apache.james.mime4j.field.datetime.DateTime;
+import org.apache.james.mime4j.field.datetime.parser.ParseException;
+
+import java.util.Date;
+
+public class DateTimeField extends Field {
+ private Date date;
+ private ParseException parseException;
+
+ protected DateTimeField(String name, String body, String raw, Date date, ParseException parseException) {
+ super(name, body, raw);
+ this.date = date;
+ this.parseException = parseException;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public ParseException getParseException() {
+ return parseException;
+ }
+
+ public static class Parser implements FieldParser {
+ private static Log log = LogFactory.getLog(Parser.class);
+
+ public Field parse(final String name, String body, final String raw) {
+ Date date = null;
+ ParseException parseException = null;
+ //BEGIN android-changed
+ body = LogUtils.cleanUpMimeDate(body);
+ //END android-changed
+ try {
+ date = DateTime.parse(body).getDate();
+ }
+ catch (ParseException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Parsing value '" + body + "': "+ e.getMessage());
+ }
+ parseException = e;
+ }
+ return new DateTimeField(name, body, raw, date, parseException);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java
new file mode 100644
index 000000000..3695afe3e
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2006 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field;
+
+public class DefaultFieldParser extends DelegatingFieldParser {
+
+ public DefaultFieldParser() {
+ setFieldParser(Field.CONTENT_TRANSFER_ENCODING, new ContentTransferEncodingField.Parser());
+ setFieldParser(Field.CONTENT_TYPE, new ContentTypeField.Parser());
+
+ final DateTimeField.Parser dateTimeParser = new DateTimeField.Parser();
+ setFieldParser(Field.DATE, dateTimeParser);
+ setFieldParser(Field.RESENT_DATE, dateTimeParser);
+
+ final MailboxListField.Parser mailboxListParser = new MailboxListField.Parser();
+ setFieldParser(Field.FROM, mailboxListParser);
+ setFieldParser(Field.RESENT_FROM, mailboxListParser);
+
+ final MailboxField.Parser mailboxParser = new MailboxField.Parser();
+ setFieldParser(Field.SENDER, mailboxParser);
+ setFieldParser(Field.RESENT_SENDER, mailboxParser);
+
+ final AddressListField.Parser addressListParser = new AddressListField.Parser();
+ setFieldParser(Field.TO, addressListParser);
+ setFieldParser(Field.RESENT_TO, addressListParser);
+ setFieldParser(Field.CC, addressListParser);
+ setFieldParser(Field.RESENT_CC, addressListParser);
+ setFieldParser(Field.BCC, addressListParser);
+ setFieldParser(Field.RESENT_BCC, addressListParser);
+ setFieldParser(Field.REPLY_TO, addressListParser);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java
new file mode 100644
index 000000000..32b69ec13
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2006 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class DelegatingFieldParser implements FieldParser {
+
+ private Map<String, FieldParser> parsers = new HashMap<String, FieldParser>();
+ private FieldParser defaultParser = new UnstructuredField.Parser();
+
+ /**
+ * Sets the parser used for the field named <code>name</code>.
+ * @param name the name of the field
+ * @param parser the parser for fields named <code>name</code>
+ */
+ public void setFieldParser(final String name, final FieldParser parser) {
+ parsers.put(name.toLowerCase(), parser);
+ }
+
+ public FieldParser getParser(final String name) {
+ final FieldParser field = parsers.get(name.toLowerCase());
+ if(field==null) {
+ return defaultParser;
+ }
+ return field;
+ }
+
+ public Field parse(final String name, final String body, final String raw) {
+ final FieldParser parser = getParser(name);
+ return parser.parse(name, body, raw);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java
new file mode 100644
index 000000000..4dea5c5cf
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java
@@ -0,0 +1,192 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * The base class of all field classes.
+ *
+ *
+ * @version $Id: Field.java,v 1.6 2004/10/25 07:26:46 ntherning Exp $
+ */
+public abstract class Field {
+ public static final String SENDER = "Sender";
+ public static final String FROM = "From";
+ public static final String TO = "To";
+ public static final String CC = "Cc";
+ public static final String BCC = "Bcc";
+ public static final String REPLY_TO = "Reply-To";
+ public static final String RESENT_SENDER = "Resent-Sender";
+ public static final String RESENT_FROM = "Resent-From";
+ public static final String RESENT_TO = "Resent-To";
+ public static final String RESENT_CC = "Resent-Cc";
+ public static final String RESENT_BCC = "Resent-Bcc";
+
+ public static final String DATE = "Date";
+ public static final String RESENT_DATE = "Resent-Date";
+
+ public static final String SUBJECT = "Subject";
+ public static final String CONTENT_TYPE = "Content-Type";
+ public static final String CONTENT_TRANSFER_ENCODING =
+ "Content-Transfer-Encoding";
+
+ private static final String FIELD_NAME_PATTERN =
+ "^([\\x21-\\x39\\x3b-\\x7e]+)[ \t]*:";
+ private static final Pattern fieldNamePattern =
+ Pattern.compile(FIELD_NAME_PATTERN);
+
+ private static final DefaultFieldParser parser = new DefaultFieldParser();
+
+ private final String name;
+ private final String body;
+ private final String raw;
+
+ protected Field(final String name, final String body, final String raw) {
+ this.name = name;
+ this.body = body;
+ this.raw = raw;
+ }
+
+ /**
+ * Parses the given string and returns an instance of the
+ * <code>Field</code> class. The type of the class returned depends on
+ * the field name:
+ * <table>
+ * <tr>
+ * <td><em>Field name</em></td><td><em>Class returned</em></td>
+ * <td>Content-Type</td><td>org.apache.james.mime4j.field.ContentTypeField</td>
+ * <td>other</td><td>org.apache.james.mime4j.field.UnstructuredField</td>
+ * </tr>
+ * </table>
+ *
+ * @param s the string to parse.
+ * @return a <code>Field</code> instance.
+ * @throws IllegalArgumentException on parse errors.
+ */
+ public static Field parse(final String raw) {
+
+ /*
+ * Unfold the field.
+ */
+ final String unfolded = raw.replaceAll("\r|\n", "");
+
+ /*
+ * Split into name and value.
+ */
+ final Matcher fieldMatcher = fieldNamePattern.matcher(unfolded);
+ if (!fieldMatcher.find()) {
+ throw new IllegalArgumentException("Invalid field in string");
+ }
+ final String name = fieldMatcher.group(1);
+
+ String body = unfolded.substring(fieldMatcher.end());
+ if (body.length() > 0 && body.charAt(0) == ' ') {
+ body = body.substring(1);
+ }
+
+ return parser.parse(name, body, raw);
+ }
+
+ /**
+ * Gets the default parser used to parse fields.
+ * @return the default field parser
+ */
+ public static DefaultFieldParser getParser() {
+ return parser;
+ }
+
+ /**
+ * Gets the name of the field (<code>Subject</code>,
+ * <code>From</code>, etc).
+ *
+ * @return the field name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Gets the original raw field string.
+ *
+ * @return the original raw field string.
+ */
+ public String getRaw() {
+ return raw;
+ }
+
+ /**
+ * Gets the unfolded, unparsed and possibly encoded (see RFC 2047) field
+ * body string.
+ *
+ * @return the unfolded unparsed field body string.
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * Determines if this is a <code>Content-Type</code> field.
+ *
+ * @return <code>true</code> if this is a <code>Content-Type</code> field,
+ * <code>false</code> otherwise.
+ */
+ public boolean isContentType() {
+ return CONTENT_TYPE.equalsIgnoreCase(name);
+ }
+
+ /**
+ * Determines if this is a <code>Subject</code> field.
+ *
+ * @return <code>true</code> if this is a <code>Subject</code> field,
+ * <code>false</code> otherwise.
+ */
+ public boolean isSubject() {
+ return SUBJECT.equalsIgnoreCase(name);
+ }
+
+ /**
+ * Determines if this is a <code>From</code> field.
+ *
+ * @return <code>true</code> if this is a <code>From</code> field,
+ * <code>false</code> otherwise.
+ */
+ public boolean isFrom() {
+ return FROM.equalsIgnoreCase(name);
+ }
+
+ /**
+ * Determines if this is a <code>To</code> field.
+ *
+ * @return <code>true</code> if this is a <code>To</code> field,
+ * <code>false</code> otherwise.
+ */
+ public boolean isTo() {
+ return TO.equalsIgnoreCase(name);
+ }
+
+ /**
+ * @see #getRaw()
+ */
+ public String toString() {
+ return raw;
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java
new file mode 100644
index 000000000..78aaf1334
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2006 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field;
+
+public interface FieldParser {
+
+ Field parse(final String name, final String body, final String raw);
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java
new file mode 100644
index 000000000..f15980055
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java
@@ -0,0 +1,70 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+import org.apache.james.mime4j.field.address.AddressList;
+import org.apache.james.mime4j.field.address.Mailbox;
+import org.apache.james.mime4j.field.address.MailboxList;
+import org.apache.james.mime4j.field.address.parser.ParseException;
+
+public class MailboxField extends Field {
+ private final Mailbox mailbox;
+ private final ParseException parseException;
+
+ protected MailboxField(final String name, final String body, final String raw, final Mailbox mailbox, final ParseException parseException) {
+ super(name, body, raw);
+ this.mailbox = mailbox;
+ this.parseException = parseException;
+ }
+
+ public Mailbox getMailbox() {
+ return mailbox;
+ }
+
+ public ParseException getParseException() {
+ return parseException;
+ }
+
+ public static class Parser implements FieldParser {
+ private static Log log = LogFactory.getLog(Parser.class);
+
+ public Field parse(final String name, final String body, final String raw) {
+ Mailbox mailbox = null;
+ ParseException parseException = null;
+ try {
+ MailboxList mailboxList = AddressList.parse(body).flatten();
+ if (mailboxList.size() > 0) {
+ mailbox = mailboxList.get(0);
+ }
+ }
+ catch (ParseException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Parsing value '" + body + "': "+ e.getMessage());
+ }
+ parseException = e;
+ }
+ return new MailboxField(name, body, raw, mailbox, parseException);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java
new file mode 100644
index 000000000..23378d4fa
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java
@@ -0,0 +1,67 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+import org.apache.james.mime4j.field.address.AddressList;
+import org.apache.james.mime4j.field.address.MailboxList;
+import org.apache.james.mime4j.field.address.parser.ParseException;
+
+public class MailboxListField extends Field {
+
+ private MailboxList mailboxList;
+ private ParseException parseException;
+
+ protected MailboxListField(final String name, final String body, final String raw, final MailboxList mailboxList, final ParseException parseException) {
+ super(name, body, raw);
+ this.mailboxList = mailboxList;
+ this.parseException = parseException;
+ }
+
+ public MailboxList getMailboxList() {
+ return mailboxList;
+ }
+
+ public ParseException getParseException() {
+ return parseException;
+ }
+
+ public static class Parser implements FieldParser {
+ private static Log log = LogFactory.getLog(Parser.class);
+
+ public Field parse(final String name, final String body, final String raw) {
+ MailboxList mailboxList = null;
+ ParseException parseException = null;
+ try {
+ mailboxList = AddressList.parse(body).flatten();
+ }
+ catch (ParseException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Parsing value '" + body + "': "+ e.getMessage());
+ }
+ parseException = e;
+ }
+ return new MailboxListField(name, body, raw, mailboxList, parseException);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java
new file mode 100644
index 000000000..6084e4435
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java
@@ -0,0 +1,49 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field;
+
+import org.apache.james.mime4j.decoder.DecoderUtil;
+
+
+/**
+ * Simple unstructured field such as <code>Subject</code>.
+ *
+ *
+ * @version $Id: UnstructuredField.java,v 1.3 2004/10/25 07:26:46 ntherning Exp $
+ */
+public class UnstructuredField extends Field {
+ private String value;
+
+ protected UnstructuredField(String name, String body, String raw, String value) {
+ super(name, body, raw);
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static class Parser implements FieldParser {
+ public Field parse(final String name, final String body, final String raw) {
+ final String value = DecoderUtil.decodeEncodedWords(body);
+ return new UnstructuredField(name, body, raw, value);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java
new file mode 100644
index 000000000..3e24e91aa
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java
@@ -0,0 +1,52 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import java.util.ArrayList;
+
+/**
+ * The abstract base for classes that represent RFC2822 addresses.
+ * This includes groups and mailboxes.
+ *
+ * Currently, no public methods are introduced on this class.
+ *
+ *
+ */
+public abstract class Address {
+
+ /**
+ * Adds any mailboxes represented by this address
+ * into the given ArrayList. Note that this method
+ * has default (package) access, so a doAddMailboxesTo
+ * method is needed to allow the behavior to be
+ * overridden by subclasses.
+ */
+ final void addMailboxesTo(ArrayList<Address> results) {
+ doAddMailboxesTo(results);
+ }
+
+ /**
+ * Adds any mailboxes represented by this address
+ * into the given ArrayList. Must be overridden by
+ * concrete subclasses.
+ */
+ protected abstract void doAddMailboxesTo(ArrayList<Address> results);
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java
new file mode 100644
index 000000000..1829e79aa
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java
@@ -0,0 +1,138 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import org.apache.james.mime4j.field.address.parser.AddressListParser;
+import org.apache.james.mime4j.field.address.parser.ParseException;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+
+/**
+ * An immutable, random-access list of Address objects.
+ *
+ *
+ */
+public class AddressList {
+
+ private ArrayList<Address> addresses;
+
+ /**
+ * @param addresses An ArrayList that contains only Address objects.
+ * @param dontCopy true iff it is not possible for the addresses ArrayList to be modified by someone else.
+ */
+ public AddressList(ArrayList<Address> addresses, boolean dontCopy) {
+ if (addresses != null)
+ this.addresses = (dontCopy ? addresses : new ArrayList<Address>(addresses));
+ else
+ this.addresses = new ArrayList<Address>(0);
+ }
+
+ /**
+ * The number of elements in this list.
+ */
+ public int size() {
+ return addresses.size();
+ }
+
+ /**
+ * Gets an address.
+ */
+ public Address get(int index) {
+ if (0 > index || size() <= index)
+ throw new IndexOutOfBoundsException();
+ return addresses.get(index);
+ }
+
+ /**
+ * Returns a flat list of all mailboxes represented
+ * in this address list. Use this if you don't care
+ * about grouping.
+ */
+ public MailboxList flatten() {
+ // in the common case, all addresses are mailboxes
+ boolean groupDetected = false;
+ for (int i = 0; i < size(); i++) {
+ if (!(get(i) instanceof Mailbox)) {
+ groupDetected = true;
+ break;
+ }
+ }
+
+ if (!groupDetected)
+ return new MailboxList(addresses, true);
+
+ ArrayList<Address> results = new ArrayList<Address>();
+ for (int i = 0; i < size(); i++) {
+ Address addr = get(i);
+ addr.addMailboxesTo(results);
+ }
+
+ // copy-on-construct this time, because subclasses
+ // could have held onto a reference to the results
+ return new MailboxList(results, false);
+ }
+
+ /**
+ * Dumps a representation of this address list to
+ * stdout, for debugging purposes.
+ */
+ public void print() {
+ for (int i = 0; i < size(); i++) {
+ Address addr = get(i);
+ System.out.println(addr.toString());
+ }
+ }
+
+ /**
+ * Parse the address list string, such as the value
+ * of a From, To, Cc, Bcc, Sender, or Reply-To
+ * header.
+ *
+ * The string MUST be unfolded already.
+ */
+ public static AddressList parse(String rawAddressList) throws ParseException {
+ AddressListParser parser = new AddressListParser(new StringReader(rawAddressList));
+ return Builder.getInstance().buildAddressList(parser.parse());
+ }
+
+ /**
+ * Test console.
+ */
+ public static void main(String[] args) throws Exception {
+ java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
+ while (true) {
+ try {
+ System.out.print("> ");
+ String line = reader.readLine();
+ if (line.length() == 0 || line.toLowerCase().equals("exit") || line.toLowerCase().equals("quit")) {
+ System.out.println("Goodbye.");
+ return;
+ }
+ AddressList list = parse(line);
+ list.print();
+ }
+ catch(Exception e) {
+ e.printStackTrace();
+ Thread.sleep(300);
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java
new file mode 100644
index 000000000..3bcd15b6f
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java
@@ -0,0 +1,243 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.apache.james.mime4j.decoder.DecoderUtil;
+import org.apache.james.mime4j.field.address.parser.ASTaddr_spec;
+import org.apache.james.mime4j.field.address.parser.ASTaddress;
+import org.apache.james.mime4j.field.address.parser.ASTaddress_list;
+import org.apache.james.mime4j.field.address.parser.ASTangle_addr;
+import org.apache.james.mime4j.field.address.parser.ASTdomain;
+import org.apache.james.mime4j.field.address.parser.ASTgroup_body;
+import org.apache.james.mime4j.field.address.parser.ASTlocal_part;
+import org.apache.james.mime4j.field.address.parser.ASTmailbox;
+import org.apache.james.mime4j.field.address.parser.ASTname_addr;
+import org.apache.james.mime4j.field.address.parser.ASTphrase;
+import org.apache.james.mime4j.field.address.parser.ASTroute;
+import org.apache.james.mime4j.field.address.parser.Node;
+import org.apache.james.mime4j.field.address.parser.SimpleNode;
+import org.apache.james.mime4j.field.address.parser.Token;
+
+/**
+ * Transforms the JJTree-generated abstract syntax tree
+ * into a graph of org.apache.james.mime4j.field.address objects.
+ *
+ *
+ */
+class Builder {
+
+ private static Builder singleton = new Builder();
+
+ public static Builder getInstance() {
+ return singleton;
+ }
+
+
+
+ public AddressList buildAddressList(ASTaddress_list node) {
+ ArrayList<Address> list = new ArrayList<Address>();
+ for (int i = 0; i < node.jjtGetNumChildren(); i++) {
+ ASTaddress childNode = (ASTaddress) node.jjtGetChild(i);
+ Address address = buildAddress(childNode);
+ list.add(address);
+ }
+ return new AddressList(list, true);
+ }
+
+ private Address buildAddress(ASTaddress node) {
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ Node n = it.nextNode();
+ if (n instanceof ASTaddr_spec) {
+ return buildAddrSpec((ASTaddr_spec)n);
+ }
+ else if (n instanceof ASTangle_addr) {
+ return buildAngleAddr((ASTangle_addr)n);
+ }
+ else if (n instanceof ASTphrase) {
+ String name = buildString((ASTphrase)n, false);
+ Node n2 = it.nextNode();
+ if (n2 instanceof ASTgroup_body) {
+ return new Group(name, buildGroupBody((ASTgroup_body)n2));
+ }
+ else if (n2 instanceof ASTangle_addr) {
+ name = DecoderUtil.decodeEncodedWords(name);
+ return new NamedMailbox(name, buildAngleAddr((ASTangle_addr)n2));
+ }
+ else {
+ throw new IllegalStateException();
+ }
+ }
+ else {
+ throw new IllegalStateException();
+ }
+ }
+
+
+
+ private MailboxList buildGroupBody(ASTgroup_body node) {
+ ArrayList<Address> results = new ArrayList<Address>();
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ while (it.hasNext()) {
+ Node n = it.nextNode();
+ if (n instanceof ASTmailbox)
+ results.add(buildMailbox((ASTmailbox)n));
+ else
+ throw new IllegalStateException();
+ }
+ return new MailboxList(results, true);
+ }
+
+ private Mailbox buildMailbox(ASTmailbox node) {
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ Node n = it.nextNode();
+ if (n instanceof ASTaddr_spec) {
+ return buildAddrSpec((ASTaddr_spec)n);
+ }
+ else if (n instanceof ASTangle_addr) {
+ return buildAngleAddr((ASTangle_addr)n);
+ }
+ else if (n instanceof ASTname_addr) {
+ return buildNameAddr((ASTname_addr)n);
+ }
+ else {
+ throw new IllegalStateException();
+ }
+ }
+
+ private NamedMailbox buildNameAddr(ASTname_addr node) {
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ Node n = it.nextNode();
+ String name;
+ if (n instanceof ASTphrase) {
+ name = buildString((ASTphrase)n, false);
+ }
+ else {
+ throw new IllegalStateException();
+ }
+
+ n = it.nextNode();
+ if (n instanceof ASTangle_addr) {
+ name = DecoderUtil.decodeEncodedWords(name);
+ return new NamedMailbox(name, buildAngleAddr((ASTangle_addr) n));
+ }
+ else {
+ throw new IllegalStateException();
+ }
+ }
+
+ private Mailbox buildAngleAddr(ASTangle_addr node) {
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ DomainList route = null;
+ Node n = it.nextNode();
+ if (n instanceof ASTroute) {
+ route = buildRoute((ASTroute)n);
+ n = it.nextNode();
+ }
+ else if (n instanceof ASTaddr_spec)
+ ; // do nothing
+ else
+ throw new IllegalStateException();
+
+ if (n instanceof ASTaddr_spec)
+ return buildAddrSpec(route, (ASTaddr_spec)n);
+ else
+ throw new IllegalStateException();
+ }
+
+ private DomainList buildRoute(ASTroute node) {
+ ArrayList<String> results = new ArrayList<String>(node.jjtGetNumChildren());
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ while (it.hasNext()) {
+ Node n = it.nextNode();
+ if (n instanceof ASTdomain)
+ results.add(buildString((ASTdomain)n, true));
+ else
+ throw new IllegalStateException();
+ }
+ return new DomainList(results, true);
+ }
+
+ private Mailbox buildAddrSpec(ASTaddr_spec node) {
+ return buildAddrSpec(null, node);
+ }
+ private Mailbox buildAddrSpec(DomainList route, ASTaddr_spec node) {
+ ChildNodeIterator it = new ChildNodeIterator(node);
+ String localPart = buildString((ASTlocal_part)it.nextNode(), true);
+ String domain = buildString((ASTdomain)it.nextNode(), true);
+ return new Mailbox(route, localPart, domain);
+ }
+
+
+ private String buildString(SimpleNode node, boolean stripSpaces) {
+ Token head = node.firstToken;
+ Token tail = node.lastToken;
+ StringBuffer out = new StringBuffer();
+
+ while (head != tail) {
+ out.append(head.image);
+ head = head.next;
+ if (!stripSpaces)
+ addSpecials(out, head.specialToken);
+ }
+ out.append(tail.image);
+
+ return out.toString();
+ }
+
+ private void addSpecials(StringBuffer out, Token specialToken) {
+ if (specialToken != null) {
+ addSpecials(out, specialToken.specialToken);
+ out.append(specialToken.image);
+ }
+ }
+
+ private static class ChildNodeIterator implements Iterator<Node> {
+
+ private SimpleNode simpleNode;
+ private int index;
+ private int len;
+
+ public ChildNodeIterator(SimpleNode simpleNode) {
+ this.simpleNode = simpleNode;
+ this.len = simpleNode.jjtGetNumChildren();
+ this.index = 0;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean hasNext() {
+ return index < len;
+ }
+
+ public Node next() {
+ return nextNode();
+ }
+
+ public Node nextNode() {
+ return simpleNode.jjtGetChild(index++);
+ }
+
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java
new file mode 100644
index 000000000..49b0f3be5
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java
@@ -0,0 +1,76 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import java.util.ArrayList;
+
+/**
+ * An immutable, random-access list of Strings (that
+ * are supposedly domain names or domain literals).
+ *
+ *
+ */
+public class DomainList {
+ private ArrayList<String> domains;
+
+ /**
+ * @param domains An ArrayList that contains only String objects.
+ * @param dontCopy true iff it is not possible for the domains ArrayList to be modified by someone else.
+ */
+ public DomainList(ArrayList<String> domains, boolean dontCopy) {
+ if (domains != null)
+ this.domains = (dontCopy ? domains : new ArrayList<String>(domains));
+ else
+ this.domains = new ArrayList<String>(0);
+ }
+
+ /**
+ * The number of elements in this list.
+ */
+ public int size() {
+ return domains.size();
+ }
+
+ /**
+ * Gets the domain name or domain literal at the
+ * specified index.
+ * @throws IndexOutOfBoundsException If index is &lt; 0 or &gt;= size().
+ */
+ public String get(int index) {
+ if (0 > index || size() <= index)
+ throw new IndexOutOfBoundsException();
+ return domains.get(index);
+ }
+
+ /**
+ * Returns the list of domains formatted as a route
+ * string (not including the trailing ':').
+ */
+ public String toRouteString() {
+ StringBuffer out = new StringBuffer();
+ for (int i = 0; i < domains.size(); i++) {
+ out.append("@");
+ out.append(get(i));
+ if (i + 1 < domains.size())
+ out.append(",");
+ }
+ return out.toString();
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java
new file mode 100644
index 000000000..c0ab7f724
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java
@@ -0,0 +1,75 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import java.util.ArrayList;
+
+/**
+ * A named group of zero or more mailboxes.
+ *
+ *
+ */
+public class Group extends Address {
+ private String name;
+ private MailboxList mailboxList;
+
+ /**
+ * @param name The group name.
+ * @param mailboxes The mailboxes in this group.
+ */
+ public Group(String name, MailboxList mailboxes) {
+ this.name = name;
+ this.mailboxList = mailboxes;
+ }
+
+ /**
+ * Returns the group name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the mailboxes in this group.
+ */
+ public MailboxList getMailboxes() {
+ return mailboxList;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer buf = new StringBuffer();
+ buf.append(name);
+ buf.append(":");
+ for (int i = 0; i < mailboxList.size(); i++) {
+ buf.append(mailboxList.get(i).toString());
+ if (i + 1 < mailboxList.size())
+ buf.append(",");
+ }
+ buf.append(";");
+ return buf.toString();
+ }
+
+ @Override
+ protected void doAddMailboxesTo(ArrayList<Address> results) {
+ for (int i = 0; i < mailboxList.size(); i++)
+ results.add(mailboxList.get(i));
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java
new file mode 100644
index 000000000..25f2548d4
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java
@@ -0,0 +1,121 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a single e-mail address.
+ *
+ *
+ */
+public class Mailbox extends Address {
+ private DomainList route;
+ private String localPart;
+ private String domain;
+
+ /**
+ * Creates a mailbox without a route. Routes are obsolete.
+ * @param localPart The part of the e-mail address to the left of the "@".
+ * @param domain The part of the e-mail address to the right of the "@".
+ */
+ public Mailbox(String localPart, String domain) {
+ this(null, localPart, domain);
+ }
+
+ /**
+ * Creates a mailbox with a route. Routes are obsolete.
+ * @param route The zero or more domains that make up the route. Can be null.
+ * @param localPart The part of the e-mail address to the left of the "@".
+ * @param domain The part of the e-mail address to the right of the "@".
+ */
+ public Mailbox(DomainList route, String localPart, String domain) {
+ this.route = route;
+ this.localPart = localPart;
+ this.domain = domain;
+ }
+
+ /**
+ * Returns the route list.
+ */
+ public DomainList getRoute() {
+ return route;
+ }
+
+ /**
+ * Returns the left part of the e-mail address
+ * (before "@").
+ */
+ public String getLocalPart() {
+ return localPart;
+ }
+
+ /**
+ * Returns the right part of the e-mail address
+ * (after "@").
+ */
+ public String getDomain() {
+ return domain;
+ }
+
+ /**
+ * Formats the address as a string, not including
+ * the route.
+ *
+ * @see #getAddressString(boolean)
+ */
+ public String getAddressString() {
+ return getAddressString(false);
+ }
+
+ /**
+ * Note that this value may not be usable
+ * for transport purposes, only display purposes.
+ *
+ * For example, if the unparsed address was
+ *
+ * <"Joe Cheng"@joecheng.com>
+ *
+ * this method would return
+ *
+ * <Joe Cheng@joecheng.com>
+ *
+ * which is not valid for transport; the local part
+ * would need to be re-quoted.
+ *
+ * @param includeRoute true if the route should be included if it exists.
+ */
+ public String getAddressString(boolean includeRoute) {
+ return "<" + (!includeRoute || route == null ? "" : route.toRouteString() + ":")
+ + localPart
+ + (domain == null ? "" : "@")
+ + domain + ">";
+ }
+
+ @Override
+ protected final void doAddMailboxesTo(ArrayList<Address> results) {
+ results.add(this);
+ }
+
+ @Override
+ public String toString() {
+ return getAddressString();
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java
new file mode 100644
index 000000000..2c9efb37f
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java
@@ -0,0 +1,71 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+import java.util.ArrayList;
+
+/**
+ * An immutable, random-access list of Mailbox objects.
+ *
+ *
+ */
+public class MailboxList {
+
+ private ArrayList<Address> mailboxes;
+
+ /**
+ * @param mailboxes An ArrayList that contains only Mailbox objects.
+ * @param dontCopy true iff it is not possible for the mailboxes ArrayList to be modified by someone else.
+ */
+ public MailboxList(ArrayList<Address> mailboxes, boolean dontCopy) {
+ if (mailboxes != null)
+ this.mailboxes = (dontCopy ? mailboxes : new ArrayList<Address>(mailboxes));
+ else
+ this.mailboxes = new ArrayList<Address>(0);
+ }
+
+ /**
+ * The number of elements in this list.
+ */
+ public int size() {
+ return mailboxes.size();
+ }
+
+ /**
+ * Gets an address.
+ */
+ public Mailbox get(int index) {
+ if (0 > index || size() <= index)
+ throw new IndexOutOfBoundsException();
+ return (Mailbox)mailboxes.get(index);
+ }
+
+ /**
+ * Dumps a representation of this mailbox list to
+ * stdout, for debugging purposes.
+ */
+ public void print() {
+ for (int i = 0; i < size(); i++) {
+ Mailbox mailbox = get(i);
+ System.out.println(mailbox.toString());
+ }
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java
new file mode 100644
index 000000000..4b8306037
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java
@@ -0,0 +1,71 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address;
+
+/**
+ * A Mailbox that has a name/description.
+ *
+ *
+ */
+public class NamedMailbox extends Mailbox {
+ private String name;
+
+ /**
+ * @see Mailbox#Mailbox(String, String)
+ */
+ public NamedMailbox(String name, String localPart, String domain) {
+ super(localPart, domain);
+ this.name = name;
+ }
+
+ /**
+ * @see Mailbox#Mailbox(DomainList, String, String)
+ */
+ public NamedMailbox(String name, DomainList route, String localPart, String domain) {
+ super(route, localPart, domain);
+ this.name = name;
+ }
+
+ /**
+ * Creates a named mailbox based on an unnamed mailbox.
+ */
+ public NamedMailbox(String name, Mailbox baseMailbox) {
+ super(baseMailbox.getRoute(), baseMailbox.getLocalPart(), baseMailbox.getDomain());
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of the mailbox.
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Same features (or problems) as Mailbox.getAddressString(boolean),
+ * only more so.
+ *
+ * @see Mailbox#getAddressString(boolean)
+ */
+ @Override
+ public String getAddressString(boolean includeRoute) {
+ return (name == null ? "" : name + " ") + super.getAddressString(includeRoute);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
new file mode 100644
index 000000000..4d56d000b
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTaddr_spec.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTaddr_spec extends SimpleNode {
+ public ASTaddr_spec(int id) {
+ super(id);
+ }
+
+ public ASTaddr_spec(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java
new file mode 100644
index 000000000..47bdeda8e
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTaddress.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTaddress extends SimpleNode {
+ public ASTaddress(int id) {
+ super(id);
+ }
+
+ public ASTaddress(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
new file mode 100644
index 000000000..737840e38
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTaddress_list.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTaddress_list extends SimpleNode {
+ public ASTaddress_list(int id) {
+ super(id);
+ }
+
+ public ASTaddress_list(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
new file mode 100644
index 000000000..8cb8f421f
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTangle_addr.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTangle_addr extends SimpleNode {
+ public ASTangle_addr(int id) {
+ super(id);
+ }
+
+ public ASTangle_addr(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java
new file mode 100644
index 000000000..b52664386
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTdomain.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTdomain extends SimpleNode {
+ public ASTdomain(int id) {
+ super(id);
+ }
+
+ public ASTdomain(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
new file mode 100644
index 000000000..f6017b9fc
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTgroup_body.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTgroup_body extends SimpleNode {
+ public ASTgroup_body(int id) {
+ super(id);
+ }
+
+ public ASTgroup_body(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
new file mode 100644
index 000000000..5c244fa3e
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTlocal_part.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTlocal_part extends SimpleNode {
+ public ASTlocal_part(int id) {
+ super(id);
+ }
+
+ public ASTlocal_part(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
new file mode 100644
index 000000000..aeb469da1
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTmailbox.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTmailbox extends SimpleNode {
+ public ASTmailbox(int id) {
+ super(id);
+ }
+
+ public ASTmailbox(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
new file mode 100644
index 000000000..846c73167
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTname_addr.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTname_addr extends SimpleNode {
+ public ASTname_addr(int id) {
+ super(id);
+ }
+
+ public ASTname_addr(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java
new file mode 100644
index 000000000..7d711c529
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTphrase.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTphrase extends SimpleNode {
+ public ASTphrase(int id) {
+ super(id);
+ }
+
+ public ASTphrase(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java
new file mode 100644
index 000000000..54ea11523
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. ASTroute.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class ASTroute extends SimpleNode {
+ public ASTroute(int id) {
+ super(id);
+ }
+
+ public ASTroute(AddressListParser p, int id) {
+ super(p, id);
+ }
+
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java
new file mode 100644
index 000000000..8094df0ad
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java
@@ -0,0 +1,977 @@
+/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParser.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants, AddressListParserConstants {/*@bgen(jjtree)*/
+ protected JJTAddressListParserState jjtree = new JJTAddressListParserState();public static void main(String args[]) throws ParseException {
+ while (true) {
+ try {
+ AddressListParser parser = new AddressListParser(System.in);
+ parser.parseLine();
+ ((SimpleNode)parser.jjtree.rootNode()).dump("> ");
+ } catch (Exception x) {
+ x.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ private static void log(String msg) {
+ System.out.print(msg);
+ }
+
+ public ASTaddress_list parse() throws ParseException {
+ try {
+ parseAll();
+ return (ASTaddress_list)jjtree.rootNode();
+ } catch (TokenMgrError tme) {
+ throw new ParseException(tme.getMessage());
+ }
+ }
+
+
+ void jjtreeOpenNodeScope(Node n) {
+ ((SimpleNode)n).firstToken = getToken(1);
+ }
+
+ void jjtreeCloseNodeScope(Node n) {
+ ((SimpleNode)n).lastToken = getToken(0);
+ }
+
+ final public void parseLine() throws ParseException {
+ address_list();
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 1:
+ jj_consume_token(1);
+ break;
+ default:
+ jj_la1[0] = jj_gen;
+ ;
+ }
+ jj_consume_token(2);
+ }
+
+ final public void parseAll() throws ParseException {
+ address_list();
+ jj_consume_token(0);
+ }
+
+ final public void address_list() throws ParseException {
+ /*@bgen(jjtree) address_list */
+ ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 6:
+ case DOTATOM:
+ case QUOTEDSTRING:
+ address();
+ break;
+ default:
+ jj_la1[1] = jj_gen;
+ ;
+ }
+ label_1:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 3:
+ ;
+ break;
+ default:
+ jj_la1[2] = jj_gen;
+ break label_1;
+ }
+ jj_consume_token(3);
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 6:
+ case DOTATOM:
+ case QUOTEDSTRING:
+ address();
+ break;
+ default:
+ jj_la1[3] = jj_gen;
+ ;
+ }
+ }
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void address() throws ParseException {
+ /*@bgen(jjtree) address */
+ ASTaddress jjtn000 = new ASTaddress(JJTADDRESS);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ if (jj_2_1(2147483647)) {
+ addr_spec();
+ } else {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 6:
+ angle_addr();
+ break;
+ case DOTATOM:
+ case QUOTEDSTRING:
+ phrase();
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 4:
+ group_body();
+ break;
+ case 6:
+ angle_addr();
+ break;
+ default:
+ jj_la1[4] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ break;
+ default:
+ jj_la1[5] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ }
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void mailbox() throws ParseException {
+ /*@bgen(jjtree) mailbox */
+ ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ if (jj_2_2(2147483647)) {
+ addr_spec();
+ } else {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 6:
+ angle_addr();
+ break;
+ case DOTATOM:
+ case QUOTEDSTRING:
+ name_addr();
+ break;
+ default:
+ jj_la1[6] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ }
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void name_addr() throws ParseException {
+ /*@bgen(jjtree) name_addr */
+ ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ phrase();
+ angle_addr();
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void group_body() throws ParseException {
+ /*@bgen(jjtree) group_body */
+ ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ jj_consume_token(4);
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 6:
+ case DOTATOM:
+ case QUOTEDSTRING:
+ mailbox();
+ break;
+ default:
+ jj_la1[7] = jj_gen;
+ ;
+ }
+ label_2:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 3:
+ ;
+ break;
+ default:
+ jj_la1[8] = jj_gen;
+ break label_2;
+ }
+ jj_consume_token(3);
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 6:
+ case DOTATOM:
+ case QUOTEDSTRING:
+ mailbox();
+ break;
+ default:
+ jj_la1[9] = jj_gen;
+ ;
+ }
+ }
+ jj_consume_token(5);
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void angle_addr() throws ParseException {
+ /*@bgen(jjtree) angle_addr */
+ ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ jj_consume_token(6);
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 8:
+ route();
+ break;
+ default:
+ jj_la1[10] = jj_gen;
+ ;
+ }
+ addr_spec();
+ jj_consume_token(7);
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void route() throws ParseException {
+ /*@bgen(jjtree) route */
+ ASTroute jjtn000 = new ASTroute(JJTROUTE);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ jj_consume_token(8);
+ domain();
+ label_3:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 3:
+ case 8:
+ ;
+ break;
+ default:
+ jj_la1[11] = jj_gen;
+ break label_3;
+ }
+ label_4:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 3:
+ ;
+ break;
+ default:
+ jj_la1[12] = jj_gen;
+ break label_4;
+ }
+ jj_consume_token(3);
+ }
+ jj_consume_token(8);
+ domain();
+ }
+ jj_consume_token(4);
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void phrase() throws ParseException {
+ /*@bgen(jjtree) phrase */
+ ASTphrase jjtn000 = new ASTphrase(JJTPHRASE);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ label_5:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case DOTATOM:
+ jj_consume_token(DOTATOM);
+ break;
+ case QUOTEDSTRING:
+ jj_consume_token(QUOTEDSTRING);
+ break;
+ default:
+ jj_la1[13] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case DOTATOM:
+ case QUOTEDSTRING:
+ ;
+ break;
+ default:
+ jj_la1[14] = jj_gen;
+ break label_5;
+ }
+ }
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void addr_spec() throws ParseException {
+ /*@bgen(jjtree) addr_spec */
+ ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+ try {
+ local_part();
+ jj_consume_token(8);
+ domain();
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ {if (true) throw (RuntimeException)jjte000;}
+ }
+ if (jjte000 instanceof ParseException) {
+ {if (true) throw (ParseException)jjte000;}
+ }
+ {if (true) throw (Error)jjte000;}
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void local_part() throws ParseException {
+ /*@bgen(jjtree) local_part */
+ ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);Token t;
+ try {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case DOTATOM:
+ t = jj_consume_token(DOTATOM);
+ break;
+ case QUOTEDSTRING:
+ t = jj_consume_token(QUOTEDSTRING);
+ break;
+ default:
+ jj_la1[15] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ label_6:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 9:
+ case DOTATOM:
+ case QUOTEDSTRING:
+ ;
+ break;
+ default:
+ jj_la1[16] = jj_gen;
+ break label_6;
+ }
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 9:
+ t = jj_consume_token(9);
+ break;
+ default:
+ jj_la1[17] = jj_gen;
+ ;
+ }
+ if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING)
+ {if (true) throw new ParseException("Words in local part must be separated by '.'");}
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case DOTATOM:
+ t = jj_consume_token(DOTATOM);
+ break;
+ case QUOTEDSTRING:
+ t = jj_consume_token(QUOTEDSTRING);
+ break;
+ default:
+ jj_la1[18] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ }
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final public void domain() throws ParseException {
+ /*@bgen(jjtree) domain */
+ ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);Token t;
+ try {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case DOTATOM:
+ t = jj_consume_token(DOTATOM);
+ label_7:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 9:
+ case DOTATOM:
+ ;
+ break;
+ default:
+ jj_la1[19] = jj_gen;
+ break label_7;
+ }
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 9:
+ t = jj_consume_token(9);
+ break;
+ default:
+ jj_la1[20] = jj_gen;
+ ;
+ }
+ if (t.image.charAt(t.image.length() - 1) != '.')
+ {if (true) throw new ParseException("Atoms in domain names must be separated by '.'");}
+ t = jj_consume_token(DOTATOM);
+ }
+ break;
+ case DOMAINLITERAL:
+ jj_consume_token(DOMAINLITERAL);
+ break;
+ default:
+ jj_la1[21] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+ }
+
+ final private boolean jj_2_1(int xla) {
+ jj_la = xla; jj_lastpos = jj_scanpos = token;
+ try { return !jj_3_1(); }
+ catch(LookaheadSuccess ls) { return true; }
+ finally { jj_save(0, xla); }
+ }
+
+ final private boolean jj_2_2(int xla) {
+ jj_la = xla; jj_lastpos = jj_scanpos = token;
+ try { return !jj_3_2(); }
+ catch(LookaheadSuccess ls) { return true; }
+ finally { jj_save(1, xla); }
+ }
+
+ final private boolean jj_3R_11() {
+ Token xsp;
+ xsp = jj_scanpos;
+ if (jj_scan_token(9)) jj_scanpos = xsp;
+ xsp = jj_scanpos;
+ if (jj_scan_token(14)) {
+ jj_scanpos = xsp;
+ if (jj_scan_token(31)) return true;
+ }
+ return false;
+ }
+
+ final private boolean jj_3R_13() {
+ Token xsp;
+ xsp = jj_scanpos;
+ if (jj_scan_token(9)) jj_scanpos = xsp;
+ if (jj_scan_token(DOTATOM)) return true;
+ return false;
+ }
+
+ final private boolean jj_3R_8() {
+ if (jj_3R_9()) return true;
+ if (jj_scan_token(8)) return true;
+ if (jj_3R_10()) return true;
+ return false;
+ }
+
+ final private boolean jj_3_1() {
+ if (jj_3R_8()) return true;
+ return false;
+ }
+
+ final private boolean jj_3R_12() {
+ if (jj_scan_token(DOTATOM)) return true;
+ Token xsp;
+ while (true) {
+ xsp = jj_scanpos;
+ if (jj_3R_13()) { jj_scanpos = xsp; break; }
+ }
+ return false;
+ }
+
+ final private boolean jj_3R_10() {
+ Token xsp;
+ xsp = jj_scanpos;
+ if (jj_3R_12()) {
+ jj_scanpos = xsp;
+ if (jj_scan_token(18)) return true;
+ }
+ return false;
+ }
+
+ final private boolean jj_3_2() {
+ if (jj_3R_8()) return true;
+ return false;
+ }
+
+ final private boolean jj_3R_9() {
+ Token xsp;
+ xsp = jj_scanpos;
+ if (jj_scan_token(14)) {
+ jj_scanpos = xsp;
+ if (jj_scan_token(31)) return true;
+ }
+ while (true) {
+ xsp = jj_scanpos;
+ if (jj_3R_11()) { jj_scanpos = xsp; break; }
+ }
+ return false;
+ }
+
+ public AddressListParserTokenManager token_source;
+ SimpleCharStream jj_input_stream;
+ public Token token, jj_nt;
+ private int jj_ntk;
+ private Token jj_scanpos, jj_lastpos;
+ private int jj_la;
+ public boolean lookingAhead = false;
+ private boolean jj_semLA;
+ private int jj_gen;
+ final private int[] jj_la1 = new int[22];
+ static private int[] jj_la1_0;
+ static private int[] jj_la1_1;
+ static {
+ jj_la1_0();
+ jj_la1_1();
+ }
+ private static void jj_la1_0() {
+ jj_la1_0 = new int[] {0x2,0x80004040,0x8,0x80004040,0x50,0x80004040,0x80004040,0x80004040,0x8,0x80004040,0x100,0x108,0x8,0x80004000,0x80004000,0x80004000,0x80004200,0x200,0x80004000,0x4200,0x200,0x44000,};
+ }
+ private static void jj_la1_1() {
+ jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,};
+ }
+ final private JJCalls[] jj_2_rtns = new JJCalls[2];
+ private boolean jj_rescan = false;
+ private int jj_gc = 0;
+
+ public AddressListParser(java.io.InputStream stream) {
+ this(stream, null);
+ }
+ public AddressListParser(java.io.InputStream stream, String encoding) {
+ try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+ token_source = new AddressListParserTokenManager(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 22; i++) jj_la1[i] = -1;
+ for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
+ }
+
+ public void ReInit(java.io.InputStream stream) {
+ ReInit(stream, null);
+ }
+ public void ReInit(java.io.InputStream stream, String encoding) {
+ try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+ token_source.ReInit(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jjtree.reset();
+ jj_gen = 0;
+ for (int i = 0; i < 22; i++) jj_la1[i] = -1;
+ for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
+ }
+
+ public AddressListParser(java.io.Reader stream) {
+ jj_input_stream = new SimpleCharStream(stream, 1, 1);
+ token_source = new AddressListParserTokenManager(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 22; i++) jj_la1[i] = -1;
+ for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
+ }
+
+ public void ReInit(java.io.Reader stream) {
+ jj_input_stream.ReInit(stream, 1, 1);
+ token_source.ReInit(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jjtree.reset();
+ jj_gen = 0;
+ for (int i = 0; i < 22; i++) jj_la1[i] = -1;
+ for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
+ }
+
+ public AddressListParser(AddressListParserTokenManager tm) {
+ token_source = tm;
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 22; i++) jj_la1[i] = -1;
+ for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
+ }
+
+ public void ReInit(AddressListParserTokenManager tm) {
+ token_source = tm;
+ token = new Token();
+ jj_ntk = -1;
+ jjtree.reset();
+ jj_gen = 0;
+ for (int i = 0; i < 22; i++) jj_la1[i] = -1;
+ for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls();
+ }
+
+ final private Token jj_consume_token(int kind) throws ParseException {
+ Token oldToken;
+ if ((oldToken = token).next != null) token = token.next;
+ else token = token.next = token_source.getNextToken();
+ jj_ntk = -1;
+ if (token.kind == kind) {
+ jj_gen++;
+ if (++jj_gc > 100) {
+ jj_gc = 0;
+ for (int i = 0; i < jj_2_rtns.length; i++) {
+ JJCalls c = jj_2_rtns[i];
+ while (c != null) {
+ if (c.gen < jj_gen) c.first = null;
+ c = c.next;
+ }
+ }
+ }
+ return token;
+ }
+ token = oldToken;
+ jj_kind = kind;
+ throw generateParseException();
+ }
+
+ static private final class LookaheadSuccess extends java.lang.Error { }
+ final private LookaheadSuccess jj_ls = new LookaheadSuccess();
+ final private boolean jj_scan_token(int kind) {
+ if (jj_scanpos == jj_lastpos) {
+ jj_la--;
+ if (jj_scanpos.next == null) {
+ jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken();
+ } else {
+ jj_lastpos = jj_scanpos = jj_scanpos.next;
+ }
+ } else {
+ jj_scanpos = jj_scanpos.next;
+ }
+ if (jj_rescan) {
+ int i = 0; Token tok = token;
+ while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; }
+ if (tok != null) jj_add_error_token(kind, i);
+ }
+ if (jj_scanpos.kind != kind) return true;
+ if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls;
+ return false;
+ }
+
+ final public Token getNextToken() {
+ if (token.next != null) token = token.next;
+ else token = token.next = token_source.getNextToken();
+ jj_ntk = -1;
+ jj_gen++;
+ return token;
+ }
+
+ final public Token getToken(int index) {
+ Token t = lookingAhead ? jj_scanpos : token;
+ for (int i = 0; i < index; i++) {
+ if (t.next != null) t = t.next;
+ else t = t.next = token_source.getNextToken();
+ }
+ return t;
+ }
+
+ final private int jj_ntk() {
+ if ((jj_nt=token.next) == null)
+ return (jj_ntk = (token.next=token_source.getNextToken()).kind);
+ else
+ return (jj_ntk = jj_nt.kind);
+ }
+
+ private java.util.Vector<int[]> jj_expentries = new java.util.Vector<int[]>();
+ private int[] jj_expentry;
+ private int jj_kind = -1;
+ private int[] jj_lasttokens = new int[100];
+ private int jj_endpos;
+
+ private void jj_add_error_token(int kind, int pos) {
+ if (pos >= 100) return;
+ if (pos == jj_endpos + 1) {
+ jj_lasttokens[jj_endpos++] = kind;
+ } else if (jj_endpos != 0) {
+ jj_expentry = new int[jj_endpos];
+ for (int i = 0; i < jj_endpos; i++) {
+ jj_expentry[i] = jj_lasttokens[i];
+ }
+ boolean exists = false;
+ for (java.util.Enumeration<int[]> e = jj_expentries.elements(); e.hasMoreElements();) {
+ int[] oldentry = e.nextElement();
+ if (oldentry.length == jj_expentry.length) {
+ exists = true;
+ for (int i = 0; i < jj_expentry.length; i++) {
+ if (oldentry[i] != jj_expentry[i]) {
+ exists = false;
+ break;
+ }
+ }
+ if (exists) break;
+ }
+ }
+ if (!exists) jj_expentries.addElement(jj_expentry);
+ if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind;
+ }
+ }
+
+ public ParseException generateParseException() {
+ jj_expentries.removeAllElements();
+ boolean[] la1tokens = new boolean[34];
+ for (int i = 0; i < 34; i++) {
+ la1tokens[i] = false;
+ }
+ if (jj_kind >= 0) {
+ la1tokens[jj_kind] = true;
+ jj_kind = -1;
+ }
+ for (int i = 0; i < 22; i++) {
+ if (jj_la1[i] == jj_gen) {
+ for (int j = 0; j < 32; j++) {
+ if ((jj_la1_0[i] & (1<<j)) != 0) {
+ la1tokens[j] = true;
+ }
+ if ((jj_la1_1[i] & (1<<j)) != 0) {
+ la1tokens[32+j] = true;
+ }
+ }
+ }
+ }
+ for (int i = 0; i < 34; i++) {
+ if (la1tokens[i]) {
+ jj_expentry = new int[1];
+ jj_expentry[0] = i;
+ jj_expentries.addElement(jj_expentry);
+ }
+ }
+ jj_endpos = 0;
+ jj_rescan_token();
+ jj_add_error_token(0, 0);
+ int[][] exptokseq = new int[jj_expentries.size()][];
+ for (int i = 0; i < jj_expentries.size(); i++) {
+ exptokseq[i] = jj_expentries.elementAt(i);
+ }
+ return new ParseException(token, exptokseq, tokenImage);
+ }
+
+ final public void enable_tracing() {
+ }
+
+ final public void disable_tracing() {
+ }
+
+ final private void jj_rescan_token() {
+ jj_rescan = true;
+ for (int i = 0; i < 2; i++) {
+ try {
+ JJCalls p = jj_2_rtns[i];
+ do {
+ if (p.gen > jj_gen) {
+ jj_la = p.arg; jj_lastpos = jj_scanpos = p.first;
+ switch (i) {
+ case 0: jj_3_1(); break;
+ case 1: jj_3_2(); break;
+ }
+ }
+ p = p.next;
+ } while (p != null);
+ } catch(LookaheadSuccess ls) { }
+ }
+ jj_rescan = false;
+ }
+
+ final private void jj_save(int index, int xla) {
+ JJCalls p = jj_2_rtns[index];
+ while (p.gen > jj_gen) {
+ if (p.next == null) { p = p.next = new JJCalls(); break; }
+ p = p.next;
+ }
+ p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla;
+ }
+
+ static final class JJCalls {
+ int gen;
+ Token first;
+ int arg;
+ JJCalls next;
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
new file mode 100644
index 000000000..c14277bc6
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj
@@ -0,0 +1,595 @@
+/*@bgen(jjtree) Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParser.jj */
+/*@egen*//****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+
+/**
+ * RFC2822 address list parser.
+ *
+ * Created 9/17/2004
+ * by Joe Cheng <code@joecheng.com>
+ */
+
+options {
+ STATIC=false;
+ LOOKAHEAD=1;
+ //DEBUG_PARSER=true;
+ //DEBUG_TOKEN_MANAGER=true;
+}
+
+PARSER_BEGIN(AddressListParser)
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants/*@egen*/ {/*@bgen(jjtree)*/
+ protected JJTAddressListParserState jjtree = new JJTAddressListParserState();
+
+/*@egen*/
+ public static void main(String args[]) throws ParseException {
+ while (true) {
+ try {
+ AddressListParser parser = new AddressListParser(System.in);
+ parser.parseLine();
+ ((SimpleNode)parser.jjtree.rootNode()).dump("> ");
+ } catch (Exception x) {
+ x.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ private static void log(String msg) {
+ System.out.print(msg);
+ }
+
+ public ASTaddress_list parse() throws ParseException {
+ try {
+ parseAll();
+ return (ASTaddress_list)jjtree.rootNode();
+ } catch (TokenMgrError tme) {
+ throw new ParseException(tme.getMessage());
+ }
+ }
+
+
+ void jjtreeOpenNodeScope(Node n) {
+ ((SimpleNode)n).firstToken = getToken(1);
+ }
+
+ void jjtreeCloseNodeScope(Node n) {
+ ((SimpleNode)n).lastToken = getToken(0);
+ }
+}
+
+PARSER_END(AddressListParser)
+
+void parseLine() :
+{}
+{
+ address_list() ["\r"] "\n"
+}
+
+void parseAll() :
+{}
+{
+ address_list() <EOF>
+}
+
+void address_list() :
+{/*@bgen(jjtree) address_list */
+ ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) address_list */
+ try {
+/*@egen*/
+ [ address() ]
+ (
+ ","
+ [ address() ]
+ )*/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void address() :
+{/*@bgen(jjtree) address */
+ ASTaddress jjtn000 = new ASTaddress(JJTADDRESS);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) address */
+ try {
+/*@egen*/
+ LOOKAHEAD(2147483647)
+ addr_spec()
+| angle_addr()
+| ( phrase() (group_body() | angle_addr()) )/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void mailbox() :
+{/*@bgen(jjtree) mailbox */
+ ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) mailbox */
+ try {
+/*@egen*/
+ LOOKAHEAD(2147483647)
+ addr_spec()
+| angle_addr()
+| name_addr()/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void name_addr() :
+{/*@bgen(jjtree) name_addr */
+ ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) name_addr */
+ try {
+/*@egen*/
+ phrase() angle_addr()/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void group_body() :
+{/*@bgen(jjtree) group_body */
+ ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) group_body */
+ try {
+/*@egen*/
+ ":"
+ [ mailbox() ]
+ (
+ ","
+ [ mailbox() ]
+ )*
+ ";"/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void angle_addr() :
+{/*@bgen(jjtree) angle_addr */
+ ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) angle_addr */
+ try {
+/*@egen*/
+ "<" [ route() ] addr_spec() ">"/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void route() :
+{/*@bgen(jjtree) route */
+ ASTroute jjtn000 = new ASTroute(JJTROUTE);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) route */
+ try {
+/*@egen*/
+ "@" domain() ( (",")* "@" domain() )* ":"/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void phrase() :
+{/*@bgen(jjtree) phrase */
+ ASTphrase jjtn000 = new ASTphrase(JJTPHRASE);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) phrase */
+try {
+/*@egen*/
+( <DOTATOM>
+| <QUOTEDSTRING>
+)+/*@bgen(jjtree)*/
+} finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+}
+/*@egen*/
+}
+
+void addr_spec() :
+{/*@bgen(jjtree) addr_spec */
+ ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/}
+{/*@bgen(jjtree) addr_spec */
+ try {
+/*@egen*/
+ ( local_part() "@" domain() )/*@bgen(jjtree)*/
+ } catch (Throwable jjte000) {
+ if (jjtc000) {
+ jjtree.clearNodeScope(jjtn000);
+ jjtc000 = false;
+ } else {
+ jjtree.popNode();
+ }
+ if (jjte000 instanceof RuntimeException) {
+ throw (RuntimeException)jjte000;
+ }
+ if (jjte000 instanceof ParseException) {
+ throw (ParseException)jjte000;
+ }
+ throw (Error)jjte000;
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void local_part() :
+{/*@bgen(jjtree) local_part */
+ ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/ Token t; }
+{/*@bgen(jjtree) local_part */
+ try {
+/*@egen*/
+ ( t=<DOTATOM> | t=<QUOTEDSTRING> )
+ ( [t="."]
+ {
+ if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING)
+ throw new ParseException("Words in local part must be separated by '.'");
+ }
+ ( t=<DOTATOM> | t=<QUOTEDSTRING> )
+ )*/*@bgen(jjtree)*/
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+void domain() :
+{/*@bgen(jjtree) domain */
+ ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN);
+ boolean jjtc000 = true;
+ jjtree.openNodeScope(jjtn000);
+ jjtreeOpenNodeScope(jjtn000);
+/*@egen*/ Token t; }
+{/*@bgen(jjtree) domain */
+ try {
+/*@egen*/
+ ( t=<DOTATOM>
+ ( [t="."]
+ {
+ if (t.image.charAt(t.image.length() - 1) != '.')
+ throw new ParseException("Atoms in domain names must be separated by '.'");
+ }
+ t=<DOTATOM>
+ )*
+ )
+| <DOMAINLITERAL>/*@bgen(jjtree)*/
+ } finally {
+ if (jjtc000) {
+ jjtree.closeNodeScope(jjtn000, true);
+ jjtreeCloseNodeScope(jjtn000);
+ }
+ }
+/*@egen*/
+}
+
+SPECIAL_TOKEN :
+{
+ < WS: ( [" ", "\t"] )+ >
+}
+
+TOKEN :
+{
+ < #ALPHA: ["a" - "z", "A" - "Z"] >
+| < #DIGIT: ["0" - "9"] >
+| < #ATEXT: ( <ALPHA> | <DIGIT>
+ | "!" | "#" | "$" | "%"
+ | "&" | "'" | "*" | "+"
+ | "-" | "/" | "=" | "?"
+ | "^" | "_" | "`" | "{"
+ | "|" | "}" | "~"
+ )>
+| < DOTATOM: <ATEXT> ( <ATEXT> | "." )* >
+}
+
+TOKEN_MGR_DECLS :
+{
+ // Keeps track of how many levels of comment nesting
+ // we've encountered. This is only used when the 2nd
+ // level is reached, for example ((this)), not (this).
+ // This is because the outermost level must be treated
+ // specially anyway, because the outermost ")" has a
+ // different token type than inner ")" instances.
+ static int commentNest;
+}
+
+MORE :
+{
+ // domain literal
+ "[" : INDOMAINLITERAL
+}
+
+<INDOMAINLITERAL>
+MORE :
+{
+ < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
+| < ~["[", "]", "\\"] >
+}
+
+<INDOMAINLITERAL>
+TOKEN :
+{
+ < DOMAINLITERAL: "]" > { matchedToken.image = image.toString(); }: DEFAULT
+}
+
+MORE :
+{
+ // starts a comment
+ "(" : INCOMMENT
+}
+
+<INCOMMENT>
+SKIP :
+{
+ // ends a comment
+ < COMMENT: ")" > : DEFAULT
+ // if this is ever changed to not be a SKIP, need
+ // to make sure matchedToken.token = token.toString()
+ // is called.
+}
+
+<INCOMMENT>
+MORE :
+{
+ < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
+| "(" { commentNest = 1; } : NESTED_COMMENT
+| < <ANY>>
+}
+
+<NESTED_COMMENT>
+MORE :
+{
+ < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
+| "(" { ++commentNest; }
+| ")" { --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); }
+| < <ANY>>
+}
+
+
+// QUOTED STRINGS
+
+MORE :
+{
+ "\"" { image.deleteCharAt(image.length() - 1); } : INQUOTEDSTRING
+}
+
+<INQUOTEDSTRING>
+MORE :
+{
+ < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); }
+| < (~["\"", "\\"])+ >
+}
+
+<INQUOTEDSTRING>
+TOKEN :
+{
+ < QUOTEDSTRING: "\"" > { matchedToken.image = image.substring(0, image.length() - 1); } : DEFAULT
+}
+
+// GLOBALS
+
+<*>
+TOKEN :
+{
+ < #QUOTEDPAIR: "\\" <ANY> >
+| < #ANY: ~[] >
+}
+
+// ERROR!
+/*
+
+<*>
+TOKEN :
+{
+ < UNEXPECTED_CHAR: <ANY> >
+}
+
+*/ \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
new file mode 100644
index 000000000..006a082c1
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java
@@ -0,0 +1,76 @@
+/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserConstants.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+public interface AddressListParserConstants {
+
+ int EOF = 0;
+ int WS = 10;
+ int ALPHA = 11;
+ int DIGIT = 12;
+ int ATEXT = 13;
+ int DOTATOM = 14;
+ int DOMAINLITERAL = 18;
+ int COMMENT = 20;
+ int QUOTEDSTRING = 31;
+ int QUOTEDPAIR = 32;
+ int ANY = 33;
+
+ int DEFAULT = 0;
+ int INDOMAINLITERAL = 1;
+ int INCOMMENT = 2;
+ int NESTED_COMMENT = 3;
+ int INQUOTEDSTRING = 4;
+
+ String[] tokenImage = {
+ "<EOF>",
+ "\"\\r\"",
+ "\"\\n\"",
+ "\",\"",
+ "\":\"",
+ "\";\"",
+ "\"<\"",
+ "\">\"",
+ "\"@\"",
+ "\".\"",
+ "<WS>",
+ "<ALPHA>",
+ "<DIGIT>",
+ "<ATEXT>",
+ "<DOTATOM>",
+ "\"[\"",
+ "<token of kind 16>",
+ "<token of kind 17>",
+ "\"]\"",
+ "\"(\"",
+ "\")\"",
+ "<token of kind 21>",
+ "\"(\"",
+ "<token of kind 23>",
+ "<token of kind 24>",
+ "\"(\"",
+ "\")\"",
+ "<token of kind 27>",
+ "\"\\\"\"",
+ "<token of kind 29>",
+ "<token of kind 30>",
+ "\"\\\"\"",
+ "<QUOTEDPAIR>",
+ "<ANY>",
+ };
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
new file mode 100644
index 000000000..d2dd88dd3
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java
@@ -0,0 +1,1009 @@
+/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserTokenManager.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+public class AddressListParserTokenManager implements AddressListParserConstants
+{
+ // Keeps track of how many levels of comment nesting
+ // we've encountered. This is only used when the 2nd
+ // level is reached, for example ((this)), not (this).
+ // This is because the outermost level must be treated
+ // specially anyway, because the outermost ")" has a
+ // different token type than inner ")" instances.
+ static int commentNest;
+ public java.io.PrintStream debugStream = System.out;
+ public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
+private final int jjStopStringLiteralDfa_0(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_0(int pos, long active0)
+{
+ return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
+}
+private final int jjStopAtPos(int pos, int kind)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ return pos + 1;
+}
+private final int jjStartNfaWithStates_0(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_0(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_0()
+{
+ switch(curChar)
+ {
+ case 10:
+ return jjStopAtPos(0, 2);
+ case 13:
+ return jjStopAtPos(0, 1);
+ case 34:
+ return jjStopAtPos(0, 28);
+ case 40:
+ return jjStopAtPos(0, 19);
+ case 44:
+ return jjStopAtPos(0, 3);
+ case 46:
+ return jjStopAtPos(0, 9);
+ case 58:
+ return jjStopAtPos(0, 4);
+ case 59:
+ return jjStopAtPos(0, 5);
+ case 60:
+ return jjStopAtPos(0, 6);
+ case 62:
+ return jjStopAtPos(0, 7);
+ case 64:
+ return jjStopAtPos(0, 8);
+ case 91:
+ return jjStopAtPos(0, 15);
+ default :
+ return jjMoveNfa_0(1, 0);
+ }
+}
+private final void jjCheckNAdd(int state)
+{
+ if (jjrounds[state] != jjround)
+ {
+ jjstateSet[jjnewStateCnt++] = state;
+ jjrounds[state] = jjround;
+ }
+}
+private final void jjAddStates(int start, int end)
+{
+ do {
+ jjstateSet[jjnewStateCnt++] = jjnextStates[start];
+ } while (start++ != end);
+}
+private final void jjCheckNAddTwoStates(int state1, int state2)
+{
+ jjCheckNAdd(state1);
+ jjCheckNAdd(state2);
+}
+private final void jjCheckNAddStates(int start, int end)
+{
+ do {
+ jjCheckNAdd(jjnextStates[start]);
+ } while (start++ != end);
+}
+private final void jjCheckNAddStates(int start)
+{
+ jjCheckNAdd(jjnextStates[start]);
+ jjCheckNAdd(jjnextStates[start + 1]);
+}
+private final int jjMoveNfa_0(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 1:
+ if ((0xa3ffacfa00000000L & l) != 0L)
+ {
+ if (kind > 14)
+ kind = 14;
+ jjCheckNAdd(2);
+ }
+ else if ((0x100000200L & l) != 0L)
+ {
+ if (kind > 10)
+ kind = 10;
+ jjCheckNAdd(0);
+ }
+ break;
+ case 0:
+ if ((0x100000200L & l) == 0L)
+ break;
+ kind = 10;
+ jjCheckNAdd(0);
+ break;
+ case 2:
+ if ((0xa3ffecfa00000000L & l) == 0L)
+ break;
+ if (kind > 14)
+ kind = 14;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 1:
+ case 2:
+ if ((0x7fffffffc7fffffeL & l) == 0L)
+ break;
+ if (kind > 14)
+ kind = 14;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_2(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_2(int pos, long active0)
+{
+ return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_2(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_2(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_2()
+{
+ switch(curChar)
+ {
+ case 40:
+ return jjStopAtPos(0, 22);
+ case 41:
+ return jjStopAtPos(0, 20);
+ default :
+ return jjMoveNfa_2(0, 0);
+ }
+}
+static final long[] jjbitVec0 = {
+ 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL
+};
+private final int jjMoveNfa_2(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 23)
+ kind = 23;
+ break;
+ case 1:
+ if (kind > 21)
+ kind = 21;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 23)
+ kind = 23;
+ if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 21)
+ kind = 21;
+ break;
+ case 2:
+ if (kind > 23)
+ kind = 23;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 23)
+ kind = 23;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 21)
+ kind = 21;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_4(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_4(int pos, long active0)
+{
+ return jjMoveNfa_4(jjStopStringLiteralDfa_4(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_4(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_4(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_4()
+{
+ switch(curChar)
+ {
+ case 34:
+ return jjStopAtPos(0, 31);
+ default :
+ return jjMoveNfa_4(0, 0);
+ }
+}
+private final int jjMoveNfa_4(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ case 2:
+ if ((0xfffffffbffffffffL & l) == 0L)
+ break;
+ if (kind > 30)
+ kind = 30;
+ jjCheckNAdd(2);
+ break;
+ case 1:
+ if (kind > 29)
+ kind = 29;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((0xffffffffefffffffL & l) != 0L)
+ {
+ if (kind > 30)
+ kind = 30;
+ jjCheckNAdd(2);
+ }
+ else if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 29)
+ kind = 29;
+ break;
+ case 2:
+ if ((0xffffffffefffffffL & l) == 0L)
+ break;
+ if (kind > 30)
+ kind = 30;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ case 2:
+ if ((jjbitVec0[i2] & l2) == 0L)
+ break;
+ if (kind > 30)
+ kind = 30;
+ jjCheckNAdd(2);
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 29)
+ kind = 29;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_3(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_3(int pos, long active0)
+{
+ return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_3(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_3(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_3()
+{
+ switch(curChar)
+ {
+ case 40:
+ return jjStopAtPos(0, 25);
+ case 41:
+ return jjStopAtPos(0, 26);
+ default :
+ return jjMoveNfa_3(0, 0);
+ }
+}
+private final int jjMoveNfa_3(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 27)
+ kind = 27;
+ break;
+ case 1:
+ if (kind > 24)
+ kind = 24;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 27)
+ kind = 27;
+ if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 24)
+ kind = 24;
+ break;
+ case 2:
+ if (kind > 27)
+ kind = 27;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 27)
+ kind = 27;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 24)
+ kind = 24;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_1(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_1(int pos, long active0)
+{
+ return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_1(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_1(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_1()
+{
+ switch(curChar)
+ {
+ case 93:
+ return jjStopAtPos(0, 18);
+ default :
+ return jjMoveNfa_1(0, 0);
+ }
+}
+private final int jjMoveNfa_1(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 17)
+ kind = 17;
+ break;
+ case 1:
+ if (kind > 16)
+ kind = 16;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((0xffffffffc7ffffffL & l) != 0L)
+ {
+ if (kind > 17)
+ kind = 17;
+ }
+ else if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 16)
+ kind = 16;
+ break;
+ case 2:
+ if ((0xffffffffc7ffffffL & l) != 0L && kind > 17)
+ kind = 17;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 17)
+ kind = 17;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 16)
+ kind = 16;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+static final int[] jjnextStates = {
+};
+public static final String[] jjstrLiteralImages = {
+"", "\15", "\12", "\54", "\72", "\73", "\74", "\76", "\100", "\56", null, null,
+null, null, null, null, null, null, null, null, null, null, null, null, null, null,
+null, null, null, null, null, null, null, null, };
+public static final String[] lexStateNames = {
+ "DEFAULT",
+ "INDOMAINLITERAL",
+ "INCOMMENT",
+ "NESTED_COMMENT",
+ "INQUOTEDSTRING",
+};
+public static final int[] jjnewLexState = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, 0, 2, 0, -1, 3, -1, -1,
+ -1, -1, -1, 4, -1, -1, 0, -1, -1,
+};
+static final long[] jjtoToken = {
+ 0x800443ffL,
+};
+static final long[] jjtoSkip = {
+ 0x100400L,
+};
+static final long[] jjtoSpecial = {
+ 0x400L,
+};
+static final long[] jjtoMore = {
+ 0x7feb8000L,
+};
+protected SimpleCharStream input_stream;
+private final int[] jjrounds = new int[3];
+private final int[] jjstateSet = new int[6];
+StringBuffer image;
+int jjimageLen;
+int lengthOfMatch;
+protected char curChar;
+public AddressListParserTokenManager(SimpleCharStream stream){
+ if (SimpleCharStream.staticFlag)
+ throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
+ input_stream = stream;
+}
+public AddressListParserTokenManager(SimpleCharStream stream, int lexState){
+ this(stream);
+ SwitchTo(lexState);
+}
+public void ReInit(SimpleCharStream stream)
+{
+ jjmatchedPos = jjnewStateCnt = 0;
+ curLexState = defaultLexState;
+ input_stream = stream;
+ ReInitRounds();
+}
+private final void ReInitRounds()
+{
+ int i;
+ jjround = 0x80000001;
+ for (i = 3; i-- > 0;)
+ jjrounds[i] = 0x80000000;
+}
+public void ReInit(SimpleCharStream stream, int lexState)
+{
+ ReInit(stream);
+ SwitchTo(lexState);
+}
+public void SwitchTo(int lexState)
+{
+ if (lexState >= 5 || lexState < 0)
+ throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
+ else
+ curLexState = lexState;
+}
+
+protected Token jjFillToken()
+{
+ Token t = Token.newToken(jjmatchedKind);
+ t.kind = jjmatchedKind;
+ String im = jjstrLiteralImages[jjmatchedKind];
+ t.image = (im == null) ? input_stream.GetImage() : im;
+ t.beginLine = input_stream.getBeginLine();
+ t.beginColumn = input_stream.getBeginColumn();
+ t.endLine = input_stream.getEndLine();
+ t.endColumn = input_stream.getEndColumn();
+ return t;
+}
+
+int curLexState = 0;
+int defaultLexState = 0;
+int jjnewStateCnt;
+int jjround;
+int jjmatchedPos;
+int jjmatchedKind;
+
+public Token getNextToken()
+{
+ int kind;
+ Token specialToken = null;
+ Token matchedToken;
+ int curPos = 0;
+
+ EOFLoop :
+ for (;;)
+ {
+ try
+ {
+ curChar = input_stream.BeginToken();
+ }
+ catch(java.io.IOException e)
+ {
+ jjmatchedKind = 0;
+ matchedToken = jjFillToken();
+ matchedToken.specialToken = specialToken;
+ return matchedToken;
+ }
+ image = null;
+ jjimageLen = 0;
+
+ for (;;)
+ {
+ switch(curLexState)
+ {
+ case 0:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_0();
+ break;
+ case 1:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_1();
+ break;
+ case 2:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_2();
+ break;
+ case 3:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_3();
+ break;
+ case 4:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_4();
+ break;
+ }
+ if (jjmatchedKind != 0x7fffffff)
+ {
+ if (jjmatchedPos + 1 < curPos)
+ input_stream.backup(curPos - jjmatchedPos - 1);
+ if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ matchedToken = jjFillToken();
+ matchedToken.specialToken = specialToken;
+ TokenLexicalActions(matchedToken);
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ return matchedToken;
+ }
+ else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ matchedToken = jjFillToken();
+ if (specialToken == null)
+ specialToken = matchedToken;
+ else
+ {
+ matchedToken.specialToken = specialToken;
+ specialToken = (specialToken.next = matchedToken);
+ }
+ }
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ continue EOFLoop;
+ }
+ MoreLexicalActions();
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ curPos = 0;
+ jjmatchedKind = 0x7fffffff;
+ try {
+ curChar = input_stream.readChar();
+ continue;
+ }
+ catch (java.io.IOException e1) { }
+ }
+ int error_line = input_stream.getEndLine();
+ int error_column = input_stream.getEndColumn();
+ String error_after = null;
+ boolean EOFSeen = false;
+ try { input_stream.readChar(); input_stream.backup(1); }
+ catch (java.io.IOException e1) {
+ EOFSeen = true;
+ error_after = curPos <= 1 ? "" : input_stream.GetImage();
+ if (curChar == '\n' || curChar == '\r') {
+ error_line++;
+ error_column = 0;
+ }
+ else
+ error_column++;
+ }
+ if (!EOFSeen) {
+ input_stream.backup(1);
+ error_after = curPos <= 1 ? "" : input_stream.GetImage();
+ }
+ throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR);
+ }
+ }
+}
+
+void MoreLexicalActions()
+{
+ jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
+ switch(jjmatchedKind)
+ {
+ case 16 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 21 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 22 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ commentNest = 1;
+ break;
+ case 24 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 25 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ ++commentNest;
+ break;
+ case 26 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT);
+ break;
+ case 28 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 1);
+ break;
+ case 29 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ default :
+ break;
+ }
+}
+void TokenLexicalActions(Token matchedToken)
+{
+ switch(jjmatchedKind)
+ {
+ case 18 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
+ matchedToken.image = image.toString();
+ break;
+ case 31 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
+ matchedToken.image = image.substring(0, image.length() - 1);
+ break;
+ default :
+ break;
+ }
+}
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
new file mode 100644
index 000000000..5987f19d8
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java
@@ -0,0 +1,35 @@
+/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public interface AddressListParserTreeConstants
+{
+ public int JJTVOID = 0;
+ public int JJTADDRESS_LIST = 1;
+ public int JJTADDRESS = 2;
+ public int JJTMAILBOX = 3;
+ public int JJTNAME_ADDR = 4;
+ public int JJTGROUP_BODY = 5;
+ public int JJTANGLE_ADDR = 6;
+ public int JJTROUTE = 7;
+ public int JJTPHRASE = 8;
+ public int JJTADDR_SPEC = 9;
+ public int JJTLOCAL_PART = 10;
+ public int JJTDOMAIN = 11;
+
+
+ public String[] jjtNodeName = {
+ "void",
+ "address_list",
+ "address",
+ "mailbox",
+ "name_addr",
+ "group_body",
+ "angle_addr",
+ "route",
+ "phrase",
+ "addr_spec",
+ "local_part",
+ "domain",
+ };
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
new file mode 100644
index 000000000..8ec2fe7d2
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java
@@ -0,0 +1,19 @@
+/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public interface AddressListParserVisitor
+{
+ public Object visit(SimpleNode node, Object data);
+ public Object visit(ASTaddress_list node, Object data);
+ public Object visit(ASTaddress node, Object data);
+ public Object visit(ASTmailbox node, Object data);
+ public Object visit(ASTname_addr node, Object data);
+ public Object visit(ASTgroup_body node, Object data);
+ public Object visit(ASTangle_addr node, Object data);
+ public Object visit(ASTroute node, Object data);
+ public Object visit(ASTphrase node, Object data);
+ public Object visit(ASTaddr_spec node, Object data);
+ public Object visit(ASTlocal_part node, Object data);
+ public Object visit(ASTdomain node, Object data);
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java
new file mode 100644
index 000000000..780974616
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java
@@ -0,0 +1,30 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.address.parser;
+
+import org.apache.james.mime4j.field.address.parser.Node;
+import org.apache.james.mime4j.field.address.parser.Token;
+
+public abstract class BaseNode implements Node {
+
+ public Token firstToken;
+ public Token lastToken;
+
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
new file mode 100644
index 000000000..08b5c5bef
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java
@@ -0,0 +1,123 @@
+/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+class JJTAddressListParserState {
+ private java.util.Stack<Node> nodes;
+ private java.util.Stack<Integer> marks;
+
+ private int sp; // number of nodes on stack
+ private int mk; // current mark
+ private boolean node_created;
+
+ JJTAddressListParserState() {
+ nodes = new java.util.Stack<Node>();
+ marks = new java.util.Stack<Integer>();
+ sp = 0;
+ mk = 0;
+ }
+
+ /* Determines whether the current node was actually closed and
+ pushed. This should only be called in the final user action of a
+ node scope. */
+ boolean nodeCreated() {
+ return node_created;
+ }
+
+ /* Call this to reinitialize the node stack. It is called
+ automatically by the parser's ReInit() method. */
+ void reset() {
+ nodes.removeAllElements();
+ marks.removeAllElements();
+ sp = 0;
+ mk = 0;
+ }
+
+ /* Returns the root node of the AST. It only makes sense to call
+ this after a successful parse. */
+ Node rootNode() {
+ return nodes.elementAt(0);
+ }
+
+ /* Pushes a node on to the stack. */
+ void pushNode(Node n) {
+ nodes.push(n);
+ ++sp;
+ }
+
+ /* Returns the node on the top of the stack, and remove it from the
+ stack. */
+ Node popNode() {
+ if (--sp < mk) {
+ mk = marks.pop().intValue();
+ }
+ return nodes.pop();
+ }
+
+ /* Returns the node currently on the top of the stack. */
+ Node peekNode() {
+ return nodes.peek();
+ }
+
+ /* Returns the number of children on the stack in the current node
+ scope. */
+ int nodeArity() {
+ return sp - mk;
+ }
+
+
+ void clearNodeScope(Node n) {
+ while (sp > mk) {
+ popNode();
+ }
+ mk = marks.pop().intValue();
+ }
+
+
+ void openNodeScope(Node n) {
+ marks.push(new Integer(mk));
+ mk = sp;
+ n.jjtOpen();
+ }
+
+
+ /* A definite node is constructed from a specified number of
+ children. That number of nodes are popped from the stack and
+ made the children of the definite node. Then the definite node
+ is pushed on to the stack. */
+ void closeNodeScope(Node n, int num) {
+ mk = marks.pop().intValue();
+ while (num-- > 0) {
+ Node c = popNode();
+ c.jjtSetParent(n);
+ n.jjtAddChild(c, num);
+ }
+ n.jjtClose();
+ pushNode(n);
+ node_created = true;
+ }
+
+
+ /* A conditional node is constructed if its condition is true. All
+ the nodes that have been pushed since the node was opened are
+ made children of the the conditional node, which is then pushed
+ on to the stack. If the condition is false the node is not
+ constructed and they are left on the stack. */
+ void closeNodeScope(Node n, boolean condition) {
+ if (condition) {
+ int a = nodeArity();
+ mk = marks.pop().intValue();
+ while (a-- > 0) {
+ Node c = popNode();
+ c.jjtSetParent(n);
+ n.jjtAddChild(c, a);
+ }
+ n.jjtClose();
+ pushNode(n);
+ node_created = true;
+ } else {
+ mk = marks.pop().intValue();
+ node_created = false;
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java
new file mode 100644
index 000000000..158892016
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java
@@ -0,0 +1,37 @@
+/* Generated By:JJTree: Do not edit this line. Node.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+/* All AST nodes must implement this interface. It provides basic
+ machinery for constructing the parent and child relationships
+ between nodes. */
+
+public interface Node {
+
+ /** This method is called after the node has been made the current
+ node. It indicates that child nodes can now be added to it. */
+ public void jjtOpen();
+
+ /** This method is called after all the child nodes have been
+ added. */
+ public void jjtClose();
+
+ /** This pair of methods are used to inform the node of its
+ parent. */
+ public void jjtSetParent(Node n);
+ public Node jjtGetParent();
+
+ /** This method tells the node to add its argument to the node's
+ list of children. */
+ public void jjtAddChild(Node n, int i);
+
+ /** This method returns a child node. The children are numbered
+ from zero, left to right. */
+ public Node jjtGetChild(int i);
+
+ /** Return the number of children the node has. */
+ public int jjtGetNumChildren();
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data);
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java
new file mode 100644
index 000000000..e20146fb6
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java
@@ -0,0 +1,207 @@
+/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+/**
+ * This exception is thrown when parse errors are encountered.
+ * You can explicitly create objects of this exception type by
+ * calling the method generateParseException in the generated
+ * parser.
+ *
+ * You can modify this class to customize your error reporting
+ * mechanisms so long as you retain the public fields.
+ */
+public class ParseException extends Exception {
+
+ /**
+ * This constructor is used by the method "generateParseException"
+ * in the generated parser. Calling this constructor generates
+ * a new object of this type with the fields "currentToken",
+ * "expectedTokenSequences", and "tokenImage" set. The boolean
+ * flag "specialConstructor" is also set to true to indicate that
+ * this constructor was used to create this object.
+ * This constructor calls its super class with the empty string
+ * to force the "toString" method of parent class "Throwable" to
+ * print the error message in the form:
+ * ParseException: <result of getMessage>
+ */
+ public ParseException(Token currentTokenVal,
+ int[][] expectedTokenSequencesVal,
+ String[] tokenImageVal
+ )
+ {
+ super("");
+ specialConstructor = true;
+ currentToken = currentTokenVal;
+ expectedTokenSequences = expectedTokenSequencesVal;
+ tokenImage = tokenImageVal;
+ }
+
+ /**
+ * The following constructors are for use by you for whatever
+ * purpose you can think of. Constructing the exception in this
+ * manner makes the exception behave in the normal way - i.e., as
+ * documented in the class "Throwable". The fields "errorToken",
+ * "expectedTokenSequences", and "tokenImage" do not contain
+ * relevant information. The JavaCC generated code does not use
+ * these constructors.
+ */
+
+ public ParseException() {
+ super();
+ specialConstructor = false;
+ }
+
+ public ParseException(String message) {
+ super(message);
+ specialConstructor = false;
+ }
+
+ /**
+ * This variable determines which constructor was used to create
+ * this object and thereby affects the semantics of the
+ * "getMessage" method (see below).
+ */
+ protected boolean specialConstructor;
+
+ /**
+ * This is the last token that has been consumed successfully. If
+ * this object has been created due to a parse error, the token
+ * followng this token will (therefore) be the first error token.
+ */
+ public Token currentToken;
+
+ /**
+ * Each entry in this array is an array of integers. Each array
+ * of integers represents a sequence of tokens (by their ordinal
+ * values) that is expected at this point of the parse.
+ */
+ public int[][] expectedTokenSequences;
+
+ /**
+ * This is a reference to the "tokenImage" array of the generated
+ * parser within which the parse error occurred. This array is
+ * defined in the generated ...Constants interface.
+ */
+ public String[] tokenImage;
+
+ /**
+ * This method has the standard behavior when this object has been
+ * created using the standard constructors. Otherwise, it uses
+ * "currentToken" and "expectedTokenSequences" to generate a parse
+ * error message and returns it. If this object has been created
+ * due to a parse error, and you do not catch it (it gets thrown
+ * from the parser), then this method is called during the printing
+ * of the final stack trace, and hence the correct error message
+ * gets displayed.
+ */
+ public String getMessage() {
+ if (!specialConstructor) {
+ return super.getMessage();
+ }
+ StringBuffer expected = new StringBuffer();
+ int maxSize = 0;
+ for (int i = 0; i < expectedTokenSequences.length; i++) {
+ if (maxSize < expectedTokenSequences[i].length) {
+ maxSize = expectedTokenSequences[i].length;
+ }
+ for (int j = 0; j < expectedTokenSequences[i].length; j++) {
+ expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" ");
+ }
+ if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) {
+ expected.append("...");
+ }
+ expected.append(eol).append(" ");
+ }
+ String retval = "Encountered \"";
+ Token tok = currentToken.next;
+ for (int i = 0; i < maxSize; i++) {
+ if (i != 0) retval += " ";
+ if (tok.kind == 0) {
+ retval += tokenImage[0];
+ break;
+ }
+ retval += add_escapes(tok.image);
+ tok = tok.next;
+ }
+ retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
+ retval += "." + eol;
+ if (expectedTokenSequences.length == 1) {
+ retval += "Was expecting:" + eol + " ";
+ } else {
+ retval += "Was expecting one of:" + eol + " ";
+ }
+ retval += expected.toString();
+ return retval;
+ }
+
+ /**
+ * The end of line string for this machine.
+ */
+ protected String eol = System.getProperty("line.separator", "\n");
+
+ /**
+ * Used to convert raw characters to their escaped version
+ * when these raw version cannot be used as part of an ASCII
+ * string literal.
+ */
+ protected String add_escapes(String str) {
+ StringBuffer retval = new StringBuffer();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
new file mode 100644
index 000000000..c9ba0b444
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java
@@ -0,0 +1,454 @@
+/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+/**
+ * An implementation of interface CharStream, where the stream is assumed to
+ * contain only ASCII characters (without unicode processing).
+ */
+
+public class SimpleCharStream
+{
+ public static final boolean staticFlag = false;
+ int bufsize;
+ int available;
+ int tokenBegin;
+ public int bufpos = -1;
+ protected int bufline[];
+ protected int bufcolumn[];
+
+ protected int column = 0;
+ protected int line = 1;
+
+ protected boolean prevCharIsCR = false;
+ protected boolean prevCharIsLF = false;
+
+ protected java.io.Reader inputStream;
+
+ protected char[] buffer;
+ protected int maxNextCharInd = 0;
+ protected int inBuf = 0;
+ protected int tabSize = 8;
+
+ protected void setTabSize(int i) { tabSize = i; }
+ protected int getTabSize(int i) { return tabSize; }
+
+
+ protected void ExpandBuff(boolean wrapAround)
+ {
+ char[] newbuffer = new char[bufsize + 2048];
+ int newbufline[] = new int[bufsize + 2048];
+ int newbufcolumn[] = new int[bufsize + 2048];
+
+ try
+ {
+ if (wrapAround)
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ System.arraycopy(buffer, 0, newbuffer,
+ bufsize - tokenBegin, bufpos);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos += (bufsize - tokenBegin));
+ }
+ else
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos -= tokenBegin);
+ }
+ }
+ catch (Throwable t)
+ {
+ throw new Error(t.getMessage());
+ }
+
+
+ bufsize += 2048;
+ available = bufsize;
+ tokenBegin = 0;
+ }
+
+ protected void FillBuff() throws java.io.IOException
+ {
+ if (maxNextCharInd == available)
+ {
+ if (available == bufsize)
+ {
+ if (tokenBegin > 2048)
+ {
+ bufpos = maxNextCharInd = 0;
+ available = tokenBegin;
+ }
+ else if (tokenBegin < 0)
+ bufpos = maxNextCharInd = 0;
+ else
+ ExpandBuff(false);
+ }
+ else if (available > tokenBegin)
+ available = bufsize;
+ else if ((tokenBegin - available) < 2048)
+ ExpandBuff(true);
+ else
+ available = tokenBegin;
+ }
+
+ int i;
+ try {
+ if ((i = inputStream.read(buffer, maxNextCharInd,
+ available - maxNextCharInd)) == -1)
+ {
+ inputStream.close();
+ throw new java.io.IOException();
+ }
+ else
+ maxNextCharInd += i;
+ return;
+ }
+ catch(java.io.IOException e) {
+ --bufpos;
+ backup(0);
+ if (tokenBegin == -1)
+ tokenBegin = bufpos;
+ throw e;
+ }
+ }
+
+ public char BeginToken() throws java.io.IOException
+ {
+ tokenBegin = -1;
+ char c = readChar();
+ tokenBegin = bufpos;
+
+ return c;
+ }
+
+ protected void UpdateLineColumn(char c)
+ {
+ column++;
+
+ if (prevCharIsLF)
+ {
+ prevCharIsLF = false;
+ line += (column = 1);
+ }
+ else if (prevCharIsCR)
+ {
+ prevCharIsCR = false;
+ if (c == '\n')
+ {
+ prevCharIsLF = true;
+ }
+ else
+ line += (column = 1);
+ }
+
+ switch (c)
+ {
+ case '\r' :
+ prevCharIsCR = true;
+ break;
+ case '\n' :
+ prevCharIsLF = true;
+ break;
+ case '\t' :
+ column--;
+ column += (tabSize - (column % tabSize));
+ break;
+ default :
+ break;
+ }
+
+ bufline[bufpos] = line;
+ bufcolumn[bufpos] = column;
+ }
+
+ public char readChar() throws java.io.IOException
+ {
+ if (inBuf > 0)
+ {
+ --inBuf;
+
+ if (++bufpos == bufsize)
+ bufpos = 0;
+
+ return buffer[bufpos];
+ }
+
+ if (++bufpos >= maxNextCharInd)
+ FillBuff();
+
+ char c = buffer[bufpos];
+
+ UpdateLineColumn(c);
+ return (c);
+ }
+
+ /**
+ * @deprecated
+ * @see #getEndColumn
+ */
+ @Deprecated
+ public int getColumn() {
+ return bufcolumn[bufpos];
+ }
+
+ /**
+ * @deprecated
+ * @see #getEndLine
+ */
+ @Deprecated
+ public int getLine() {
+ return bufline[bufpos];
+ }
+
+ public int getEndColumn() {
+ return bufcolumn[bufpos];
+ }
+
+ public int getEndLine() {
+ return bufline[bufpos];
+ }
+
+ public int getBeginColumn() {
+ return bufcolumn[tokenBegin];
+ }
+
+ public int getBeginLine() {
+ return bufline[tokenBegin];
+ }
+
+ public void backup(int amount) {
+
+ inBuf += amount;
+ if ((bufpos -= amount) < 0)
+ bufpos += bufsize;
+ }
+
+ public SimpleCharStream(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ available = bufsize = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+
+ public SimpleCharStream(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.Reader dstream)
+ {
+ this(dstream, 1, 1, 4096);
+ }
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ if (buffer == null || buffersize != buffer.length)
+ {
+ available = bufsize = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+ prevCharIsLF = prevCharIsCR = false;
+ tokenBegin = inBuf = maxNextCharInd = 0;
+ bufpos = -1;
+ }
+
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+
+ public void ReInit(java.io.Reader dstream)
+ {
+ ReInit(dstream, 1, 1, 4096);
+ }
+ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
+ {
+ this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn) throws java.io.UnsupportedEncodingException
+ {
+ this(dstream, encoding, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
+ {
+ this(dstream, encoding, 1, 1, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream)
+ {
+ this(dstream, 1, 1, 4096);
+ }
+
+ public void ReInit(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
+ }
+
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(dstream, encoding, 1, 1, 4096);
+ }
+
+ public void ReInit(java.io.InputStream dstream)
+ {
+ ReInit(dstream, 1, 1, 4096);
+ }
+ public void ReInit(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(dstream, encoding, startline, startcolumn, 4096);
+ }
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+ public String GetImage()
+ {
+ if (bufpos >= tokenBegin)
+ return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
+ else
+ return new String(buffer, tokenBegin, bufsize - tokenBegin) +
+ new String(buffer, 0, bufpos + 1);
+ }
+
+ public char[] GetSuffix(int len)
+ {
+ char[] ret = new char[len];
+
+ if ((bufpos + 1) >= len)
+ System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
+ else
+ {
+ System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
+ len - bufpos - 1);
+ System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
+ }
+
+ return ret;
+ }
+
+ public void Done()
+ {
+ buffer = null;
+ bufline = null;
+ bufcolumn = null;
+ }
+
+ /**
+ * Method to adjust line and column numbers for the start of a token.
+ */
+ public void adjustBeginLineColumn(int newLine, int newCol)
+ {
+ int start = tokenBegin;
+ int len;
+
+ if (bufpos >= tokenBegin)
+ {
+ len = bufpos - tokenBegin + inBuf + 1;
+ }
+ else
+ {
+ len = bufsize - tokenBegin + bufpos + 1 + inBuf;
+ }
+
+ int i = 0, j = 0, k = 0;
+ int nextColDiff = 0, columnDiff = 0;
+
+ while (i < len &&
+ bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
+ {
+ bufline[j] = newLine;
+ nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
+ bufcolumn[j] = newCol + columnDiff;
+ columnDiff = nextColDiff;
+ i++;
+ }
+
+ if (i < len)
+ {
+ bufline[j] = newLine++;
+ bufcolumn[j] = newCol + columnDiff;
+
+ while (i++ < len)
+ {
+ if (bufline[j = start % bufsize] != bufline[++start % bufsize])
+ bufline[j] = newLine++;
+ else
+ bufline[j] = newLine;
+ }
+ }
+
+ line = bufline[j];
+ column = bufcolumn[j];
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java
new file mode 100644
index 000000000..9bf537e60
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java
@@ -0,0 +1,87 @@
+/* Generated By:JJTree: Do not edit this line. SimpleNode.java */
+
+package org.apache.james.mime4j.field.address.parser;
+
+public class SimpleNode extends org.apache.james.mime4j.field.address.parser.BaseNode implements Node {
+ protected Node parent;
+ protected Node[] children;
+ protected int id;
+ protected AddressListParser parser;
+
+ public SimpleNode(int i) {
+ id = i;
+ }
+
+ public SimpleNode(AddressListParser p, int i) {
+ this(i);
+ parser = p;
+ }
+
+ public void jjtOpen() {
+ }
+
+ public void jjtClose() {
+ }
+
+ public void jjtSetParent(Node n) { parent = n; }
+ public Node jjtGetParent() { return parent; }
+
+ public void jjtAddChild(Node n, int i) {
+ if (children == null) {
+ children = new Node[i + 1];
+ } else if (i >= children.length) {
+ Node c[] = new Node[i + 1];
+ System.arraycopy(children, 0, c, 0, children.length);
+ children = c;
+ }
+ children[i] = n;
+ }
+
+ public Node jjtGetChild(int i) {
+ return children[i];
+ }
+
+ public int jjtGetNumChildren() {
+ return (children == null) ? 0 : children.length;
+ }
+
+ /** Accept the visitor. **/
+ public Object jjtAccept(AddressListParserVisitor visitor, Object data) {
+ return visitor.visit(this, data);
+ }
+
+ /** Accept the visitor. **/
+ public Object childrenAccept(AddressListParserVisitor visitor, Object data) {
+ if (children != null) {
+ for (int i = 0; i < children.length; ++i) {
+ children[i].jjtAccept(visitor, data);
+ }
+ }
+ return data;
+ }
+
+ /* You can override these two methods in subclasses of SimpleNode to
+ customize the way the node appears when the tree is dumped. If
+ your output uses more than one line you should override
+ toString(String), otherwise overriding toString() is probably all
+ you need to do. */
+
+ public String toString() { return AddressListParserTreeConstants.jjtNodeName[id]; }
+ public String toString(String prefix) { return prefix + toString(); }
+
+ /* Override this method if you want to customize how the node dumps
+ out its children. */
+
+ public void dump(String prefix) {
+ System.out.println(toString(prefix));
+ if (children != null) {
+ for (int i = 0; i < children.length; ++i) {
+ SimpleNode n = (SimpleNode)children[i];
+ if (n != null) {
+ n.dump(prefix + " ");
+ }
+ }
+ }
+ }
+}
+
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java
new file mode 100644
index 000000000..2382e8e92
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java
@@ -0,0 +1,96 @@
+/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+/**
+ * Describes the input token stream.
+ */
+
+public class Token {
+
+ /**
+ * An integer that describes the kind of this token. This numbering
+ * system is determined by JavaCCParser, and a table of these numbers is
+ * stored in the file ...Constants.java.
+ */
+ public int kind;
+
+ /**
+ * beginLine and beginColumn describe the position of the first character
+ * of this token; endLine and endColumn describe the position of the
+ * last character of this token.
+ */
+ public int beginLine, beginColumn, endLine, endColumn;
+
+ /**
+ * The string image of the token.
+ */
+ public String image;
+
+ /**
+ * A reference to the next regular (non-special) token from the input
+ * stream. If this is the last token from the input stream, or if the
+ * token manager has not read tokens beyond this one, this field is
+ * set to null. This is true only if this token is also a regular
+ * token. Otherwise, see below for a description of the contents of
+ * this field.
+ */
+ public Token next;
+
+ /**
+ * This field is used to access special tokens that occur prior to this
+ * token, but after the immediately preceding regular (non-special) token.
+ * If there are no such special tokens, this field is set to null.
+ * When there are more than one such special token, this field refers
+ * to the last of these special tokens, which in turn refers to the next
+ * previous special token through its specialToken field, and so on
+ * until the first special token (whose specialToken field is null).
+ * The next fields of special tokens refer to other special tokens that
+ * immediately follow it (without an intervening regular token). If there
+ * is no such token, this field is null.
+ */
+ public Token specialToken;
+
+ /**
+ * Returns the image.
+ */
+ public String toString()
+ {
+ return image;
+ }
+
+ /**
+ * Returns a new Token object, by default. However, if you want, you
+ * can create and return subclass objects based on the value of ofKind.
+ * Simply add the cases to the switch for all those special cases.
+ * For example, if you have a subclass of Token called IDToken that
+ * you want to create if ofKind is ID, simlpy add something like :
+ *
+ * case MyParserConstants.ID : return new IDToken();
+ *
+ * to the following switch statement. Then you can cast matchedToken
+ * variable to the appropriate type and use it in your lexical actions.
+ */
+ public static final Token newToken(int ofKind)
+ {
+ switch(ofKind)
+ {
+ default : return new Token();
+ }
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
new file mode 100644
index 000000000..0299c8523
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java
@@ -0,0 +1,148 @@
+/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.address.parser;
+
+public class TokenMgrError extends Error
+{
+ /*
+ * Ordinals for various reasons why an Error of this type can be thrown.
+ */
+
+ /**
+ * Lexical error occured.
+ */
+ static final int LEXICAL_ERROR = 0;
+
+ /**
+ * An attempt wass made to create a second instance of a static token manager.
+ */
+ static final int STATIC_LEXER_ERROR = 1;
+
+ /**
+ * Tried to change to an invalid lexical state.
+ */
+ static final int INVALID_LEXICAL_STATE = 2;
+
+ /**
+ * Detected (and bailed out of) an infinite loop in the token manager.
+ */
+ static final int LOOP_DETECTED = 3;
+
+ /**
+ * Indicates the reason why the exception is thrown. It will have
+ * one of the above 4 values.
+ */
+ int errorCode;
+
+ /**
+ * Replaces unprintable characters by their espaced (or unicode escaped)
+ * equivalents in the given string
+ */
+ protected static final String addEscapes(String str) {
+ StringBuffer retval = new StringBuffer();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
+ }
+
+ /**
+ * Returns a detailed message for the Error when it is thrown by the
+ * token manager to indicate a lexical error.
+ * Parameters :
+ * EOFSeen : indicates if EOF caused the lexicl error
+ * curLexState : lexical state in which this error occured
+ * errorLine : line number when the error occured
+ * errorColumn : column number when the error occured
+ * errorAfter : prefix that was seen before this error occured
+ * curchar : the offending character
+ * Note: You can customize the lexical error message by modifying this method.
+ */
+ protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
+ return("Lexical error at line " +
+ errorLine + ", column " +
+ errorColumn + ". Encountered: " +
+ (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") +
+ "after : \"" + addEscapes(errorAfter) + "\"");
+ }
+
+ /**
+ * You can also modify the body of this method to customize your error messages.
+ * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
+ * of end-users concern, so you can return something like :
+ *
+ * "Internal Error : Please file a bug report .... "
+ *
+ * from this method for such cases in the release version of your parser.
+ */
+ public String getMessage() {
+ return super.getMessage();
+ }
+
+ /*
+ * Constructors of various flavors follow.
+ */
+
+ public TokenMgrError() {
+ }
+
+ public TokenMgrError(String message, int reason) {
+ super(message);
+ errorCode = reason;
+ }
+
+ public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
+ this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
new file mode 100644
index 000000000..cacf3af21
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java
@@ -0,0 +1,268 @@
+/* Generated By:JavaCC: Do not edit this line. ContentTypeParser.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+
+import java.util.ArrayList;
+import java.util.Vector;
+
+public class ContentTypeParser implements ContentTypeParserConstants {
+
+ private String type;
+ private String subtype;
+ private ArrayList<String> paramNames = new ArrayList<String>();
+ private ArrayList<String> paramValues = new ArrayList<String>();
+
+ public String getType() { return type; }
+ public String getSubType() { return subtype; }
+ public ArrayList<String> getParamNames() { return paramNames; }
+ public ArrayList<String> getParamValues() { return paramValues; }
+
+ public static void main(String args[]) throws ParseException {
+ while (true) {
+ try {
+ ContentTypeParser parser = new ContentTypeParser(System.in);
+ parser.parseLine();
+ } catch (Exception x) {
+ x.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ final public void parseLine() throws ParseException {
+ parse();
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 1:
+ jj_consume_token(1);
+ break;
+ default:
+ jj_la1[0] = jj_gen;
+ ;
+ }
+ jj_consume_token(2);
+ }
+
+ final public void parseAll() throws ParseException {
+ parse();
+ jj_consume_token(0);
+ }
+
+ final public void parse() throws ParseException {
+ Token type;
+ Token subtype;
+ type = jj_consume_token(ATOKEN);
+ jj_consume_token(3);
+ subtype = jj_consume_token(ATOKEN);
+ this.type = type.image;
+ this.subtype = subtype.image;
+ label_1:
+ while (true) {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 4:
+ ;
+ break;
+ default:
+ jj_la1[1] = jj_gen;
+ break label_1;
+ }
+ jj_consume_token(4);
+ parameter();
+ }
+ }
+
+ final public void parameter() throws ParseException {
+ Token attrib;
+ String val;
+ attrib = jj_consume_token(ATOKEN);
+ jj_consume_token(5);
+ val = value();
+ paramNames.add(attrib.image);
+ paramValues.add(val);
+ }
+
+ final public String value() throws ParseException {
+ Token t;
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case ATOKEN:
+ t = jj_consume_token(ATOKEN);
+ break;
+ case QUOTEDSTRING:
+ t = jj_consume_token(QUOTEDSTRING);
+ break;
+ default:
+ jj_la1[2] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ {if (true) return t.image;}
+ throw new Error("Missing return statement in function");
+ }
+
+ public ContentTypeParserTokenManager token_source;
+ SimpleCharStream jj_input_stream;
+ public Token token, jj_nt;
+ private int jj_ntk;
+ private int jj_gen;
+ final private int[] jj_la1 = new int[3];
+ static private int[] jj_la1_0;
+ static {
+ jj_la1_0();
+ }
+ private static void jj_la1_0() {
+ jj_la1_0 = new int[] {0x2,0x10,0x280000,};
+ }
+
+ public ContentTypeParser(java.io.InputStream stream) {
+ this(stream, null);
+ }
+ public ContentTypeParser(java.io.InputStream stream, String encoding) {
+ try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+ token_source = new ContentTypeParserTokenManager(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 3; i++) jj_la1[i] = -1;
+ }
+
+ public void ReInit(java.io.InputStream stream) {
+ ReInit(stream, null);
+ }
+ public void ReInit(java.io.InputStream stream, String encoding) {
+ try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+ token_source.ReInit(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 3; i++) jj_la1[i] = -1;
+ }
+
+ public ContentTypeParser(java.io.Reader stream) {
+ jj_input_stream = new SimpleCharStream(stream, 1, 1);
+ token_source = new ContentTypeParserTokenManager(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 3; i++) jj_la1[i] = -1;
+ }
+
+ public void ReInit(java.io.Reader stream) {
+ jj_input_stream.ReInit(stream, 1, 1);
+ token_source.ReInit(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 3; i++) jj_la1[i] = -1;
+ }
+
+ public ContentTypeParser(ContentTypeParserTokenManager tm) {
+ token_source = tm;
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 3; i++) jj_la1[i] = -1;
+ }
+
+ public void ReInit(ContentTypeParserTokenManager tm) {
+ token_source = tm;
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 3; i++) jj_la1[i] = -1;
+ }
+
+ final private Token jj_consume_token(int kind) throws ParseException {
+ Token oldToken;
+ if ((oldToken = token).next != null) token = token.next;
+ else token = token.next = token_source.getNextToken();
+ jj_ntk = -1;
+ if (token.kind == kind) {
+ jj_gen++;
+ return token;
+ }
+ token = oldToken;
+ jj_kind = kind;
+ throw generateParseException();
+ }
+
+ final public Token getNextToken() {
+ if (token.next != null) token = token.next;
+ else token = token.next = token_source.getNextToken();
+ jj_ntk = -1;
+ jj_gen++;
+ return token;
+ }
+
+ final public Token getToken(int index) {
+ Token t = token;
+ for (int i = 0; i < index; i++) {
+ if (t.next != null) t = t.next;
+ else t = t.next = token_source.getNextToken();
+ }
+ return t;
+ }
+
+ final private int jj_ntk() {
+ if ((jj_nt=token.next) == null)
+ return (jj_ntk = (token.next=token_source.getNextToken()).kind);
+ else
+ return (jj_ntk = jj_nt.kind);
+ }
+
+ private Vector<int[]> jj_expentries = new Vector<int[]>();
+ private int[] jj_expentry;
+ private int jj_kind = -1;
+
+ public ParseException generateParseException() {
+ jj_expentries.removeAllElements();
+ boolean[] la1tokens = new boolean[24];
+ for (int i = 0; i < 24; i++) {
+ la1tokens[i] = false;
+ }
+ if (jj_kind >= 0) {
+ la1tokens[jj_kind] = true;
+ jj_kind = -1;
+ }
+ for (int i = 0; i < 3; i++) {
+ if (jj_la1[i] == jj_gen) {
+ for (int j = 0; j < 32; j++) {
+ if ((jj_la1_0[i] & (1<<j)) != 0) {
+ la1tokens[j] = true;
+ }
+ }
+ }
+ }
+ for (int i = 0; i < 24; i++) {
+ if (la1tokens[i]) {
+ jj_expentry = new int[1];
+ jj_expentry[0] = i;
+ jj_expentries.addElement(jj_expentry);
+ }
+ }
+ int[][] exptokseq = new int[jj_expentries.size()][];
+ for (int i = 0; i < jj_expentries.size(); i++) {
+ exptokseq[i] = jj_expentries.elementAt(i);
+ }
+ return new ParseException(token, exptokseq, tokenImage);
+ }
+
+ final public void enable_tracing() {
+ }
+
+ final public void disable_tracing() {
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
new file mode 100644
index 000000000..d933d800d
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java
@@ -0,0 +1,62 @@
+/* Generated By:JavaCC: Do not edit this line. ContentTypeParserConstants.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+
+public interface ContentTypeParserConstants {
+
+ int EOF = 0;
+ int WS = 6;
+ int COMMENT = 8;
+ int QUOTEDSTRING = 19;
+ int DIGITS = 20;
+ int ATOKEN = 21;
+ int QUOTEDPAIR = 22;
+ int ANY = 23;
+
+ int DEFAULT = 0;
+ int INCOMMENT = 1;
+ int NESTED_COMMENT = 2;
+ int INQUOTEDSTRING = 3;
+
+ String[] tokenImage = {
+ "<EOF>",
+ "\"\\r\"",
+ "\"\\n\"",
+ "\"/\"",
+ "\";\"",
+ "\"=\"",
+ "<WS>",
+ "\"(\"",
+ "\")\"",
+ "<token of kind 9>",
+ "\"(\"",
+ "<token of kind 11>",
+ "<token of kind 12>",
+ "\"(\"",
+ "\")\"",
+ "<token of kind 15>",
+ "\"\\\"\"",
+ "<token of kind 17>",
+ "<token of kind 18>",
+ "\"\\\"\"",
+ "<DIGITS>",
+ "<ATOKEN>",
+ "<QUOTEDPAIR>",
+ "<ANY>",
+ };
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
new file mode 100644
index 000000000..25b7abafa
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java
@@ -0,0 +1,877 @@
+/* Generated By:JavaCC: Do not edit this line. ContentTypeParserTokenManager.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+import java.util.ArrayList;
+
+public class ContentTypeParserTokenManager implements ContentTypeParserConstants
+{
+ // Keeps track of how many levels of comment nesting
+ // we've encountered. This is only used when the 2nd
+ // level is reached, for example ((this)), not (this).
+ // This is because the outermost level must be treated
+ // specially anyway, because the outermost ")" has a
+ // different token type than inner ")" instances.
+ static int commentNest;
+ public java.io.PrintStream debugStream = System.out;
+ public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
+private final int jjStopStringLiteralDfa_0(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_0(int pos, long active0)
+{
+ return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
+}
+private final int jjStopAtPos(int pos, int kind)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ return pos + 1;
+}
+private final int jjStartNfaWithStates_0(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_0(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_0()
+{
+ switch(curChar)
+ {
+ case 10:
+ return jjStartNfaWithStates_0(0, 2, 2);
+ case 13:
+ return jjStartNfaWithStates_0(0, 1, 2);
+ case 34:
+ return jjStopAtPos(0, 16);
+ case 40:
+ return jjStopAtPos(0, 7);
+ case 47:
+ return jjStopAtPos(0, 3);
+ case 59:
+ return jjStopAtPos(0, 4);
+ case 61:
+ return jjStopAtPos(0, 5);
+ default :
+ return jjMoveNfa_0(3, 0);
+ }
+}
+private final void jjCheckNAdd(int state)
+{
+ if (jjrounds[state] != jjround)
+ {
+ jjstateSet[jjnewStateCnt++] = state;
+ jjrounds[state] = jjround;
+ }
+}
+private final void jjAddStates(int start, int end)
+{
+ do {
+ jjstateSet[jjnewStateCnt++] = jjnextStates[start];
+ } while (start++ != end);
+}
+private final void jjCheckNAddTwoStates(int state1, int state2)
+{
+ jjCheckNAdd(state1);
+ jjCheckNAdd(state2);
+}
+private final void jjCheckNAddStates(int start, int end)
+{
+ do {
+ jjCheckNAdd(jjnextStates[start]);
+ } while (start++ != end);
+}
+private final void jjCheckNAddStates(int start)
+{
+ jjCheckNAdd(jjnextStates[start]);
+ jjCheckNAdd(jjnextStates[start + 1]);
+}
+static final long[] jjbitVec0 = {
+ 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL
+};
+private final int jjMoveNfa_0(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 3:
+ if ((0x3ff6cfafffffdffL & l) != 0L)
+ {
+ if (kind > 21)
+ kind = 21;
+ jjCheckNAdd(2);
+ }
+ else if ((0x100000200L & l) != 0L)
+ {
+ if (kind > 6)
+ kind = 6;
+ jjCheckNAdd(0);
+ }
+ if ((0x3ff000000000000L & l) != 0L)
+ {
+ if (kind > 20)
+ kind = 20;
+ jjCheckNAdd(1);
+ }
+ break;
+ case 0:
+ if ((0x100000200L & l) == 0L)
+ break;
+ kind = 6;
+ jjCheckNAdd(0);
+ break;
+ case 1:
+ if ((0x3ff000000000000L & l) == 0L)
+ break;
+ if (kind > 20)
+ kind = 20;
+ jjCheckNAdd(1);
+ break;
+ case 2:
+ if ((0x3ff6cfafffffdffL & l) == 0L)
+ break;
+ if (kind > 21)
+ kind = 21;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 3:
+ case 2:
+ if ((0xffffffffc7fffffeL & l) == 0L)
+ break;
+ kind = 21;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 3:
+ case 2:
+ if ((jjbitVec0[i2] & l2) == 0L)
+ break;
+ if (kind > 21)
+ kind = 21;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_1(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_1(int pos, long active0)
+{
+ return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_1(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_1(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_1()
+{
+ switch(curChar)
+ {
+ case 40:
+ return jjStopAtPos(0, 10);
+ case 41:
+ return jjStopAtPos(0, 8);
+ default :
+ return jjMoveNfa_1(0, 0);
+ }
+}
+private final int jjMoveNfa_1(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 11)
+ kind = 11;
+ break;
+ case 1:
+ if (kind > 9)
+ kind = 9;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 11)
+ kind = 11;
+ if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 9)
+ kind = 9;
+ break;
+ case 2:
+ if (kind > 11)
+ kind = 11;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 11)
+ kind = 11;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 9)
+ kind = 9;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_3(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_3(int pos, long active0)
+{
+ return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_3(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_3(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_3()
+{
+ switch(curChar)
+ {
+ case 34:
+ return jjStopAtPos(0, 19);
+ default :
+ return jjMoveNfa_3(0, 0);
+ }
+}
+private final int jjMoveNfa_3(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ case 2:
+ if ((0xfffffffbffffffffL & l) == 0L)
+ break;
+ if (kind > 18)
+ kind = 18;
+ jjCheckNAdd(2);
+ break;
+ case 1:
+ if (kind > 17)
+ kind = 17;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((0xffffffffefffffffL & l) != 0L)
+ {
+ if (kind > 18)
+ kind = 18;
+ jjCheckNAdd(2);
+ }
+ else if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 17)
+ kind = 17;
+ break;
+ case 2:
+ if ((0xffffffffefffffffL & l) == 0L)
+ break;
+ if (kind > 18)
+ kind = 18;
+ jjCheckNAdd(2);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ case 2:
+ if ((jjbitVec0[i2] & l2) == 0L)
+ break;
+ if (kind > 18)
+ kind = 18;
+ jjCheckNAdd(2);
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 17)
+ kind = 17;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_2(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_2(int pos, long active0)
+{
+ return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_2(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_2(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_2()
+{
+ switch(curChar)
+ {
+ case 40:
+ return jjStopAtPos(0, 13);
+ case 41:
+ return jjStopAtPos(0, 14);
+ default :
+ return jjMoveNfa_2(0, 0);
+ }
+}
+private final int jjMoveNfa_2(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 15)
+ kind = 15;
+ break;
+ case 1:
+ if (kind > 12)
+ kind = 12;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 15)
+ kind = 15;
+ if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 12)
+ kind = 12;
+ break;
+ case 2:
+ if (kind > 15)
+ kind = 15;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 15)
+ kind = 15;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 12)
+ kind = 12;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+static final int[] jjnextStates = {
+};
+public static final String[] jjstrLiteralImages = {
+"", "\15", "\12", "\57", "\73", "\75", null, null, null, null, null, null,
+null, null, null, null, null, null, null, null, null, null, null, null, };
+public static final String[] lexStateNames = {
+ "DEFAULT",
+ "INCOMMENT",
+ "NESTED_COMMENT",
+ "INQUOTEDSTRING",
+};
+public static final int[] jjnewLexState = {
+ -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, 3, -1, -1, 0, -1, -1, -1, -1,
+};
+static final long[] jjtoToken = {
+ 0x38003fL,
+};
+static final long[] jjtoSkip = {
+ 0x140L,
+};
+static final long[] jjtoSpecial = {
+ 0x40L,
+};
+static final long[] jjtoMore = {
+ 0x7fe80L,
+};
+protected SimpleCharStream input_stream;
+private final int[] jjrounds = new int[3];
+private final int[] jjstateSet = new int[6];
+StringBuffer image;
+int jjimageLen;
+int lengthOfMatch;
+protected char curChar;
+public ContentTypeParserTokenManager(SimpleCharStream stream){
+ if (SimpleCharStream.staticFlag)
+ throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
+ input_stream = stream;
+}
+public ContentTypeParserTokenManager(SimpleCharStream stream, int lexState){
+ this(stream);
+ SwitchTo(lexState);
+}
+public void ReInit(SimpleCharStream stream)
+{
+ jjmatchedPos = jjnewStateCnt = 0;
+ curLexState = defaultLexState;
+ input_stream = stream;
+ ReInitRounds();
+}
+private final void ReInitRounds()
+{
+ int i;
+ jjround = 0x80000001;
+ for (i = 3; i-- > 0;)
+ jjrounds[i] = 0x80000000;
+}
+public void ReInit(SimpleCharStream stream, int lexState)
+{
+ ReInit(stream);
+ SwitchTo(lexState);
+}
+public void SwitchTo(int lexState)
+{
+ if (lexState >= 4 || lexState < 0)
+ throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
+ else
+ curLexState = lexState;
+}
+
+protected Token jjFillToken()
+{
+ Token t = Token.newToken(jjmatchedKind);
+ t.kind = jjmatchedKind;
+ String im = jjstrLiteralImages[jjmatchedKind];
+ t.image = (im == null) ? input_stream.GetImage() : im;
+ t.beginLine = input_stream.getBeginLine();
+ t.beginColumn = input_stream.getBeginColumn();
+ t.endLine = input_stream.getEndLine();
+ t.endColumn = input_stream.getEndColumn();
+ return t;
+}
+
+int curLexState = 0;
+int defaultLexState = 0;
+int jjnewStateCnt;
+int jjround;
+int jjmatchedPos;
+int jjmatchedKind;
+
+public Token getNextToken()
+{
+ int kind;
+ Token specialToken = null;
+ Token matchedToken;
+ int curPos = 0;
+
+ EOFLoop :
+ for (;;)
+ {
+ try
+ {
+ curChar = input_stream.BeginToken();
+ }
+ catch(java.io.IOException e)
+ {
+ jjmatchedKind = 0;
+ matchedToken = jjFillToken();
+ matchedToken.specialToken = specialToken;
+ return matchedToken;
+ }
+ image = null;
+ jjimageLen = 0;
+
+ for (;;)
+ {
+ switch(curLexState)
+ {
+ case 0:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_0();
+ break;
+ case 1:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_1();
+ break;
+ case 2:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_2();
+ break;
+ case 3:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_3();
+ break;
+ }
+ if (jjmatchedKind != 0x7fffffff)
+ {
+ if (jjmatchedPos + 1 < curPos)
+ input_stream.backup(curPos - jjmatchedPos - 1);
+ if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ matchedToken = jjFillToken();
+ matchedToken.specialToken = specialToken;
+ TokenLexicalActions(matchedToken);
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ return matchedToken;
+ }
+ else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ matchedToken = jjFillToken();
+ if (specialToken == null)
+ specialToken = matchedToken;
+ else
+ {
+ matchedToken.specialToken = specialToken;
+ specialToken = (specialToken.next = matchedToken);
+ }
+ }
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ continue EOFLoop;
+ }
+ MoreLexicalActions();
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ curPos = 0;
+ jjmatchedKind = 0x7fffffff;
+ try {
+ curChar = input_stream.readChar();
+ continue;
+ }
+ catch (java.io.IOException e1) { }
+ }
+ int error_line = input_stream.getEndLine();
+ int error_column = input_stream.getEndColumn();
+ String error_after = null;
+ boolean EOFSeen = false;
+ try { input_stream.readChar(); input_stream.backup(1); }
+ catch (java.io.IOException e1) {
+ EOFSeen = true;
+ error_after = curPos <= 1 ? "" : input_stream.GetImage();
+ if (curChar == '\n' || curChar == '\r') {
+ error_line++;
+ error_column = 0;
+ }
+ else
+ error_column++;
+ }
+ if (!EOFSeen) {
+ input_stream.backup(1);
+ error_after = curPos <= 1 ? "" : input_stream.GetImage();
+ }
+ throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR);
+ }
+ }
+}
+
+void MoreLexicalActions()
+{
+ jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
+ switch(jjmatchedKind)
+ {
+ case 9 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 10 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ commentNest = 1;
+ break;
+ case 12 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 13 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ ++commentNest;
+ break;
+ case 14 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT);
+ break;
+ case 16 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 1);
+ break;
+ case 17 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ default :
+ break;
+ }
+}
+void TokenLexicalActions(Token matchedToken)
+{
+ switch(jjmatchedKind)
+ {
+ case 19 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
+ matchedToken.image = image.substring(0, image.length() - 1);
+ break;
+ default :
+ break;
+ }
+}
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
new file mode 100644
index 000000000..d9b69b25c
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java
@@ -0,0 +1,207 @@
+/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+
+/**
+ * This exception is thrown when parse errors are encountered.
+ * You can explicitly create objects of this exception type by
+ * calling the method generateParseException in the generated
+ * parser.
+ *
+ * You can modify this class to customize your error reporting
+ * mechanisms so long as you retain the public fields.
+ */
+public class ParseException extends Exception {
+
+ /**
+ * This constructor is used by the method "generateParseException"
+ * in the generated parser. Calling this constructor generates
+ * a new object of this type with the fields "currentToken",
+ * "expectedTokenSequences", and "tokenImage" set. The boolean
+ * flag "specialConstructor" is also set to true to indicate that
+ * this constructor was used to create this object.
+ * This constructor calls its super class with the empty string
+ * to force the "toString" method of parent class "Throwable" to
+ * print the error message in the form:
+ * ParseException: <result of getMessage>
+ */
+ public ParseException(Token currentTokenVal,
+ int[][] expectedTokenSequencesVal,
+ String[] tokenImageVal
+ )
+ {
+ super("");
+ specialConstructor = true;
+ currentToken = currentTokenVal;
+ expectedTokenSequences = expectedTokenSequencesVal;
+ tokenImage = tokenImageVal;
+ }
+
+ /**
+ * The following constructors are for use by you for whatever
+ * purpose you can think of. Constructing the exception in this
+ * manner makes the exception behave in the normal way - i.e., as
+ * documented in the class "Throwable". The fields "errorToken",
+ * "expectedTokenSequences", and "tokenImage" do not contain
+ * relevant information. The JavaCC generated code does not use
+ * these constructors.
+ */
+
+ public ParseException() {
+ super();
+ specialConstructor = false;
+ }
+
+ public ParseException(String message) {
+ super(message);
+ specialConstructor = false;
+ }
+
+ /**
+ * This variable determines which constructor was used to create
+ * this object and thereby affects the semantics of the
+ * "getMessage" method (see below).
+ */
+ protected boolean specialConstructor;
+
+ /**
+ * This is the last token that has been consumed successfully. If
+ * this object has been created due to a parse error, the token
+ * followng this token will (therefore) be the first error token.
+ */
+ public Token currentToken;
+
+ /**
+ * Each entry in this array is an array of integers. Each array
+ * of integers represents a sequence of tokens (by their ordinal
+ * values) that is expected at this point of the parse.
+ */
+ public int[][] expectedTokenSequences;
+
+ /**
+ * This is a reference to the "tokenImage" array of the generated
+ * parser within which the parse error occurred. This array is
+ * defined in the generated ...Constants interface.
+ */
+ public String[] tokenImage;
+
+ /**
+ * This method has the standard behavior when this object has been
+ * created using the standard constructors. Otherwise, it uses
+ * "currentToken" and "expectedTokenSequences" to generate a parse
+ * error message and returns it. If this object has been created
+ * due to a parse error, and you do not catch it (it gets thrown
+ * from the parser), then this method is called during the printing
+ * of the final stack trace, and hence the correct error message
+ * gets displayed.
+ */
+ public String getMessage() {
+ if (!specialConstructor) {
+ return super.getMessage();
+ }
+ StringBuffer expected = new StringBuffer();
+ int maxSize = 0;
+ for (int i = 0; i < expectedTokenSequences.length; i++) {
+ if (maxSize < expectedTokenSequences[i].length) {
+ maxSize = expectedTokenSequences[i].length;
+ }
+ for (int j = 0; j < expectedTokenSequences[i].length; j++) {
+ expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" ");
+ }
+ if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) {
+ expected.append("...");
+ }
+ expected.append(eol).append(" ");
+ }
+ String retval = "Encountered \"";
+ Token tok = currentToken.next;
+ for (int i = 0; i < maxSize; i++) {
+ if (i != 0) retval += " ";
+ if (tok.kind == 0) {
+ retval += tokenImage[0];
+ break;
+ }
+ retval += add_escapes(tok.image);
+ tok = tok.next;
+ }
+ retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
+ retval += "." + eol;
+ if (expectedTokenSequences.length == 1) {
+ retval += "Was expecting:" + eol + " ";
+ } else {
+ retval += "Was expecting one of:" + eol + " ";
+ }
+ retval += expected.toString();
+ return retval;
+ }
+
+ /**
+ * The end of line string for this machine.
+ */
+ protected String eol = System.getProperty("line.separator", "\n");
+
+ /**
+ * Used to convert raw characters to their escaped version
+ * when these raw version cannot be used as part of an ASCII
+ * string literal.
+ */
+ protected String add_escapes(String str) {
+ StringBuffer retval = new StringBuffer();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
new file mode 100644
index 000000000..ae035b717
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java
@@ -0,0 +1,454 @@
+/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+
+/**
+ * An implementation of interface CharStream, where the stream is assumed to
+ * contain only ASCII characters (without unicode processing).
+ */
+
+public class SimpleCharStream
+{
+ public static final boolean staticFlag = false;
+ int bufsize;
+ int available;
+ int tokenBegin;
+ public int bufpos = -1;
+ protected int bufline[];
+ protected int bufcolumn[];
+
+ protected int column = 0;
+ protected int line = 1;
+
+ protected boolean prevCharIsCR = false;
+ protected boolean prevCharIsLF = false;
+
+ protected java.io.Reader inputStream;
+
+ protected char[] buffer;
+ protected int maxNextCharInd = 0;
+ protected int inBuf = 0;
+ protected int tabSize = 8;
+
+ protected void setTabSize(int i) { tabSize = i; }
+ protected int getTabSize(int i) { return tabSize; }
+
+
+ protected void ExpandBuff(boolean wrapAround)
+ {
+ char[] newbuffer = new char[bufsize + 2048];
+ int newbufline[] = new int[bufsize + 2048];
+ int newbufcolumn[] = new int[bufsize + 2048];
+
+ try
+ {
+ if (wrapAround)
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ System.arraycopy(buffer, 0, newbuffer,
+ bufsize - tokenBegin, bufpos);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos += (bufsize - tokenBegin));
+ }
+ else
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos -= tokenBegin);
+ }
+ }
+ catch (Throwable t)
+ {
+ throw new Error(t.getMessage());
+ }
+
+
+ bufsize += 2048;
+ available = bufsize;
+ tokenBegin = 0;
+ }
+
+ protected void FillBuff() throws java.io.IOException
+ {
+ if (maxNextCharInd == available)
+ {
+ if (available == bufsize)
+ {
+ if (tokenBegin > 2048)
+ {
+ bufpos = maxNextCharInd = 0;
+ available = tokenBegin;
+ }
+ else if (tokenBegin < 0)
+ bufpos = maxNextCharInd = 0;
+ else
+ ExpandBuff(false);
+ }
+ else if (available > tokenBegin)
+ available = bufsize;
+ else if ((tokenBegin - available) < 2048)
+ ExpandBuff(true);
+ else
+ available = tokenBegin;
+ }
+
+ int i;
+ try {
+ if ((i = inputStream.read(buffer, maxNextCharInd,
+ available - maxNextCharInd)) == -1)
+ {
+ inputStream.close();
+ throw new java.io.IOException();
+ }
+ else
+ maxNextCharInd += i;
+ return;
+ }
+ catch(java.io.IOException e) {
+ --bufpos;
+ backup(0);
+ if (tokenBegin == -1)
+ tokenBegin = bufpos;
+ throw e;
+ }
+ }
+
+ public char BeginToken() throws java.io.IOException
+ {
+ tokenBegin = -1;
+ char c = readChar();
+ tokenBegin = bufpos;
+
+ return c;
+ }
+
+ protected void UpdateLineColumn(char c)
+ {
+ column++;
+
+ if (prevCharIsLF)
+ {
+ prevCharIsLF = false;
+ line += (column = 1);
+ }
+ else if (prevCharIsCR)
+ {
+ prevCharIsCR = false;
+ if (c == '\n')
+ {
+ prevCharIsLF = true;
+ }
+ else
+ line += (column = 1);
+ }
+
+ switch (c)
+ {
+ case '\r' :
+ prevCharIsCR = true;
+ break;
+ case '\n' :
+ prevCharIsLF = true;
+ break;
+ case '\t' :
+ column--;
+ column += (tabSize - (column % tabSize));
+ break;
+ default :
+ break;
+ }
+
+ bufline[bufpos] = line;
+ bufcolumn[bufpos] = column;
+ }
+
+ public char readChar() throws java.io.IOException
+ {
+ if (inBuf > 0)
+ {
+ --inBuf;
+
+ if (++bufpos == bufsize)
+ bufpos = 0;
+
+ return buffer[bufpos];
+ }
+
+ if (++bufpos >= maxNextCharInd)
+ FillBuff();
+
+ char c = buffer[bufpos];
+
+ UpdateLineColumn(c);
+ return (c);
+ }
+
+ /**
+ * @deprecated
+ * @see #getEndColumn
+ */
+ @Deprecated
+ public int getColumn() {
+ return bufcolumn[bufpos];
+ }
+
+ /**
+ * @deprecated
+ * @see #getEndLine
+ */
+ @Deprecated
+ public int getLine() {
+ return bufline[bufpos];
+ }
+
+ public int getEndColumn() {
+ return bufcolumn[bufpos];
+ }
+
+ public int getEndLine() {
+ return bufline[bufpos];
+ }
+
+ public int getBeginColumn() {
+ return bufcolumn[tokenBegin];
+ }
+
+ public int getBeginLine() {
+ return bufline[tokenBegin];
+ }
+
+ public void backup(int amount) {
+
+ inBuf += amount;
+ if ((bufpos -= amount) < 0)
+ bufpos += bufsize;
+ }
+
+ public SimpleCharStream(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ available = bufsize = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+
+ public SimpleCharStream(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.Reader dstream)
+ {
+ this(dstream, 1, 1, 4096);
+ }
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ if (buffer == null || buffersize != buffer.length)
+ {
+ available = bufsize = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+ prevCharIsLF = prevCharIsCR = false;
+ tokenBegin = inBuf = maxNextCharInd = 0;
+ bufpos = -1;
+ }
+
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+
+ public void ReInit(java.io.Reader dstream)
+ {
+ ReInit(dstream, 1, 1, 4096);
+ }
+ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
+ {
+ this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn) throws java.io.UnsupportedEncodingException
+ {
+ this(dstream, encoding, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
+ {
+ this(dstream, encoding, 1, 1, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream)
+ {
+ this(dstream, 1, 1, 4096);
+ }
+
+ public void ReInit(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
+ }
+
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(dstream, encoding, 1, 1, 4096);
+ }
+
+ public void ReInit(java.io.InputStream dstream)
+ {
+ ReInit(dstream, 1, 1, 4096);
+ }
+ public void ReInit(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(dstream, encoding, startline, startcolumn, 4096);
+ }
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+ public String GetImage()
+ {
+ if (bufpos >= tokenBegin)
+ return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
+ else
+ return new String(buffer, tokenBegin, bufsize - tokenBegin) +
+ new String(buffer, 0, bufpos + 1);
+ }
+
+ public char[] GetSuffix(int len)
+ {
+ char[] ret = new char[len];
+
+ if ((bufpos + 1) >= len)
+ System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
+ else
+ {
+ System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
+ len - bufpos - 1);
+ System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
+ }
+
+ return ret;
+ }
+
+ public void Done()
+ {
+ buffer = null;
+ bufline = null;
+ bufcolumn = null;
+ }
+
+ /**
+ * Method to adjust line and column numbers for the start of a token.
+ */
+ public void adjustBeginLineColumn(int newLine, int newCol)
+ {
+ int start = tokenBegin;
+ int len;
+
+ if (bufpos >= tokenBegin)
+ {
+ len = bufpos - tokenBegin + inBuf + 1;
+ }
+ else
+ {
+ len = bufsize - tokenBegin + bufpos + 1 + inBuf;
+ }
+
+ int i = 0, j = 0, k = 0;
+ int nextColDiff = 0, columnDiff = 0;
+
+ while (i < len &&
+ bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
+ {
+ bufline[j] = newLine;
+ nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
+ bufcolumn[j] = newCol + columnDiff;
+ columnDiff = nextColDiff;
+ i++;
+ }
+
+ if (i < len)
+ {
+ bufline[j] = newLine++;
+ bufcolumn[j] = newCol + columnDiff;
+
+ while (i++ < len)
+ {
+ if (bufline[j = start % bufsize] != bufline[++start % bufsize])
+ bufline[j] = newLine++;
+ else
+ bufline[j] = newLine;
+ }
+ }
+
+ line = bufline[j];
+ column = bufcolumn[j];
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java
new file mode 100644
index 000000000..34e65eec0
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java
@@ -0,0 +1,96 @@
+/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+
+/**
+ * Describes the input token stream.
+ */
+
+public class Token {
+
+ /**
+ * An integer that describes the kind of this token. This numbering
+ * system is determined by JavaCCParser, and a table of these numbers is
+ * stored in the file ...Constants.java.
+ */
+ public int kind;
+
+ /**
+ * beginLine and beginColumn describe the position of the first character
+ * of this token; endLine and endColumn describe the position of the
+ * last character of this token.
+ */
+ public int beginLine, beginColumn, endLine, endColumn;
+
+ /**
+ * The string image of the token.
+ */
+ public String image;
+
+ /**
+ * A reference to the next regular (non-special) token from the input
+ * stream. If this is the last token from the input stream, or if the
+ * token manager has not read tokens beyond this one, this field is
+ * set to null. This is true only if this token is also a regular
+ * token. Otherwise, see below for a description of the contents of
+ * this field.
+ */
+ public Token next;
+
+ /**
+ * This field is used to access special tokens that occur prior to this
+ * token, but after the immediately preceding regular (non-special) token.
+ * If there are no such special tokens, this field is set to null.
+ * When there are more than one such special token, this field refers
+ * to the last of these special tokens, which in turn refers to the next
+ * previous special token through its specialToken field, and so on
+ * until the first special token (whose specialToken field is null).
+ * The next fields of special tokens refer to other special tokens that
+ * immediately follow it (without an intervening regular token). If there
+ * is no such token, this field is null.
+ */
+ public Token specialToken;
+
+ /**
+ * Returns the image.
+ */
+ public String toString()
+ {
+ return image;
+ }
+
+ /**
+ * Returns a new Token object, by default. However, if you want, you
+ * can create and return subclass objects based on the value of ofKind.
+ * Simply add the cases to the switch for all those special cases.
+ * For example, if you have a subclass of Token called IDToken that
+ * you want to create if ofKind is ID, simlpy add something like :
+ *
+ * case MyParserConstants.ID : return new IDToken();
+ *
+ * to the following switch statement. Then you can cast matchedToken
+ * variable to the appropriate type and use it in your lexical actions.
+ */
+ public static final Token newToken(int ofKind)
+ {
+ switch(ofKind)
+ {
+ default : return new Token();
+ }
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
new file mode 100644
index 000000000..ea5a7826e
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java
@@ -0,0 +1,148 @@
+/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.contenttype.parser;
+
+public class TokenMgrError extends Error
+{
+ /*
+ * Ordinals for various reasons why an Error of this type can be thrown.
+ */
+
+ /**
+ * Lexical error occured.
+ */
+ static final int LEXICAL_ERROR = 0;
+
+ /**
+ * An attempt wass made to create a second instance of a static token manager.
+ */
+ static final int STATIC_LEXER_ERROR = 1;
+
+ /**
+ * Tried to change to an invalid lexical state.
+ */
+ static final int INVALID_LEXICAL_STATE = 2;
+
+ /**
+ * Detected (and bailed out of) an infinite loop in the token manager.
+ */
+ static final int LOOP_DETECTED = 3;
+
+ /**
+ * Indicates the reason why the exception is thrown. It will have
+ * one of the above 4 values.
+ */
+ int errorCode;
+
+ /**
+ * Replaces unprintable characters by their espaced (or unicode escaped)
+ * equivalents in the given string
+ */
+ protected static final String addEscapes(String str) {
+ StringBuffer retval = new StringBuffer();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
+ }
+
+ /**
+ * Returns a detailed message for the Error when it is thrown by the
+ * token manager to indicate a lexical error.
+ * Parameters :
+ * EOFSeen : indicates if EOF caused the lexicl error
+ * curLexState : lexical state in which this error occured
+ * errorLine : line number when the error occured
+ * errorColumn : column number when the error occured
+ * errorAfter : prefix that was seen before this error occured
+ * curchar : the offending character
+ * Note: You can customize the lexical error message by modifying this method.
+ */
+ protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
+ return("Lexical error at line " +
+ errorLine + ", column " +
+ errorColumn + ". Encountered: " +
+ (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") +
+ "after : \"" + addEscapes(errorAfter) + "\"");
+ }
+
+ /**
+ * You can also modify the body of this method to customize your error messages.
+ * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
+ * of end-users concern, so you can return something like :
+ *
+ * "Internal Error : Please file a bug report .... "
+ *
+ * from this method for such cases in the release version of your parser.
+ */
+ public String getMessage() {
+ return super.getMessage();
+ }
+
+ /*
+ * Constructors of various flavors follow.
+ */
+
+ public TokenMgrError() {
+ }
+
+ public TokenMgrError(String message, int reason) {
+ super(message);
+ errorCode = reason;
+ }
+
+ public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
+ this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java
new file mode 100644
index 000000000..506ff54e5
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java
@@ -0,0 +1,127 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.field.datetime;
+
+import org.apache.james.mime4j.field.datetime.parser.DateTimeParser;
+import org.apache.james.mime4j.field.datetime.parser.ParseException;
+import org.apache.james.mime4j.field.datetime.parser.TokenMgrError;
+
+import java.util.Date;
+import java.util.Calendar;
+import java.util.TimeZone;
+import java.util.GregorianCalendar;
+import java.io.StringReader;
+
+public class DateTime {
+ private final Date date;
+ private final int year;
+ private final int month;
+ private final int day;
+ private final int hour;
+ private final int minute;
+ private final int second;
+ private final int timeZone;
+
+ public DateTime(String yearString, int month, int day, int hour, int minute, int second, int timeZone) {
+ this.year = convertToYear(yearString);
+ this.date = convertToDate(year, month, day, hour, minute, second, timeZone);
+ this.month = month;
+ this.day = day;
+ this.hour = hour;
+ this.minute = minute;
+ this.second = second;
+ this.timeZone = timeZone;
+ }
+
+ private int convertToYear(String yearString) {
+ int year = Integer.parseInt(yearString);
+ switch (yearString.length()) {
+ case 1:
+ case 2:
+ if (year >= 0 && year < 50)
+ return 2000 + year;
+ else
+ return 1900 + year;
+ case 3:
+ return 1900 + year;
+ default:
+ return year;
+ }
+ }
+
+ public static Date convertToDate(int year, int month, int day, int hour, int minute, int second, int timeZone) {
+ Calendar c = new GregorianCalendar(TimeZone.getTimeZone("GMT+0"));
+ c.set(year, month - 1, day, hour, minute, second);
+ c.set(Calendar.MILLISECOND, 0);
+
+ if (timeZone != Integer.MIN_VALUE) {
+ int minutes = ((timeZone / 100) * 60) + timeZone % 100;
+ c.add(Calendar.MINUTE, -1 * minutes);
+ }
+
+ return c.getTime();
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public int getMonth() {
+ return month;
+ }
+
+ public int getDay() {
+ return day;
+ }
+
+ public int getHour() {
+ return hour;
+ }
+
+ public int getMinute() {
+ return minute;
+ }
+
+ public int getSecond() {
+ return second;
+ }
+
+ public int getTimeZone() {
+ return timeZone;
+ }
+
+ public void print() {
+ System.out.println(getYear() + " " + getMonth() + " " + getDay() + "; " + getHour() + " " + getMinute() + " " + getSecond() + " " + getTimeZone());
+ }
+
+
+ public static DateTime parse(String dateString) throws ParseException {
+ try {
+ return new DateTimeParser(new StringReader(dateString)).parseAll();
+ }
+ catch (TokenMgrError err) {
+ throw new ParseException(err.getMessage());
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
new file mode 100644
index 000000000..43edebb5c
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java
@@ -0,0 +1,570 @@
+/* Generated By:JavaCC: Do not edit this line. DateTimeParser.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+
+import org.apache.james.mime4j.field.datetime.DateTime;
+
+import java.util.Vector;
+
+public class DateTimeParser implements DateTimeParserConstants {
+ private static final boolean ignoreMilitaryZoneOffset = true;
+
+ public static void main(String args[]) throws ParseException {
+ while (true) {
+ try {
+ DateTimeParser parser = new DateTimeParser(System.in);
+ parser.parseLine();
+ } catch (Exception x) {
+ x.printStackTrace();
+ return;
+ }
+ }
+ }
+
+ private static int parseDigits(Token token) {
+ return Integer.parseInt(token.image, 10);
+ }
+
+ private static int getMilitaryZoneOffset(char c) {
+ if (ignoreMilitaryZoneOffset)
+ return 0;
+
+ c = Character.toUpperCase(c);
+
+ switch (c) {
+ case 'A': return 1;
+ case 'B': return 2;
+ case 'C': return 3;
+ case 'D': return 4;
+ case 'E': return 5;
+ case 'F': return 6;
+ case 'G': return 7;
+ case 'H': return 8;
+ case 'I': return 9;
+ case 'K': return 10;
+ case 'L': return 11;
+ case 'M': return 12;
+
+ case 'N': return -1;
+ case 'O': return -2;
+ case 'P': return -3;
+ case 'Q': return -4;
+ case 'R': return -5;
+ case 'S': return -6;
+ case 'T': return -7;
+ case 'U': return -8;
+ case 'V': return -9;
+ case 'W': return -10;
+ case 'X': return -11;
+ case 'Y': return -12;
+
+ case 'Z': return 0;
+ default: return 0;
+ }
+ }
+
+ private static class Time {
+ private int hour;
+ private int minute;
+ private int second;
+ private int zone;
+
+ public Time(int hour, int minute, int second, int zone) {
+ this.hour = hour;
+ this.minute = minute;
+ this.second = second;
+ this.zone = zone;
+ }
+
+ public int getHour() { return hour; }
+ public int getMinute() { return minute; }
+ public int getSecond() { return second; }
+ public int getZone() { return zone; }
+ }
+
+ private static class Date {
+ private String year;
+ private int month;
+ private int day;
+
+ public Date(String year, int month, int day) {
+ this.year = year;
+ this.month = month;
+ this.day = day;
+ }
+
+ public String getYear() { return year; }
+ public int getMonth() { return month; }
+ public int getDay() { return day; }
+ }
+
+ final public DateTime parseLine() throws ParseException {
+ DateTime dt;
+ dt = date_time();
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 1:
+ jj_consume_token(1);
+ break;
+ default:
+ jj_la1[0] = jj_gen;
+ ;
+ }
+ jj_consume_token(2);
+ {if (true) return dt;}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public DateTime parseAll() throws ParseException {
+ DateTime dt;
+ dt = date_time();
+ jj_consume_token(0);
+ {if (true) return dt;}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public DateTime date_time() throws ParseException {
+ Date d; Time t;
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ case 10:
+ day_of_week();
+ jj_consume_token(3);
+ break;
+ default:
+ jj_la1[1] = jj_gen;
+ ;
+ }
+ d = date();
+ t = time();
+ {if (true) return new DateTime(
+ d.getYear(),
+ d.getMonth(),
+ d.getDay(),
+ t.getHour(),
+ t.getMinute(),
+ t.getSecond(),
+ t.getZone());} // time zone offset
+
+ throw new Error("Missing return statement in function");
+ }
+
+ final public String day_of_week() throws ParseException {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 4:
+ jj_consume_token(4);
+ break;
+ case 5:
+ jj_consume_token(5);
+ break;
+ case 6:
+ jj_consume_token(6);
+ break;
+ case 7:
+ jj_consume_token(7);
+ break;
+ case 8:
+ jj_consume_token(8);
+ break;
+ case 9:
+ jj_consume_token(9);
+ break;
+ case 10:
+ jj_consume_token(10);
+ break;
+ default:
+ jj_la1[2] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ {if (true) return token.image;}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public Date date() throws ParseException {
+ int d, m; String y;
+ d = day();
+ m = month();
+ y = year();
+ {if (true) return new Date(y, m, d);}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int day() throws ParseException {
+ Token t;
+ t = jj_consume_token(DIGITS);
+ {if (true) return parseDigits(t);}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int month() throws ParseException {
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 11:
+ jj_consume_token(11);
+ {if (true) return 1;}
+ break;
+ case 12:
+ jj_consume_token(12);
+ {if (true) return 2;}
+ break;
+ case 13:
+ jj_consume_token(13);
+ {if (true) return 3;}
+ break;
+ case 14:
+ jj_consume_token(14);
+ {if (true) return 4;}
+ break;
+ case 15:
+ jj_consume_token(15);
+ {if (true) return 5;}
+ break;
+ case 16:
+ jj_consume_token(16);
+ {if (true) return 6;}
+ break;
+ case 17:
+ jj_consume_token(17);
+ {if (true) return 7;}
+ break;
+ case 18:
+ jj_consume_token(18);
+ {if (true) return 8;}
+ break;
+ case 19:
+ jj_consume_token(19);
+ {if (true) return 9;}
+ break;
+ case 20:
+ jj_consume_token(20);
+ {if (true) return 10;}
+ break;
+ case 21:
+ jj_consume_token(21);
+ {if (true) return 11;}
+ break;
+ case 22:
+ jj_consume_token(22);
+ {if (true) return 12;}
+ break;
+ default:
+ jj_la1[3] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ throw new Error("Missing return statement in function");
+ }
+
+ final public String year() throws ParseException {
+ Token t;
+ t = jj_consume_token(DIGITS);
+ {if (true) return t.image;}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public Time time() throws ParseException {
+ int h, m, s=0, z;
+ h = hour();
+ jj_consume_token(23);
+ m = minute();
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 23:
+ jj_consume_token(23);
+ s = second();
+ break;
+ default:
+ jj_la1[4] = jj_gen;
+ ;
+ }
+ z = zone();
+ {if (true) return new Time(h, m, s, z);}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int hour() throws ParseException {
+ Token t;
+ t = jj_consume_token(DIGITS);
+ {if (true) return parseDigits(t);}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int minute() throws ParseException {
+ Token t;
+ t = jj_consume_token(DIGITS);
+ {if (true) return parseDigits(t);}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int second() throws ParseException {
+ Token t;
+ t = jj_consume_token(DIGITS);
+ {if (true) return parseDigits(t);}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int zone() throws ParseException {
+ Token t, u; int z;
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case OFFSETDIR:
+ t = jj_consume_token(OFFSETDIR);
+ u = jj_consume_token(DIGITS);
+ z=parseDigits(u)*(t.image.equals("-") ? -1 : 1);
+ break;
+ case 25:
+ case 26:
+ case 27:
+ case 28:
+ case 29:
+ case 30:
+ case 31:
+ case 32:
+ case 33:
+ case 34:
+ case MILITARY_ZONE:
+ z = obs_zone();
+ break;
+ default:
+ jj_la1[5] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ {if (true) return z;}
+ throw new Error("Missing return statement in function");
+ }
+
+ final public int obs_zone() throws ParseException {
+ Token t; int z;
+ switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+ case 25:
+ jj_consume_token(25);
+ z=0;
+ break;
+ case 26:
+ jj_consume_token(26);
+ z=0;
+ break;
+ case 27:
+ jj_consume_token(27);
+ z=-5;
+ break;
+ case 28:
+ jj_consume_token(28);
+ z=-4;
+ break;
+ case 29:
+ jj_consume_token(29);
+ z=-6;
+ break;
+ case 30:
+ jj_consume_token(30);
+ z=-5;
+ break;
+ case 31:
+ jj_consume_token(31);
+ z=-7;
+ break;
+ case 32:
+ jj_consume_token(32);
+ z=-6;
+ break;
+ case 33:
+ jj_consume_token(33);
+ z=-8;
+ break;
+ case 34:
+ jj_consume_token(34);
+ z=-7;
+ break;
+ case MILITARY_ZONE:
+ t = jj_consume_token(MILITARY_ZONE);
+ z=getMilitaryZoneOffset(t.image.charAt(0));
+ break;
+ default:
+ jj_la1[6] = jj_gen;
+ jj_consume_token(-1);
+ throw new ParseException();
+ }
+ {if (true) return z * 100;}
+ throw new Error("Missing return statement in function");
+ }
+
+ public DateTimeParserTokenManager token_source;
+ SimpleCharStream jj_input_stream;
+ public Token token, jj_nt;
+ private int jj_ntk;
+ private int jj_gen;
+ final private int[] jj_la1 = new int[7];
+ static private int[] jj_la1_0;
+ static private int[] jj_la1_1;
+ static {
+ jj_la1_0();
+ jj_la1_1();
+ }
+ private static void jj_la1_0() {
+ jj_la1_0 = new int[] {0x2,0x7f0,0x7f0,0x7ff800,0x800000,0xff000000,0xfe000000,};
+ }
+ private static void jj_la1_1() {
+ jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0xf,0xf,};
+ }
+
+ public DateTimeParser(java.io.InputStream stream) {
+ this(stream, null);
+ }
+ public DateTimeParser(java.io.InputStream stream, String encoding) {
+ try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+ token_source = new DateTimeParserTokenManager(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 7; i++) jj_la1[i] = -1;
+ }
+
+ public void ReInit(java.io.InputStream stream) {
+ ReInit(stream, null);
+ }
+ public void ReInit(java.io.InputStream stream, String encoding) {
+ try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+ token_source.ReInit(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 7; i++) jj_la1[i] = -1;
+ }
+
+ public DateTimeParser(java.io.Reader stream) {
+ jj_input_stream = new SimpleCharStream(stream, 1, 1);
+ token_source = new DateTimeParserTokenManager(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 7; i++) jj_la1[i] = -1;
+ }
+
+ public void ReInit(java.io.Reader stream) {
+ jj_input_stream.ReInit(stream, 1, 1);
+ token_source.ReInit(jj_input_stream);
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 7; i++) jj_la1[i] = -1;
+ }
+
+ public DateTimeParser(DateTimeParserTokenManager tm) {
+ token_source = tm;
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 7; i++) jj_la1[i] = -1;
+ }
+
+ public void ReInit(DateTimeParserTokenManager tm) {
+ token_source = tm;
+ token = new Token();
+ jj_ntk = -1;
+ jj_gen = 0;
+ for (int i = 0; i < 7; i++) jj_la1[i] = -1;
+ }
+
+ final private Token jj_consume_token(int kind) throws ParseException {
+ Token oldToken;
+ if ((oldToken = token).next != null) token = token.next;
+ else token = token.next = token_source.getNextToken();
+ jj_ntk = -1;
+ if (token.kind == kind) {
+ jj_gen++;
+ return token;
+ }
+ token = oldToken;
+ jj_kind = kind;
+ throw generateParseException();
+ }
+
+ final public Token getNextToken() {
+ if (token.next != null) token = token.next;
+ else token = token.next = token_source.getNextToken();
+ jj_ntk = -1;
+ jj_gen++;
+ return token;
+ }
+
+ final public Token getToken(int index) {
+ Token t = token;
+ for (int i = 0; i < index; i++) {
+ if (t.next != null) t = t.next;
+ else t = t.next = token_source.getNextToken();
+ }
+ return t;
+ }
+
+ final private int jj_ntk() {
+ if ((jj_nt=token.next) == null)
+ return (jj_ntk = (token.next=token_source.getNextToken()).kind);
+ else
+ return (jj_ntk = jj_nt.kind);
+ }
+
+ private Vector<int[]> jj_expentries = new Vector<int[]>();
+ private int[] jj_expentry;
+ private int jj_kind = -1;
+
+ public ParseException generateParseException() {
+ jj_expentries.removeAllElements();
+ boolean[] la1tokens = new boolean[49];
+ for (int i = 0; i < 49; i++) {
+ la1tokens[i] = false;
+ }
+ if (jj_kind >= 0) {
+ la1tokens[jj_kind] = true;
+ jj_kind = -1;
+ }
+ for (int i = 0; i < 7; i++) {
+ if (jj_la1[i] == jj_gen) {
+ for (int j = 0; j < 32; j++) {
+ if ((jj_la1_0[i] & (1<<j)) != 0) {
+ la1tokens[j] = true;
+ }
+ if ((jj_la1_1[i] & (1<<j)) != 0) {
+ la1tokens[32+j] = true;
+ }
+ }
+ }
+ }
+ for (int i = 0; i < 49; i++) {
+ if (la1tokens[i]) {
+ jj_expentry = new int[1];
+ jj_expentry[0] = i;
+ jj_expentries.addElement(jj_expentry);
+ }
+ }
+ int[][] exptokseq = new int[jj_expentries.size()][];
+ for (int i = 0; i < jj_expentries.size(); i++) {
+ exptokseq[i] = jj_expentries.elementAt(i);
+ }
+ return new ParseException(token, exptokseq, tokenImage);
+ }
+
+ final public void enable_tracing() {
+ }
+
+ final public void disable_tracing() {
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
new file mode 100644
index 000000000..2c203db2e
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java
@@ -0,0 +1,86 @@
+/* Generated By:JavaCC: Do not edit this line. DateTimeParserConstants.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+
+public interface DateTimeParserConstants {
+
+ int EOF = 0;
+ int OFFSETDIR = 24;
+ int MILITARY_ZONE = 35;
+ int WS = 36;
+ int COMMENT = 38;
+ int DIGITS = 46;
+ int QUOTEDPAIR = 47;
+ int ANY = 48;
+
+ int DEFAULT = 0;
+ int INCOMMENT = 1;
+ int NESTED_COMMENT = 2;
+
+ String[] tokenImage = {
+ "<EOF>",
+ "\"\\r\"",
+ "\"\\n\"",
+ "\",\"",
+ "\"Mon\"",
+ "\"Tue\"",
+ "\"Wed\"",
+ "\"Thu\"",
+ "\"Fri\"",
+ "\"Sat\"",
+ "\"Sun\"",
+ "\"Jan\"",
+ "\"Feb\"",
+ "\"Mar\"",
+ "\"Apr\"",
+ "\"May\"",
+ "\"Jun\"",
+ "\"Jul\"",
+ "\"Aug\"",
+ "\"Sep\"",
+ "\"Oct\"",
+ "\"Nov\"",
+ "\"Dec\"",
+ "\":\"",
+ "<OFFSETDIR>",
+ "\"UT\"",
+ "\"GMT\"",
+ "\"EST\"",
+ "\"EDT\"",
+ "\"CST\"",
+ "\"CDT\"",
+ "\"MST\"",
+ "\"MDT\"",
+ "\"PST\"",
+ "\"PDT\"",
+ "<MILITARY_ZONE>",
+ "<WS>",
+ "\"(\"",
+ "\")\"",
+ "<token of kind 39>",
+ "\"(\"",
+ "<token of kind 41>",
+ "<token of kind 42>",
+ "\"(\"",
+ "\")\"",
+ "<token of kind 45>",
+ "<DIGITS>",
+ "<QUOTEDPAIR>",
+ "<ANY>",
+ };
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
new file mode 100644
index 000000000..4b2d2fd95
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java
@@ -0,0 +1,882 @@
+/* Generated By:JavaCC: Do not edit this line. DateTimeParserTokenManager.java */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+import org.apache.james.mime4j.field.datetime.DateTime;
+import java.util.Calendar;
+
+public class DateTimeParserTokenManager implements DateTimeParserConstants
+{
+ // Keeps track of how many levels of comment nesting
+ // we've encountered. This is only used when the 2nd
+ // level is reached, for example ((this)), not (this).
+ // This is because the outermost level must be treated
+ // specially anyway, because the outermost ")" has a
+ // different token type than inner ")" instances.
+ static int commentNest;
+ public java.io.PrintStream debugStream = System.out;
+ public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
+private final int jjStopStringLiteralDfa_0(int pos, long active0)
+{
+ switch (pos)
+ {
+ case 0:
+ if ((active0 & 0x7fe7cf7f0L) != 0L)
+ {
+ jjmatchedKind = 35;
+ return -1;
+ }
+ return -1;
+ case 1:
+ if ((active0 & 0x7fe7cf7f0L) != 0L)
+ {
+ if (jjmatchedPos == 0)
+ {
+ jjmatchedKind = 35;
+ jjmatchedPos = 0;
+ }
+ return -1;
+ }
+ return -1;
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_0(int pos, long active0)
+{
+ return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
+}
+private final int jjStopAtPos(int pos, int kind)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ return pos + 1;
+}
+private final int jjStartNfaWithStates_0(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_0(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_0()
+{
+ switch(curChar)
+ {
+ case 10:
+ return jjStopAtPos(0, 2);
+ case 13:
+ return jjStopAtPos(0, 1);
+ case 40:
+ return jjStopAtPos(0, 37);
+ case 44:
+ return jjStopAtPos(0, 3);
+ case 58:
+ return jjStopAtPos(0, 23);
+ case 65:
+ return jjMoveStringLiteralDfa1_0(0x44000L);
+ case 67:
+ return jjMoveStringLiteralDfa1_0(0x60000000L);
+ case 68:
+ return jjMoveStringLiteralDfa1_0(0x400000L);
+ case 69:
+ return jjMoveStringLiteralDfa1_0(0x18000000L);
+ case 70:
+ return jjMoveStringLiteralDfa1_0(0x1100L);
+ case 71:
+ return jjMoveStringLiteralDfa1_0(0x4000000L);
+ case 74:
+ return jjMoveStringLiteralDfa1_0(0x30800L);
+ case 77:
+ return jjMoveStringLiteralDfa1_0(0x18000a010L);
+ case 78:
+ return jjMoveStringLiteralDfa1_0(0x200000L);
+ case 79:
+ return jjMoveStringLiteralDfa1_0(0x100000L);
+ case 80:
+ return jjMoveStringLiteralDfa1_0(0x600000000L);
+ case 83:
+ return jjMoveStringLiteralDfa1_0(0x80600L);
+ case 84:
+ return jjMoveStringLiteralDfa1_0(0xa0L);
+ case 85:
+ return jjMoveStringLiteralDfa1_0(0x2000000L);
+ case 87:
+ return jjMoveStringLiteralDfa1_0(0x40L);
+ default :
+ return jjMoveNfa_0(0, 0);
+ }
+}
+private final int jjMoveStringLiteralDfa1_0(long active0)
+{
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) {
+ jjStopStringLiteralDfa_0(0, active0);
+ return 1;
+ }
+ switch(curChar)
+ {
+ case 68:
+ return jjMoveStringLiteralDfa2_0(active0, 0x550000000L);
+ case 77:
+ return jjMoveStringLiteralDfa2_0(active0, 0x4000000L);
+ case 83:
+ return jjMoveStringLiteralDfa2_0(active0, 0x2a8000000L);
+ case 84:
+ if ((active0 & 0x2000000L) != 0L)
+ return jjStopAtPos(1, 25);
+ break;
+ case 97:
+ return jjMoveStringLiteralDfa2_0(active0, 0xaa00L);
+ case 99:
+ return jjMoveStringLiteralDfa2_0(active0, 0x100000L);
+ case 101:
+ return jjMoveStringLiteralDfa2_0(active0, 0x481040L);
+ case 104:
+ return jjMoveStringLiteralDfa2_0(active0, 0x80L);
+ case 111:
+ return jjMoveStringLiteralDfa2_0(active0, 0x200010L);
+ case 112:
+ return jjMoveStringLiteralDfa2_0(active0, 0x4000L);
+ case 114:
+ return jjMoveStringLiteralDfa2_0(active0, 0x100L);
+ case 117:
+ return jjMoveStringLiteralDfa2_0(active0, 0x70420L);
+ default :
+ break;
+ }
+ return jjStartNfa_0(0, active0);
+}
+private final int jjMoveStringLiteralDfa2_0(long old0, long active0)
+{
+ if (((active0 &= old0)) == 0L)
+ return jjStartNfa_0(0, old0);
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) {
+ jjStopStringLiteralDfa_0(1, active0);
+ return 2;
+ }
+ switch(curChar)
+ {
+ case 84:
+ if ((active0 & 0x4000000L) != 0L)
+ return jjStopAtPos(2, 26);
+ else if ((active0 & 0x8000000L) != 0L)
+ return jjStopAtPos(2, 27);
+ else if ((active0 & 0x10000000L) != 0L)
+ return jjStopAtPos(2, 28);
+ else if ((active0 & 0x20000000L) != 0L)
+ return jjStopAtPos(2, 29);
+ else if ((active0 & 0x40000000L) != 0L)
+ return jjStopAtPos(2, 30);
+ else if ((active0 & 0x80000000L) != 0L)
+ return jjStopAtPos(2, 31);
+ else if ((active0 & 0x100000000L) != 0L)
+ return jjStopAtPos(2, 32);
+ else if ((active0 & 0x200000000L) != 0L)
+ return jjStopAtPos(2, 33);
+ else if ((active0 & 0x400000000L) != 0L)
+ return jjStopAtPos(2, 34);
+ break;
+ case 98:
+ if ((active0 & 0x1000L) != 0L)
+ return jjStopAtPos(2, 12);
+ break;
+ case 99:
+ if ((active0 & 0x400000L) != 0L)
+ return jjStopAtPos(2, 22);
+ break;
+ case 100:
+ if ((active0 & 0x40L) != 0L)
+ return jjStopAtPos(2, 6);
+ break;
+ case 101:
+ if ((active0 & 0x20L) != 0L)
+ return jjStopAtPos(2, 5);
+ break;
+ case 103:
+ if ((active0 & 0x40000L) != 0L)
+ return jjStopAtPos(2, 18);
+ break;
+ case 105:
+ if ((active0 & 0x100L) != 0L)
+ return jjStopAtPos(2, 8);
+ break;
+ case 108:
+ if ((active0 & 0x20000L) != 0L)
+ return jjStopAtPos(2, 17);
+ break;
+ case 110:
+ if ((active0 & 0x10L) != 0L)
+ return jjStopAtPos(2, 4);
+ else if ((active0 & 0x400L) != 0L)
+ return jjStopAtPos(2, 10);
+ else if ((active0 & 0x800L) != 0L)
+ return jjStopAtPos(2, 11);
+ else if ((active0 & 0x10000L) != 0L)
+ return jjStopAtPos(2, 16);
+ break;
+ case 112:
+ if ((active0 & 0x80000L) != 0L)
+ return jjStopAtPos(2, 19);
+ break;
+ case 114:
+ if ((active0 & 0x2000L) != 0L)
+ return jjStopAtPos(2, 13);
+ else if ((active0 & 0x4000L) != 0L)
+ return jjStopAtPos(2, 14);
+ break;
+ case 116:
+ if ((active0 & 0x200L) != 0L)
+ return jjStopAtPos(2, 9);
+ else if ((active0 & 0x100000L) != 0L)
+ return jjStopAtPos(2, 20);
+ break;
+ case 117:
+ if ((active0 & 0x80L) != 0L)
+ return jjStopAtPos(2, 7);
+ break;
+ case 118:
+ if ((active0 & 0x200000L) != 0L)
+ return jjStopAtPos(2, 21);
+ break;
+ case 121:
+ if ((active0 & 0x8000L) != 0L)
+ return jjStopAtPos(2, 15);
+ break;
+ default :
+ break;
+ }
+ return jjStartNfa_0(1, active0);
+}
+private final void jjCheckNAdd(int state)
+{
+ if (jjrounds[state] != jjround)
+ {
+ jjstateSet[jjnewStateCnt++] = state;
+ jjrounds[state] = jjround;
+ }
+}
+private final void jjAddStates(int start, int end)
+{
+ do {
+ jjstateSet[jjnewStateCnt++] = jjnextStates[start];
+ } while (start++ != end);
+}
+private final void jjCheckNAddTwoStates(int state1, int state2)
+{
+ jjCheckNAdd(state1);
+ jjCheckNAdd(state2);
+}
+private final void jjCheckNAddStates(int start, int end)
+{
+ do {
+ jjCheckNAdd(jjnextStates[start]);
+ } while (start++ != end);
+}
+private final void jjCheckNAddStates(int start)
+{
+ jjCheckNAdd(jjnextStates[start]);
+ jjCheckNAdd(jjnextStates[start + 1]);
+}
+private final int jjMoveNfa_0(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 4;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((0x3ff000000000000L & l) != 0L)
+ {
+ if (kind > 46)
+ kind = 46;
+ jjCheckNAdd(3);
+ }
+ else if ((0x100000200L & l) != 0L)
+ {
+ if (kind > 36)
+ kind = 36;
+ jjCheckNAdd(2);
+ }
+ else if ((0x280000000000L & l) != 0L)
+ {
+ if (kind > 24)
+ kind = 24;
+ }
+ break;
+ case 2:
+ if ((0x100000200L & l) == 0L)
+ break;
+ kind = 36;
+ jjCheckNAdd(2);
+ break;
+ case 3:
+ if ((0x3ff000000000000L & l) == 0L)
+ break;
+ kind = 46;
+ jjCheckNAdd(3);
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((0x7fffbfe07fffbfeL & l) != 0L)
+ kind = 35;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 4 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_1(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_1(int pos, long active0)
+{
+ return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_1(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_1(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_1()
+{
+ switch(curChar)
+ {
+ case 40:
+ return jjStopAtPos(0, 40);
+ case 41:
+ return jjStopAtPos(0, 38);
+ default :
+ return jjMoveNfa_1(0, 0);
+ }
+}
+static final long[] jjbitVec0 = {
+ 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL
+};
+private final int jjMoveNfa_1(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 41)
+ kind = 41;
+ break;
+ case 1:
+ if (kind > 39)
+ kind = 39;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 41)
+ kind = 41;
+ if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 39)
+ kind = 39;
+ break;
+ case 2:
+ if (kind > 41)
+ kind = 41;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 41)
+ kind = 41;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 39)
+ kind = 39;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+private final int jjStopStringLiteralDfa_2(int pos, long active0)
+{
+ switch (pos)
+ {
+ default :
+ return -1;
+ }
+}
+private final int jjStartNfa_2(int pos, long active0)
+{
+ return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
+}
+private final int jjStartNfaWithStates_2(int pos, int kind, int state)
+{
+ jjmatchedKind = kind;
+ jjmatchedPos = pos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return pos + 1; }
+ return jjMoveNfa_2(state, pos + 1);
+}
+private final int jjMoveStringLiteralDfa0_2()
+{
+ switch(curChar)
+ {
+ case 40:
+ return jjStopAtPos(0, 43);
+ case 41:
+ return jjStopAtPos(0, 44);
+ default :
+ return jjMoveNfa_2(0, 0);
+ }
+}
+private final int jjMoveNfa_2(int startState, int curPos)
+{
+ int[] nextStates;
+ int startsAt = 0;
+ jjnewStateCnt = 3;
+ int i = 1;
+ jjstateSet[0] = startState;
+ int j, kind = 0x7fffffff;
+ for (;;)
+ {
+ if (++jjround == 0x7fffffff)
+ ReInitRounds();
+ if (curChar < 64)
+ {
+ long l = 1L << curChar;
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 45)
+ kind = 45;
+ break;
+ case 1:
+ if (kind > 42)
+ kind = 42;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else if (curChar < 128)
+ {
+ long l = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if (kind > 45)
+ kind = 45;
+ if (curChar == 92)
+ jjstateSet[jjnewStateCnt++] = 1;
+ break;
+ case 1:
+ if (kind > 42)
+ kind = 42;
+ break;
+ case 2:
+ if (kind > 45)
+ kind = 45;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ else
+ {
+ int i2 = (curChar & 0xff) >> 6;
+ long l2 = 1L << (curChar & 077);
+ MatchLoop: do
+ {
+ switch(jjstateSet[--i])
+ {
+ case 0:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 45)
+ kind = 45;
+ break;
+ case 1:
+ if ((jjbitVec0[i2] & l2) != 0L && kind > 42)
+ kind = 42;
+ break;
+ default : break;
+ }
+ } while(i != startsAt);
+ }
+ if (kind != 0x7fffffff)
+ {
+ jjmatchedKind = kind;
+ jjmatchedPos = curPos;
+ kind = 0x7fffffff;
+ }
+ ++curPos;
+ if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt)))
+ return curPos;
+ try { curChar = input_stream.readChar(); }
+ catch(java.io.IOException e) { return curPos; }
+ }
+}
+static final int[] jjnextStates = {
+};
+public static final String[] jjstrLiteralImages = {
+"", "\15", "\12", "\54", "\115\157\156", "\124\165\145", "\127\145\144",
+"\124\150\165", "\106\162\151", "\123\141\164", "\123\165\156", "\112\141\156",
+"\106\145\142", "\115\141\162", "\101\160\162", "\115\141\171", "\112\165\156",
+"\112\165\154", "\101\165\147", "\123\145\160", "\117\143\164", "\116\157\166",
+"\104\145\143", "\72", null, "\125\124", "\107\115\124", "\105\123\124", "\105\104\124",
+"\103\123\124", "\103\104\124", "\115\123\124", "\115\104\124", "\120\123\124",
+"\120\104\124", null, null, null, null, null, null, null, null, null, null, null, null, null,
+null, };
+public static final String[] lexStateNames = {
+ "DEFAULT",
+ "INCOMMENT",
+ "NESTED_COMMENT",
+};
+public static final int[] jjnewLexState = {
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1,
+};
+static final long[] jjtoToken = {
+ 0x400fffffffffL,
+};
+static final long[] jjtoSkip = {
+ 0x5000000000L,
+};
+static final long[] jjtoSpecial = {
+ 0x1000000000L,
+};
+static final long[] jjtoMore = {
+ 0x3fa000000000L,
+};
+protected SimpleCharStream input_stream;
+private final int[] jjrounds = new int[4];
+private final int[] jjstateSet = new int[8];
+StringBuffer image;
+int jjimageLen;
+int lengthOfMatch;
+protected char curChar;
+public DateTimeParserTokenManager(SimpleCharStream stream){
+ if (SimpleCharStream.staticFlag)
+ throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
+ input_stream = stream;
+}
+public DateTimeParserTokenManager(SimpleCharStream stream, int lexState){
+ this(stream);
+ SwitchTo(lexState);
+}
+public void ReInit(SimpleCharStream stream)
+{
+ jjmatchedPos = jjnewStateCnt = 0;
+ curLexState = defaultLexState;
+ input_stream = stream;
+ ReInitRounds();
+}
+private final void ReInitRounds()
+{
+ int i;
+ jjround = 0x80000001;
+ for (i = 4; i-- > 0;)
+ jjrounds[i] = 0x80000000;
+}
+public void ReInit(SimpleCharStream stream, int lexState)
+{
+ ReInit(stream);
+ SwitchTo(lexState);
+}
+public void SwitchTo(int lexState)
+{
+ if (lexState >= 3 || lexState < 0)
+ throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
+ else
+ curLexState = lexState;
+}
+
+protected Token jjFillToken()
+{
+ Token t = Token.newToken(jjmatchedKind);
+ t.kind = jjmatchedKind;
+ String im = jjstrLiteralImages[jjmatchedKind];
+ t.image = (im == null) ? input_stream.GetImage() : im;
+ t.beginLine = input_stream.getBeginLine();
+ t.beginColumn = input_stream.getBeginColumn();
+ t.endLine = input_stream.getEndLine();
+ t.endColumn = input_stream.getEndColumn();
+ return t;
+}
+
+int curLexState = 0;
+int defaultLexState = 0;
+int jjnewStateCnt;
+int jjround;
+int jjmatchedPos;
+int jjmatchedKind;
+
+public Token getNextToken()
+{
+ int kind;
+ Token specialToken = null;
+ Token matchedToken;
+ int curPos = 0;
+
+ EOFLoop :
+ for (;;)
+ {
+ try
+ {
+ curChar = input_stream.BeginToken();
+ }
+ catch(java.io.IOException e)
+ {
+ jjmatchedKind = 0;
+ matchedToken = jjFillToken();
+ matchedToken.specialToken = specialToken;
+ return matchedToken;
+ }
+ image = null;
+ jjimageLen = 0;
+
+ for (;;)
+ {
+ switch(curLexState)
+ {
+ case 0:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_0();
+ break;
+ case 1:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_1();
+ break;
+ case 2:
+ jjmatchedKind = 0x7fffffff;
+ jjmatchedPos = 0;
+ curPos = jjMoveStringLiteralDfa0_2();
+ break;
+ }
+ if (jjmatchedKind != 0x7fffffff)
+ {
+ if (jjmatchedPos + 1 < curPos)
+ input_stream.backup(curPos - jjmatchedPos - 1);
+ if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ matchedToken = jjFillToken();
+ matchedToken.specialToken = specialToken;
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ return matchedToken;
+ }
+ else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L)
+ {
+ matchedToken = jjFillToken();
+ if (specialToken == null)
+ specialToken = matchedToken;
+ else
+ {
+ matchedToken.specialToken = specialToken;
+ specialToken = (specialToken.next = matchedToken);
+ }
+ }
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ continue EOFLoop;
+ }
+ MoreLexicalActions();
+ if (jjnewLexState[jjmatchedKind] != -1)
+ curLexState = jjnewLexState[jjmatchedKind];
+ curPos = 0;
+ jjmatchedKind = 0x7fffffff;
+ try {
+ curChar = input_stream.readChar();
+ continue;
+ }
+ catch (java.io.IOException e1) { }
+ }
+ int error_line = input_stream.getEndLine();
+ int error_column = input_stream.getEndColumn();
+ String error_after = null;
+ boolean EOFSeen = false;
+ try { input_stream.readChar(); input_stream.backup(1); }
+ catch (java.io.IOException e1) {
+ EOFSeen = true;
+ error_after = curPos <= 1 ? "" : input_stream.GetImage();
+ if (curChar == '\n' || curChar == '\r') {
+ error_line++;
+ error_column = 0;
+ }
+ else
+ error_column++;
+ }
+ if (!EOFSeen) {
+ input_stream.backup(1);
+ error_after = curPos <= 1 ? "" : input_stream.GetImage();
+ }
+ throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR);
+ }
+ }
+}
+
+void MoreLexicalActions()
+{
+ jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
+ switch(jjmatchedKind)
+ {
+ case 39 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 40 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ commentNest = 1;
+ break;
+ case 42 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ image.deleteCharAt(image.length() - 2);
+ break;
+ case 43 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ ++commentNest;
+ break;
+ case 44 :
+ if (image == null)
+ image = new StringBuffer();
+ image.append(input_stream.GetSuffix(jjimageLen));
+ jjimageLen = 0;
+ --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT);
+ break;
+ default :
+ break;
+ }
+}
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java
new file mode 100644
index 000000000..13b3ff097
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java
@@ -0,0 +1,207 @@
+/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+
+/**
+ * This exception is thrown when parse errors are encountered.
+ * You can explicitly create objects of this exception type by
+ * calling the method generateParseException in the generated
+ * parser.
+ *
+ * You can modify this class to customize your error reporting
+ * mechanisms so long as you retain the public fields.
+ */
+public class ParseException extends Exception {
+
+ /**
+ * This constructor is used by the method "generateParseException"
+ * in the generated parser. Calling this constructor generates
+ * a new object of this type with the fields "currentToken",
+ * "expectedTokenSequences", and "tokenImage" set. The boolean
+ * flag "specialConstructor" is also set to true to indicate that
+ * this constructor was used to create this object.
+ * This constructor calls its super class with the empty string
+ * to force the "toString" method of parent class "Throwable" to
+ * print the error message in the form:
+ * ParseException: <result of getMessage>
+ */
+ public ParseException(Token currentTokenVal,
+ int[][] expectedTokenSequencesVal,
+ String[] tokenImageVal
+ )
+ {
+ super("");
+ specialConstructor = true;
+ currentToken = currentTokenVal;
+ expectedTokenSequences = expectedTokenSequencesVal;
+ tokenImage = tokenImageVal;
+ }
+
+ /**
+ * The following constructors are for use by you for whatever
+ * purpose you can think of. Constructing the exception in this
+ * manner makes the exception behave in the normal way - i.e., as
+ * documented in the class "Throwable". The fields "errorToken",
+ * "expectedTokenSequences", and "tokenImage" do not contain
+ * relevant information. The JavaCC generated code does not use
+ * these constructors.
+ */
+
+ public ParseException() {
+ super();
+ specialConstructor = false;
+ }
+
+ public ParseException(String message) {
+ super(message);
+ specialConstructor = false;
+ }
+
+ /**
+ * This variable determines which constructor was used to create
+ * this object and thereby affects the semantics of the
+ * "getMessage" method (see below).
+ */
+ protected boolean specialConstructor;
+
+ /**
+ * This is the last token that has been consumed successfully. If
+ * this object has been created due to a parse error, the token
+ * followng this token will (therefore) be the first error token.
+ */
+ public Token currentToken;
+
+ /**
+ * Each entry in this array is an array of integers. Each array
+ * of integers represents a sequence of tokens (by their ordinal
+ * values) that is expected at this point of the parse.
+ */
+ public int[][] expectedTokenSequences;
+
+ /**
+ * This is a reference to the "tokenImage" array of the generated
+ * parser within which the parse error occurred. This array is
+ * defined in the generated ...Constants interface.
+ */
+ public String[] tokenImage;
+
+ /**
+ * This method has the standard behavior when this object has been
+ * created using the standard constructors. Otherwise, it uses
+ * "currentToken" and "expectedTokenSequences" to generate a parse
+ * error message and returns it. If this object has been created
+ * due to a parse error, and you do not catch it (it gets thrown
+ * from the parser), then this method is called during the printing
+ * of the final stack trace, and hence the correct error message
+ * gets displayed.
+ */
+ public String getMessage() {
+ if (!specialConstructor) {
+ return super.getMessage();
+ }
+ StringBuffer expected = new StringBuffer();
+ int maxSize = 0;
+ for (int i = 0; i < expectedTokenSequences.length; i++) {
+ if (maxSize < expectedTokenSequences[i].length) {
+ maxSize = expectedTokenSequences[i].length;
+ }
+ for (int j = 0; j < expectedTokenSequences[i].length; j++) {
+ expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" ");
+ }
+ if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) {
+ expected.append("...");
+ }
+ expected.append(eol).append(" ");
+ }
+ String retval = "Encountered \"";
+ Token tok = currentToken.next;
+ for (int i = 0; i < maxSize; i++) {
+ if (i != 0) retval += " ";
+ if (tok.kind == 0) {
+ retval += tokenImage[0];
+ break;
+ }
+ retval += add_escapes(tok.image);
+ tok = tok.next;
+ }
+ retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn;
+ retval += "." + eol;
+ if (expectedTokenSequences.length == 1) {
+ retval += "Was expecting:" + eol + " ";
+ } else {
+ retval += "Was expecting one of:" + eol + " ";
+ }
+ retval += expected.toString();
+ return retval;
+ }
+
+ /**
+ * The end of line string for this machine.
+ */
+ protected String eol = System.getProperty("line.separator", "\n");
+
+ /**
+ * Used to convert raw characters to their escaped version
+ * when these raw version cannot be used as part of an ASCII
+ * string literal.
+ */
+ protected String add_escapes(String str) {
+ StringBuffer retval = new StringBuffer();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
new file mode 100644
index 000000000..2724529f7
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java
@@ -0,0 +1,454 @@
+/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+
+/**
+ * An implementation of interface CharStream, where the stream is assumed to
+ * contain only ASCII characters (without unicode processing).
+ */
+
+public class SimpleCharStream
+{
+ public static final boolean staticFlag = false;
+ int bufsize;
+ int available;
+ int tokenBegin;
+ public int bufpos = -1;
+ protected int bufline[];
+ protected int bufcolumn[];
+
+ protected int column = 0;
+ protected int line = 1;
+
+ protected boolean prevCharIsCR = false;
+ protected boolean prevCharIsLF = false;
+
+ protected java.io.Reader inputStream;
+
+ protected char[] buffer;
+ protected int maxNextCharInd = 0;
+ protected int inBuf = 0;
+ protected int tabSize = 8;
+
+ protected void setTabSize(int i) { tabSize = i; }
+ protected int getTabSize(int i) { return tabSize; }
+
+
+ protected void ExpandBuff(boolean wrapAround)
+ {
+ char[] newbuffer = new char[bufsize + 2048];
+ int newbufline[] = new int[bufsize + 2048];
+ int newbufcolumn[] = new int[bufsize + 2048];
+
+ try
+ {
+ if (wrapAround)
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ System.arraycopy(buffer, 0, newbuffer,
+ bufsize - tokenBegin, bufpos);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos += (bufsize - tokenBegin));
+ }
+ else
+ {
+ System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin);
+ buffer = newbuffer;
+
+ System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin);
+ bufline = newbufline;
+
+ System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin);
+ bufcolumn = newbufcolumn;
+
+ maxNextCharInd = (bufpos -= tokenBegin);
+ }
+ }
+ catch (Throwable t)
+ {
+ throw new Error(t.getMessage());
+ }
+
+
+ bufsize += 2048;
+ available = bufsize;
+ tokenBegin = 0;
+ }
+
+ protected void FillBuff() throws java.io.IOException
+ {
+ if (maxNextCharInd == available)
+ {
+ if (available == bufsize)
+ {
+ if (tokenBegin > 2048)
+ {
+ bufpos = maxNextCharInd = 0;
+ available = tokenBegin;
+ }
+ else if (tokenBegin < 0)
+ bufpos = maxNextCharInd = 0;
+ else
+ ExpandBuff(false);
+ }
+ else if (available > tokenBegin)
+ available = bufsize;
+ else if ((tokenBegin - available) < 2048)
+ ExpandBuff(true);
+ else
+ available = tokenBegin;
+ }
+
+ int i;
+ try {
+ if ((i = inputStream.read(buffer, maxNextCharInd,
+ available - maxNextCharInd)) == -1)
+ {
+ inputStream.close();
+ throw new java.io.IOException();
+ }
+ else
+ maxNextCharInd += i;
+ return;
+ }
+ catch(java.io.IOException e) {
+ --bufpos;
+ backup(0);
+ if (tokenBegin == -1)
+ tokenBegin = bufpos;
+ throw e;
+ }
+ }
+
+ public char BeginToken() throws java.io.IOException
+ {
+ tokenBegin = -1;
+ char c = readChar();
+ tokenBegin = bufpos;
+
+ return c;
+ }
+
+ protected void UpdateLineColumn(char c)
+ {
+ column++;
+
+ if (prevCharIsLF)
+ {
+ prevCharIsLF = false;
+ line += (column = 1);
+ }
+ else if (prevCharIsCR)
+ {
+ prevCharIsCR = false;
+ if (c == '\n')
+ {
+ prevCharIsLF = true;
+ }
+ else
+ line += (column = 1);
+ }
+
+ switch (c)
+ {
+ case '\r' :
+ prevCharIsCR = true;
+ break;
+ case '\n' :
+ prevCharIsLF = true;
+ break;
+ case '\t' :
+ column--;
+ column += (tabSize - (column % tabSize));
+ break;
+ default :
+ break;
+ }
+
+ bufline[bufpos] = line;
+ bufcolumn[bufpos] = column;
+ }
+
+ public char readChar() throws java.io.IOException
+ {
+ if (inBuf > 0)
+ {
+ --inBuf;
+
+ if (++bufpos == bufsize)
+ bufpos = 0;
+
+ return buffer[bufpos];
+ }
+
+ if (++bufpos >= maxNextCharInd)
+ FillBuff();
+
+ char c = buffer[bufpos];
+
+ UpdateLineColumn(c);
+ return (c);
+ }
+
+ /**
+ * @deprecated
+ * @see #getEndColumn
+ */
+ @Deprecated
+ public int getColumn() {
+ return bufcolumn[bufpos];
+ }
+
+ /**
+ * @deprecated
+ * @see #getEndLine
+ */
+ @Deprecated
+ public int getLine() {
+ return bufline[bufpos];
+ }
+
+ public int getEndColumn() {
+ return bufcolumn[bufpos];
+ }
+
+ public int getEndLine() {
+ return bufline[bufpos];
+ }
+
+ public int getBeginColumn() {
+ return bufcolumn[tokenBegin];
+ }
+
+ public int getBeginLine() {
+ return bufline[tokenBegin];
+ }
+
+ public void backup(int amount) {
+
+ inBuf += amount;
+ if ((bufpos -= amount) < 0)
+ bufpos += bufsize;
+ }
+
+ public SimpleCharStream(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ available = bufsize = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+
+ public SimpleCharStream(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.Reader dstream)
+ {
+ this(dstream, 1, 1, 4096);
+ }
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ inputStream = dstream;
+ line = startline;
+ column = startcolumn - 1;
+
+ if (buffer == null || buffersize != buffer.length)
+ {
+ available = bufsize = buffersize;
+ buffer = new char[buffersize];
+ bufline = new int[buffersize];
+ bufcolumn = new int[buffersize];
+ }
+ prevCharIsLF = prevCharIsCR = false;
+ tokenBegin = inBuf = maxNextCharInd = 0;
+ bufpos = -1;
+ }
+
+ public void ReInit(java.io.Reader dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+
+ public void ReInit(java.io.Reader dstream)
+ {
+ ReInit(dstream, 1, 1, 4096);
+ }
+ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
+ {
+ this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn) throws java.io.UnsupportedEncodingException
+ {
+ this(dstream, encoding, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ this(dstream, startline, startcolumn, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
+ {
+ this(dstream, encoding, 1, 1, 4096);
+ }
+
+ public SimpleCharStream(java.io.InputStream dstream)
+ {
+ this(dstream, 1, 1, 4096);
+ }
+
+ public void ReInit(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize);
+ }
+
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn, int buffersize)
+ {
+ ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize);
+ }
+
+ public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(dstream, encoding, 1, 1, 4096);
+ }
+
+ public void ReInit(java.io.InputStream dstream)
+ {
+ ReInit(dstream, 1, 1, 4096);
+ }
+ public void ReInit(java.io.InputStream dstream, String encoding, int startline,
+ int startcolumn) throws java.io.UnsupportedEncodingException
+ {
+ ReInit(dstream, encoding, startline, startcolumn, 4096);
+ }
+ public void ReInit(java.io.InputStream dstream, int startline,
+ int startcolumn)
+ {
+ ReInit(dstream, startline, startcolumn, 4096);
+ }
+ public String GetImage()
+ {
+ if (bufpos >= tokenBegin)
+ return new String(buffer, tokenBegin, bufpos - tokenBegin + 1);
+ else
+ return new String(buffer, tokenBegin, bufsize - tokenBegin) +
+ new String(buffer, 0, bufpos + 1);
+ }
+
+ public char[] GetSuffix(int len)
+ {
+ char[] ret = new char[len];
+
+ if ((bufpos + 1) >= len)
+ System.arraycopy(buffer, bufpos - len + 1, ret, 0, len);
+ else
+ {
+ System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0,
+ len - bufpos - 1);
+ System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1);
+ }
+
+ return ret;
+ }
+
+ public void Done()
+ {
+ buffer = null;
+ bufline = null;
+ bufcolumn = null;
+ }
+
+ /**
+ * Method to adjust line and column numbers for the start of a token.
+ */
+ public void adjustBeginLineColumn(int newLine, int newCol)
+ {
+ int start = tokenBegin;
+ int len;
+
+ if (bufpos >= tokenBegin)
+ {
+ len = bufpos - tokenBegin + inBuf + 1;
+ }
+ else
+ {
+ len = bufsize - tokenBegin + bufpos + 1 + inBuf;
+ }
+
+ int i = 0, j = 0, k = 0;
+ int nextColDiff = 0, columnDiff = 0;
+
+ while (i < len &&
+ bufline[j = start % bufsize] == bufline[k = ++start % bufsize])
+ {
+ bufline[j] = newLine;
+ nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j];
+ bufcolumn[j] = newCol + columnDiff;
+ columnDiff = nextColDiff;
+ i++;
+ }
+
+ if (i < len)
+ {
+ bufline[j] = newLine++;
+ bufcolumn[j] = newCol + columnDiff;
+
+ while (i++ < len)
+ {
+ if (bufline[j = start % bufsize] != bufline[++start % bufsize])
+ bufline[j] = newLine++;
+ else
+ bufline[j] = newLine;
+ }
+ }
+
+ line = bufline[j];
+ column = bufcolumn[j];
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java
new file mode 100644
index 000000000..0927a0921
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java
@@ -0,0 +1,96 @@
+/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+
+/**
+ * Describes the input token stream.
+ */
+
+public class Token {
+
+ /**
+ * An integer that describes the kind of this token. This numbering
+ * system is determined by JavaCCParser, and a table of these numbers is
+ * stored in the file ...Constants.java.
+ */
+ public int kind;
+
+ /**
+ * beginLine and beginColumn describe the position of the first character
+ * of this token; endLine and endColumn describe the position of the
+ * last character of this token.
+ */
+ public int beginLine, beginColumn, endLine, endColumn;
+
+ /**
+ * The string image of the token.
+ */
+ public String image;
+
+ /**
+ * A reference to the next regular (non-special) token from the input
+ * stream. If this is the last token from the input stream, or if the
+ * token manager has not read tokens beyond this one, this field is
+ * set to null. This is true only if this token is also a regular
+ * token. Otherwise, see below for a description of the contents of
+ * this field.
+ */
+ public Token next;
+
+ /**
+ * This field is used to access special tokens that occur prior to this
+ * token, but after the immediately preceding regular (non-special) token.
+ * If there are no such special tokens, this field is set to null.
+ * When there are more than one such special token, this field refers
+ * to the last of these special tokens, which in turn refers to the next
+ * previous special token through its specialToken field, and so on
+ * until the first special token (whose specialToken field is null).
+ * The next fields of special tokens refer to other special tokens that
+ * immediately follow it (without an intervening regular token). If there
+ * is no such token, this field is null.
+ */
+ public Token specialToken;
+
+ /**
+ * Returns the image.
+ */
+ public String toString()
+ {
+ return image;
+ }
+
+ /**
+ * Returns a new Token object, by default. However, if you want, you
+ * can create and return subclass objects based on the value of ofKind.
+ * Simply add the cases to the switch for all those special cases.
+ * For example, if you have a subclass of Token called IDToken that
+ * you want to create if ofKind is ID, simlpy add something like :
+ *
+ * case MyParserConstants.ID : return new IDToken();
+ *
+ * to the following switch statement. Then you can cast matchedToken
+ * variable to the appropriate type and use it in your lexical actions.
+ */
+ public static final Token newToken(int ofKind)
+ {
+ switch(ofKind)
+ {
+ default : return new Token();
+ }
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
new file mode 100644
index 000000000..e7043c1b7
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java
@@ -0,0 +1,148 @@
+/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */
+/*
+ * Copyright 2004 the mime4j project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.james.mime4j.field.datetime.parser;
+
+public class TokenMgrError extends Error
+{
+ /*
+ * Ordinals for various reasons why an Error of this type can be thrown.
+ */
+
+ /**
+ * Lexical error occured.
+ */
+ static final int LEXICAL_ERROR = 0;
+
+ /**
+ * An attempt wass made to create a second instance of a static token manager.
+ */
+ static final int STATIC_LEXER_ERROR = 1;
+
+ /**
+ * Tried to change to an invalid lexical state.
+ */
+ static final int INVALID_LEXICAL_STATE = 2;
+
+ /**
+ * Detected (and bailed out of) an infinite loop in the token manager.
+ */
+ static final int LOOP_DETECTED = 3;
+
+ /**
+ * Indicates the reason why the exception is thrown. It will have
+ * one of the above 4 values.
+ */
+ int errorCode;
+
+ /**
+ * Replaces unprintable characters by their espaced (or unicode escaped)
+ * equivalents in the given string
+ */
+ protected static final String addEscapes(String str) {
+ StringBuffer retval = new StringBuffer();
+ char ch;
+ for (int i = 0; i < str.length(); i++) {
+ switch (str.charAt(i))
+ {
+ case 0 :
+ continue;
+ case '\b':
+ retval.append("\\b");
+ continue;
+ case '\t':
+ retval.append("\\t");
+ continue;
+ case '\n':
+ retval.append("\\n");
+ continue;
+ case '\f':
+ retval.append("\\f");
+ continue;
+ case '\r':
+ retval.append("\\r");
+ continue;
+ case '\"':
+ retval.append("\\\"");
+ continue;
+ case '\'':
+ retval.append("\\\'");
+ continue;
+ case '\\':
+ retval.append("\\\\");
+ continue;
+ default:
+ if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
+ String s = "0000" + Integer.toString(ch, 16);
+ retval.append("\\u" + s.substring(s.length() - 4, s.length()));
+ } else {
+ retval.append(ch);
+ }
+ continue;
+ }
+ }
+ return retval.toString();
+ }
+
+ /**
+ * Returns a detailed message for the Error when it is thrown by the
+ * token manager to indicate a lexical error.
+ * Parameters :
+ * EOFSeen : indicates if EOF caused the lexicl error
+ * curLexState : lexical state in which this error occured
+ * errorLine : line number when the error occured
+ * errorColumn : column number when the error occured
+ * errorAfter : prefix that was seen before this error occured
+ * curchar : the offending character
+ * Note: You can customize the lexical error message by modifying this method.
+ */
+ protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) {
+ return("Lexical error at line " +
+ errorLine + ", column " +
+ errorColumn + ". Encountered: " +
+ (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") +
+ "after : \"" + addEscapes(errorAfter) + "\"");
+ }
+
+ /**
+ * You can also modify the body of this method to customize your error messages.
+ * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not
+ * of end-users concern, so you can return something like :
+ *
+ * "Internal Error : Please file a bug report .... "
+ *
+ * from this method for such cases in the release version of your parser.
+ */
+ public String getMessage() {
+ return super.getMessage();
+ }
+
+ /*
+ * Constructors of various flavors follow.
+ */
+
+ public TokenMgrError() {
+ }
+
+ public TokenMgrError(String message, int reason) {
+ super(message);
+ errorCode = reason;
+ }
+
+ public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) {
+ this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason);
+ }
+}
diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java
new file mode 100644
index 000000000..4e712fcdd
--- /dev/null
+++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java
@@ -0,0 +1,1249 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mime4j.util;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.TreeSet;
+
+//BEGIN android-changed: Stubbing out logging
+import org.apache.james.mime4j.Log;
+import org.apache.james.mime4j.LogFactory;
+//END android-changed
+
+/**
+ * Utility class for working with character sets. It is somewhat similar to
+ * the Java 1.4 <code>java.nio.charset.Charset</code> class but knows many
+ * more aliases and is compatible with Java 1.3. It will use a simple detection
+ * mechanism to detect what character sets the current VM supports. This will
+ * be a sub-set of the character sets listed in the
+ * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html">
+ * Java 1.5 (J2SE5.0) Supported Encodings</a> document.
+ * <p>
+ * The <a href="http://www.iana.org/assignments/character-sets">
+ * IANA Character Sets</a> document has been used to determine the preferred
+ * MIME character set names and to get a list of known aliases.
+ * <p>
+ * This is a complete list of the character sets known to this class:
+ * <table>
+ * <tr>
+ * <td>Canonical (Java) name</td>
+ * <td>MIME preferred</td>
+ * <td>Aliases</td>
+ * </tr>
+ * <tr>
+ * <td>ASCII</td>
+ * <td>US-ASCII</td>
+ * <td>ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ISO646-US us IBM367 cp367 csASCII ascii7 646 iso_646.irv:1983 </td>
+ * </tr>
+ * <tr>
+ * <td>Big5</td>
+ * <td>Big5</td>
+ * <td>csBig5 CN-Big5 BIG-FIVE BIGFIVE </td>
+ * </tr>
+ * <tr>
+ * <td>Big5_HKSCS</td>
+ * <td>Big5-HKSCS</td>
+ * <td>big5hkscs </td>
+ * </tr>
+ * <tr>
+ * <td>Big5_Solaris</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp037</td>
+ * <td>IBM037</td>
+ * <td>ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl csIBM037 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1006</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1025</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1026</td>
+ * <td>IBM1026</td>
+ * <td>csIBM1026 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1046</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1047</td>
+ * <td>IBM1047</td>
+ * <td>IBM-1047 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1097</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1098</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1112</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1122</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1123</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1124</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1140</td>
+ * <td>IBM01140</td>
+ * <td>CCSID01140 CP01140 ebcdic-us-37+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1141</td>
+ * <td>IBM01141</td>
+ * <td>CCSID01141 CP01141 ebcdic-de-273+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1142</td>
+ * <td>IBM01142</td>
+ * <td>CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1143</td>
+ * <td>IBM01143</td>
+ * <td>CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1144</td>
+ * <td>IBM01144</td>
+ * <td>CCSID01144 CP01144 ebcdic-it-280+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1145</td>
+ * <td>IBM01145</td>
+ * <td>CCSID01145 CP01145 ebcdic-es-284+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1146</td>
+ * <td>IBM01146</td>
+ * <td>CCSID01146 CP01146 ebcdic-gb-285+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1147</td>
+ * <td>IBM01147</td>
+ * <td>CCSID01147 CP01147 ebcdic-fr-297+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1148</td>
+ * <td>IBM01148</td>
+ * <td>CCSID01148 CP01148 ebcdic-international-500+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1149</td>
+ * <td>IBM01149</td>
+ * <td>CCSID01149 CP01149 ebcdic-is-871+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp1250</td>
+ * <td>windows-1250</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1251</td>
+ * <td>windows-1251</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1252</td>
+ * <td>windows-1252</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1253</td>
+ * <td>windows-1253</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1254</td>
+ * <td>windows-1254</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1255</td>
+ * <td>windows-1255</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1256</td>
+ * <td>windows-1256</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1257</td>
+ * <td>windows-1257</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1258</td>
+ * <td>windows-1258</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1381</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp1383</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp273</td>
+ * <td>IBM273</td>
+ * <td>csIBM273 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp277</td>
+ * <td>IBM277</td>
+ * <td>EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp278</td>
+ * <td>IBM278</td>
+ * <td>CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp280</td>
+ * <td>IBM280</td>
+ * <td>ebcdic-cp-it csIBM280 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp284</td>
+ * <td>IBM284</td>
+ * <td>ebcdic-cp-es csIBM284 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp285</td>
+ * <td>IBM285</td>
+ * <td>ebcdic-cp-gb csIBM285 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp297</td>
+ * <td>IBM297</td>
+ * <td>ebcdic-cp-fr csIBM297 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp33722</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp420</td>
+ * <td>IBM420</td>
+ * <td>ebcdic-cp-ar1 csIBM420 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp424</td>
+ * <td>IBM424</td>
+ * <td>ebcdic-cp-he csIBM424 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp437</td>
+ * <td>IBM437</td>
+ * <td>437 csPC8CodePage437 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp500</td>
+ * <td>IBM500</td>
+ * <td>ebcdic-cp-be ebcdic-cp-ch csIBM500 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp737</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp775</td>
+ * <td>IBM775</td>
+ * <td>csPC775Baltic </td>
+ * </tr>
+ * <tr>
+ * <td>Cp838</td>
+ * <td>IBM-Thai</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp850</td>
+ * <td>IBM850</td>
+ * <td>850 csPC850Multilingual </td>
+ * </tr>
+ * <tr>
+ * <td>Cp852</td>
+ * <td>IBM852</td>
+ * <td>852 csPCp852 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp855</td>
+ * <td>IBM855</td>
+ * <td>855 csIBM855 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp856</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp857</td>
+ * <td>IBM857</td>
+ * <td>857 csIBM857 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp858</td>
+ * <td>IBM00858</td>
+ * <td>CCSID00858 CP00858 PC-Multilingual-850+euro </td>
+ * </tr>
+ * <tr>
+ * <td>Cp860</td>
+ * <td>IBM860</td>
+ * <td>860 csIBM860 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp861</td>
+ * <td>IBM861</td>
+ * <td>861 cp-is csIBM861 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp862</td>
+ * <td>IBM862</td>
+ * <td>862 csPC862LatinHebrew </td>
+ * </tr>
+ * <tr>
+ * <td>Cp863</td>
+ * <td>IBM863</td>
+ * <td>863 csIBM863 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp864</td>
+ * <td>IBM864</td>
+ * <td>cp864 csIBM864 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp865</td>
+ * <td>IBM865</td>
+ * <td>865 csIBM865 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp866</td>
+ * <td>IBM866</td>
+ * <td>866 csIBM866 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp868</td>
+ * <td>IBM868</td>
+ * <td>cp-ar csIBM868 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp869</td>
+ * <td>IBM869</td>
+ * <td>cp-gr csIBM869 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp870</td>
+ * <td>IBM870</td>
+ * <td>ebcdic-cp-roece ebcdic-cp-yu csIBM870 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp871</td>
+ * <td>IBM871</td>
+ * <td>ebcdic-cp-is csIBM871 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp875</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp918</td>
+ * <td>IBM918</td>
+ * <td>ebcdic-cp-ar2 csIBM918 </td>
+ * </tr>
+ * <tr>
+ * <td>Cp921</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp922</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp930</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp933</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp935</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp937</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp939</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp942</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp942C</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp943</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp943C</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp948</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp949</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp949C</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp950</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp964</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>Cp970</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>EUC_CN</td>
+ * <td>GB2312</td>
+ * <td>x-EUC-CN csGB2312 euccn euc-cn gb2312-80 gb2312-1980 CN-GB CN-GB-ISOIR165 </td>
+ * </tr>
+ * <tr>
+ * <td>EUC_JP</td>
+ * <td>EUC-JP</td>
+ * <td>csEUCPkdFmtJapanese Extended_UNIX_Code_Packed_Format_for_Japanese eucjis x-eucjp eucjp x-euc-jp </td>
+ * </tr>
+ * <tr>
+ * <td>EUC_JP_LINUX</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>EUC_JP_Solaris</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>EUC_KR</td>
+ * <td>EUC-KR</td>
+ * <td>csEUCKR ksc5601 5601 ksc5601_1987 ksc_5601 ksc5601-1987 ks_c_5601-1987 euckr </td>
+ * </tr>
+ * <tr>
+ * <td>EUC_TW</td>
+ * <td>EUC-TW</td>
+ * <td>x-EUC-TW cns11643 euctw </td>
+ * </tr>
+ * <tr>
+ * <td>GB18030</td>
+ * <td>GB18030</td>
+ * <td>gb18030-2000 </td>
+ * </tr>
+ * <tr>
+ * <td>GBK</td>
+ * <td>windows-936</td>
+ * <td>CP936 MS936 ms_936 x-mswin-936 </td>
+ * </tr>
+ * <tr>
+ * <td>ISCII91</td>
+ * <td>?</td>
+ * <td>x-ISCII91 iscii </td>
+ * </tr>
+ * <tr>
+ * <td>ISO2022CN</td>
+ * <td>ISO-2022-CN</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>ISO2022JP</td>
+ * <td>ISO-2022-JP</td>
+ * <td>csISO2022JP JIS jis_encoding csjisencoding </td>
+ * </tr>
+ * <tr>
+ * <td>ISO2022KR</td>
+ * <td>ISO-2022-KR</td>
+ * <td>csISO2022KR </td>
+ * </tr>
+ * <tr>
+ * <td>ISO2022_CN_CNS</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>ISO2022_CN_GB</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_1</td>
+ * <td>ISO-8859-1</td>
+ * <td>ISO_8859-1:1987 iso-ir-100 ISO_8859-1 latin1 l1 IBM819 CP819 csISOLatin1 8859_1 819 IBM-819 ISO8859-1 ISO_8859_1 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_13</td>
+ * <td>ISO-8859-13</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_15</td>
+ * <td>ISO-8859-15</td>
+ * <td>ISO_8859-15 Latin-9 8859_15 csISOlatin9 IBM923 cp923 923 L9 IBM-923 ISO8859-15 LATIN9 LATIN0 csISOlatin0 ISO8859_15_FDIS </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_2</td>
+ * <td>ISO-8859-2</td>
+ * <td>ISO_8859-2:1987 iso-ir-101 ISO_8859-2 latin2 l2 csISOLatin2 8859_2 iso8859_2 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_3</td>
+ * <td>ISO-8859-3</td>
+ * <td>ISO_8859-3:1988 iso-ir-109 ISO_8859-3 latin3 l3 csISOLatin3 8859_3 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_4</td>
+ * <td>ISO-8859-4</td>
+ * <td>ISO_8859-4:1988 iso-ir-110 ISO_8859-4 latin4 l4 csISOLatin4 8859_4 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_5</td>
+ * <td>ISO-8859-5</td>
+ * <td>ISO_8859-5:1988 iso-ir-144 ISO_8859-5 cyrillic csISOLatinCyrillic 8859_5 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_6</td>
+ * <td>ISO-8859-6</td>
+ * <td>ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ECMA-114 ASMO-708 arabic csISOLatinArabic 8859_6 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_7</td>
+ * <td>ISO-8859-7</td>
+ * <td>ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ELOT_928 ECMA-118 greek greek8 csISOLatinGreek 8859_7 sun_eu_greek </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_8</td>
+ * <td>ISO-8859-8</td>
+ * <td>ISO_8859-8:1988 iso-ir-138 ISO_8859-8 hebrew csISOLatinHebrew 8859_8 </td>
+ * </tr>
+ * <tr>
+ * <td>ISO8859_9</td>
+ * <td>ISO-8859-9</td>
+ * <td>ISO_8859-9:1989 iso-ir-148 ISO_8859-9 latin5 l5 csISOLatin5 8859_9 </td>
+ * </tr>
+ * <tr>
+ * <td>JISAutoDetect</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>JIS_C6626-1983</td>
+ * <td>JIS_C6626-1983</td>
+ * <td>x-JIS0208 JIS0208 csISO87JISX0208 x0208 JIS_X0208-1983 iso-ir-87 </td>
+ * </tr>
+ * <tr>
+ * <td>JIS_X0201</td>
+ * <td>JIS_X0201</td>
+ * <td>X0201 JIS0201 csHalfWidthKatakana </td>
+ * </tr>
+ * <tr>
+ * <td>JIS_X0212-1990</td>
+ * <td>JIS_X0212-1990</td>
+ * <td>iso-ir-159 x0212 JIS0212 csISO159JISX02121990 </td>
+ * </tr>
+ * <tr>
+ * <td>KOI8_R</td>
+ * <td>KOI8-R</td>
+ * <td>csKOI8R koi8 </td>
+ * </tr>
+ * <tr>
+ * <td>MS874</td>
+ * <td>windows-874</td>
+ * <td>cp874 </td>
+ * </tr>
+ * <tr>
+ * <td>MS932</td>
+ * <td>Windows-31J</td>
+ * <td>windows-932 csWindows31J x-ms-cp932 </td>
+ * </tr>
+ * <tr>
+ * <td>MS949</td>
+ * <td>windows-949</td>
+ * <td>windows949 ms_949 x-windows-949 </td>
+ * </tr>
+ * <tr>
+ * <td>MS950</td>
+ * <td>windows-950</td>
+ * <td>x-windows-950 </td>
+ * </tr>
+ * <tr>
+ * <td>MS950_HKSCS</td>
+ * <td></td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacArabic</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacCentralEurope</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacCroatian</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacCyrillic</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacDingbat</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacGreek</td>
+ * <td>MacGreek</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacHebrew</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacIceland</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacRoman</td>
+ * <td>MacRoman</td>
+ * <td>Macintosh MAC csMacintosh </td>
+ * </tr>
+ * <tr>
+ * <td>MacRomania</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacSymbol</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacThai</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacTurkish</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>MacUkraine</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>SJIS</td>
+ * <td>Shift_JIS</td>
+ * <td>MS_Kanji csShiftJIS shift-jis x-sjis pck </td>
+ * </tr>
+ * <tr>
+ * <td>TIS620</td>
+ * <td>TIS-620</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>UTF-16</td>
+ * <td>UTF-16</td>
+ * <td>UTF_16 </td>
+ * </tr>
+ * <tr>
+ * <td>UTF8</td>
+ * <td>UTF-8</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>UnicodeBig</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>UnicodeBigUnmarked</td>
+ * <td>UTF-16BE</td>
+ * <td>X-UTF-16BE UTF_16BE ISO-10646-UCS-2 </td>
+ * </tr>
+ * <tr>
+ * <td>UnicodeLittle</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * <tr>
+ * <td>UnicodeLittleUnmarked</td>
+ * <td>UTF-16LE</td>
+ * <td>UTF_16LE X-UTF-16LE </td>
+ * </tr>
+ * <tr>
+ * <td>x-Johab</td>
+ * <td>johab</td>
+ * <td>johab cp1361 ms1361 ksc5601-1992 ksc5601_1992 </td>
+ * </tr>
+ * <tr>
+ * <td>x-iso-8859-11</td>
+ * <td>?</td>
+ * <td></td>
+ * </tr>
+ * </table>
+ *
+ *
+ * @version $Id: CharsetUtil.java,v 1.1 2004/10/25 07:26:46 ntherning Exp $
+ */
+public class CharsetUtil {
+ private static Log log = LogFactory.getLog(CharsetUtil.class);
+
+ private static class Charset implements Comparable<Charset> {
+ private String canonical = null;
+ private String mime = null;
+ private String[] aliases = null;
+
+ private Charset(String canonical, String mime, String[] aliases) {
+ this.canonical = canonical;
+ this.mime = mime;
+ this.aliases = aliases;
+ }
+
+ public int compareTo(Charset c) {
+ return this.canonical.compareTo(c.canonical);
+ }
+ }
+
+ private static Charset[] JAVA_CHARSETS = {
+ new Charset("ISO8859_1", "ISO-8859-1",
+ new String[] {"ISO_8859-1:1987", "iso-ir-100", "ISO_8859-1",
+ "latin1", "l1", "IBM819", "CP819",
+ "csISOLatin1", "8859_1", "819", "IBM-819",
+ "ISO8859-1", "ISO_8859_1"}),
+ new Charset("ISO8859_2", "ISO-8859-2",
+ new String[] {"ISO_8859-2:1987", "iso-ir-101", "ISO_8859-2",
+ "latin2", "l2", "csISOLatin2", "8859_2",
+ "iso8859_2"}),
+ new Charset("ISO8859_3", "ISO-8859-3", new String[] {"ISO_8859-3:1988", "iso-ir-109", "ISO_8859-3", "latin3", "l3", "csISOLatin3", "8859_3"}),
+ new Charset("ISO8859_4", "ISO-8859-4",
+ new String[] {"ISO_8859-4:1988", "iso-ir-110", "ISO_8859-4",
+ "latin4", "l4", "csISOLatin4", "8859_4"}),
+ new Charset("ISO8859_5", "ISO-8859-5",
+ new String[] {"ISO_8859-5:1988", "iso-ir-144", "ISO_8859-5",
+ "cyrillic", "csISOLatinCyrillic", "8859_5"}),
+ new Charset("ISO8859_6", "ISO-8859-6", new String[] {"ISO_8859-6:1987", "iso-ir-127", "ISO_8859-6", "ECMA-114", "ASMO-708", "arabic", "csISOLatinArabic", "8859_6"}),
+ new Charset("ISO8859_7", "ISO-8859-7",
+ new String[] {"ISO_8859-7:1987", "iso-ir-126", "ISO_8859-7",
+ "ELOT_928", "ECMA-118", "greek", "greek8",
+ "csISOLatinGreek", "8859_7", "sun_eu_greek"}),
+ new Charset("ISO8859_8", "ISO-8859-8", new String[] {"ISO_8859-8:1988", "iso-ir-138", "ISO_8859-8", "hebrew", "csISOLatinHebrew", "8859_8"}),
+ new Charset("ISO8859_9", "ISO-8859-9",
+ new String[] {"ISO_8859-9:1989", "iso-ir-148", "ISO_8859-9",
+ "latin5", "l5", "csISOLatin5", "8859_9"}),
+
+ new Charset("ISO8859_13", "ISO-8859-13", new String[] {}),
+ new Charset("ISO8859_15", "ISO-8859-15",
+ new String[] {"ISO_8859-15", "Latin-9", "8859_15",
+ "csISOlatin9", "IBM923", "cp923", "923", "L9",
+ "IBM-923", "ISO8859-15", "LATIN9", "LATIN0",
+ "csISOlatin0", "ISO8859_15_FDIS"}),
+ new Charset("KOI8_R", "KOI8-R", new String[] {"csKOI8R", "koi8"}),
+ new Charset("ASCII", "US-ASCII",
+ new String[] {"ANSI_X3.4-1968", "iso-ir-6",
+ "ANSI_X3.4-1986", "ISO_646.irv:1991",
+ "ISO646-US", "us", "IBM367", "cp367",
+ "csASCII", "ascii7", "646", "iso_646.irv:1983"}),
+ new Charset("UTF8", "UTF-8", new String[] {}),
+ new Charset("UTF-16", "UTF-16", new String[] {"UTF_16"}),
+ new Charset("UnicodeBigUnmarked", "UTF-16BE", new String[] {"X-UTF-16BE", "UTF_16BE", "ISO-10646-UCS-2"}),
+ new Charset("UnicodeLittleUnmarked", "UTF-16LE", new String[] {"UTF_16LE", "X-UTF-16LE"}),
+ new Charset("Big5", "Big5", new String[] {"csBig5", "CN-Big5", "BIG-FIVE", "BIGFIVE"}),
+ new Charset("Big5_HKSCS", "Big5-HKSCS", new String[] {"big5hkscs"}),
+ new Charset("EUC_JP", "EUC-JP",
+ new String[] {"csEUCPkdFmtJapanese",
+ "Extended_UNIX_Code_Packed_Format_for_Japanese",
+ "eucjis", "x-eucjp", "eucjp", "x-euc-jp"}),
+ new Charset("EUC_KR", "EUC-KR",
+ new String[] {"csEUCKR", "ksc5601", "5601", "ksc5601_1987",
+ "ksc_5601", "ksc5601-1987", "ks_c_5601-1987",
+ "euckr"}),
+ new Charset("GB18030", "GB18030", new String[] {"gb18030-2000"}),
+ new Charset("EUC_CN", "GB2312", new String[] {"x-EUC-CN", "csGB2312", "euccn", "euc-cn", "gb2312-80", "gb2312-1980", "CN-GB", "CN-GB-ISOIR165"}),
+ new Charset("GBK", "windows-936", new String[] {"CP936", "MS936", "ms_936", "x-mswin-936"}),
+
+ new Charset("Cp037", "IBM037", new String[] {"ebcdic-cp-us", "ebcdic-cp-ca", "ebcdic-cp-wt", "ebcdic-cp-nl", "csIBM037"}),
+ new Charset("Cp273", "IBM273", new String[] {"csIBM273"}),
+ new Charset("Cp277", "IBM277", new String[] {"EBCDIC-CP-DK", "EBCDIC-CP-NO", "csIBM277"}),
+ new Charset("Cp278", "IBM278", new String[] {"CP278", "ebcdic-cp-fi", "ebcdic-cp-se", "csIBM278"}),
+ new Charset("Cp280", "IBM280", new String[] {"ebcdic-cp-it", "csIBM280"}),
+ new Charset("Cp284", "IBM284", new String[] {"ebcdic-cp-es", "csIBM284"}),
+ new Charset("Cp285", "IBM285", new String[] {"ebcdic-cp-gb", "csIBM285"}),
+ new Charset("Cp297", "IBM297", new String[] {"ebcdic-cp-fr", "csIBM297"}),
+ new Charset("Cp420", "IBM420", new String[] {"ebcdic-cp-ar1", "csIBM420"}),
+ new Charset("Cp424", "IBM424", new String[] {"ebcdic-cp-he", "csIBM424"}),
+ new Charset("Cp437", "IBM437", new String[] {"437", "csPC8CodePage437"}),
+ new Charset("Cp500", "IBM500", new String[] {"ebcdic-cp-be", "ebcdic-cp-ch", "csIBM500"}),
+ new Charset("Cp775", "IBM775", new String[] {"csPC775Baltic"}),
+ new Charset("Cp838", "IBM-Thai", new String[] {}),
+ new Charset("Cp850", "IBM850", new String[] {"850", "csPC850Multilingual"}),
+ new Charset("Cp852", "IBM852", new String[] {"852", "csPCp852"}),
+ new Charset("Cp855", "IBM855", new String[] {"855", "csIBM855"}),
+ new Charset("Cp857", "IBM857", new String[] {"857", "csIBM857"}),
+ new Charset("Cp858", "IBM00858",
+ new String[] {"CCSID00858", "CP00858",
+ "PC-Multilingual-850+euro"}),
+ new Charset("Cp860", "IBM860", new String[] {"860", "csIBM860"}),
+ new Charset("Cp861", "IBM861", new String[] {"861", "cp-is", "csIBM861"}),
+ new Charset("Cp862", "IBM862", new String[] {"862", "csPC862LatinHebrew"}),
+ new Charset("Cp863", "IBM863", new String[] {"863", "csIBM863"}),
+ new Charset("Cp864", "IBM864", new String[] {"cp864", "csIBM864"}),
+ new Charset("Cp865", "IBM865", new String[] {"865", "csIBM865"}),
+ new Charset("Cp866", "IBM866", new String[] {"866", "csIBM866"}),
+ new Charset("Cp868", "IBM868", new String[] {"cp-ar", "csIBM868"}),
+ new Charset("Cp869", "IBM869", new String[] {"cp-gr", "csIBM869"}),
+ new Charset("Cp870", "IBM870", new String[] {"ebcdic-cp-roece", "ebcdic-cp-yu", "csIBM870"}),
+ new Charset("Cp871", "IBM871", new String[] {"ebcdic-cp-is", "csIBM871"}),
+ new Charset("Cp918", "IBM918", new String[] {"ebcdic-cp-ar2", "csIBM918"}),
+ new Charset("Cp1026", "IBM1026", new String[] {"csIBM1026"}),
+ new Charset("Cp1047", "IBM1047", new String[] {"IBM-1047"}),
+ new Charset("Cp1140", "IBM01140",
+ new String[] {"CCSID01140", "CP01140",
+ "ebcdic-us-37+euro"}),
+ new Charset("Cp1141", "IBM01141",
+ new String[] {"CCSID01141", "CP01141",
+ "ebcdic-de-273+euro"}),
+ new Charset("Cp1142", "IBM01142", new String[] {"CCSID01142", "CP01142", "ebcdic-dk-277+euro", "ebcdic-no-277+euro"}),
+ new Charset("Cp1143", "IBM01143", new String[] {"CCSID01143", "CP01143", "ebcdic-fi-278+euro", "ebcdic-se-278+euro"}),
+ new Charset("Cp1144", "IBM01144", new String[] {"CCSID01144", "CP01144", "ebcdic-it-280+euro"}),
+ new Charset("Cp1145", "IBM01145", new String[] {"CCSID01145", "CP01145", "ebcdic-es-284+euro"}),
+ new Charset("Cp1146", "IBM01146", new String[] {"CCSID01146", "CP01146", "ebcdic-gb-285+euro"}),
+ new Charset("Cp1147", "IBM01147", new String[] {"CCSID01147", "CP01147", "ebcdic-fr-297+euro"}),
+ new Charset("Cp1148", "IBM01148", new String[] {"CCSID01148", "CP01148", "ebcdic-international-500+euro"}),
+ new Charset("Cp1149", "IBM01149", new String[] {"CCSID01149", "CP01149", "ebcdic-is-871+euro"}),
+ new Charset("Cp1250", "windows-1250", new String[] {}),
+ new Charset("Cp1251", "windows-1251", new String[] {}),
+ new Charset("Cp1252", "windows-1252", new String[] {}),
+ new Charset("Cp1253", "windows-1253", new String[] {}),
+ new Charset("Cp1254", "windows-1254", new String[] {}),
+ new Charset("Cp1255", "windows-1255", new String[] {}),
+ new Charset("Cp1256", "windows-1256", new String[] {}),
+ new Charset("Cp1257", "windows-1257", new String[] {}),
+ new Charset("Cp1258", "windows-1258", new String[] {}),
+ new Charset("ISO2022CN", "ISO-2022-CN", new String[] {}),
+ new Charset("ISO2022JP", "ISO-2022-JP", new String[] {"csISO2022JP", "JIS", "jis_encoding", "csjisencoding"}),
+ new Charset("ISO2022KR", "ISO-2022-KR", new String[] {"csISO2022KR"}),
+ new Charset("JIS_X0201", "JIS_X0201", new String[] {"X0201", "JIS0201", "csHalfWidthKatakana"}),
+ new Charset("JIS_X0212-1990", "JIS_X0212-1990", new String[] {"iso-ir-159", "x0212", "JIS0212", "csISO159JISX02121990"}),
+ new Charset("JIS_C6626-1983", "JIS_C6626-1983", new String[] {"x-JIS0208", "JIS0208", "csISO87JISX0208", "x0208", "JIS_X0208-1983", "iso-ir-87"}),
+ new Charset("SJIS", "Shift_JIS", new String[] {"MS_Kanji", "csShiftJIS", "shift-jis", "x-sjis", "pck"}),
+ new Charset("TIS620", "TIS-620", new String[] {}),
+ new Charset("MS932", "Windows-31J", new String[] {"windows-932", "csWindows31J", "x-ms-cp932"}),
+ new Charset("EUC_TW", "EUC-TW", new String[] {"x-EUC-TW", "cns11643", "euctw"}),
+ new Charset("x-Johab", "johab", new String[] {"johab", "cp1361", "ms1361", "ksc5601-1992", "ksc5601_1992"}),
+ new Charset("MS950_HKSCS", "", new String[] {}),
+ new Charset("MS874", "windows-874", new String[] {"cp874"}),
+ new Charset("MS949", "windows-949", new String[] {"windows949", "ms_949", "x-windows-949"}),
+ new Charset("MS950", "windows-950", new String[] {"x-windows-950"}),
+
+ new Charset("Cp737", null, new String[] {}),
+ new Charset("Cp856", null, new String[] {}),
+ new Charset("Cp875", null, new String[] {}),
+ new Charset("Cp921", null, new String[] {}),
+ new Charset("Cp922", null, new String[] {}),
+ new Charset("Cp930", null, new String[] {}),
+ new Charset("Cp933", null, new String[] {}),
+ new Charset("Cp935", null, new String[] {}),
+ new Charset("Cp937", null, new String[] {}),
+ new Charset("Cp939", null, new String[] {}),
+ new Charset("Cp942", null, new String[] {}),
+ new Charset("Cp942C", null, new String[] {}),
+ new Charset("Cp943", null, new String[] {}),
+ new Charset("Cp943C", null, new String[] {}),
+ new Charset("Cp948", null, new String[] {}),
+ new Charset("Cp949", null, new String[] {}),
+ new Charset("Cp949C", null, new String[] {}),
+ new Charset("Cp950", null, new String[] {}),
+ new Charset("Cp964", null, new String[] {}),
+ new Charset("Cp970", null, new String[] {}),
+ new Charset("Cp1006", null, new String[] {}),
+ new Charset("Cp1025", null, new String[] {}),
+ new Charset("Cp1046", null, new String[] {}),
+ new Charset("Cp1097", null, new String[] {}),
+ new Charset("Cp1098", null, new String[] {}),
+ new Charset("Cp1112", null, new String[] {}),
+ new Charset("Cp1122", null, new String[] {}),
+ new Charset("Cp1123", null, new String[] {}),
+ new Charset("Cp1124", null, new String[] {}),
+ new Charset("Cp1381", null, new String[] {}),
+ new Charset("Cp1383", null, new String[] {}),
+ new Charset("Cp33722", null, new String[] {}),
+ new Charset("Big5_Solaris", null, new String[] {}),
+ new Charset("EUC_JP_LINUX", null, new String[] {}),
+ new Charset("EUC_JP_Solaris", null, new String[] {}),
+ new Charset("ISCII91", null, new String[] {"x-ISCII91", "iscii"}),
+ new Charset("ISO2022_CN_CNS", null, new String[] {}),
+ new Charset("ISO2022_CN_GB", null, new String[] {}),
+ new Charset("x-iso-8859-11", null, new String[] {}),
+ new Charset("JISAutoDetect", null, new String[] {}),
+ new Charset("MacArabic", null, new String[] {}),
+ new Charset("MacCentralEurope", null, new String[] {}),
+ new Charset("MacCroatian", null, new String[] {}),
+ new Charset("MacCyrillic", null, new String[] {}),
+ new Charset("MacDingbat", null, new String[] {}),
+ new Charset("MacGreek", "MacGreek", new String[] {}),
+ new Charset("MacHebrew", null, new String[] {}),
+ new Charset("MacIceland", null, new String[] {}),
+ new Charset("MacRoman", "MacRoman", new String[] {"Macintosh", "MAC", "csMacintosh"}),
+ new Charset("MacRomania", null, new String[] {}),
+ new Charset("MacSymbol", null, new String[] {}),
+ new Charset("MacThai", null, new String[] {}),
+ new Charset("MacTurkish", null, new String[] {}),
+ new Charset("MacUkraine", null, new String[] {}),
+ new Charset("UnicodeBig", null, new String[] {}),
+ new Charset("UnicodeLittle", null, new String[] {})
+ };
+
+ /**
+ * Contains the canonical names of character sets which can be used to
+ * decode bytes into Java chars.
+ */
+ private static TreeSet<String> decodingSupported = null;
+
+ /**
+ * Contains the canonical names of character sets which can be used to
+ * encode Java chars into bytes.
+ */
+ private static TreeSet<String> encodingSupported = null;
+
+ /**
+ * Maps character set names to Charset objects. All possible names of
+ * a charset will be mapped to the Charset.
+ */
+ private static HashMap<String, Charset> charsetMap = null;
+
+ static {
+ decodingSupported = new TreeSet<String>();
+ encodingSupported = new TreeSet<String>();
+ byte[] dummy = new byte[] {'d', 'u', 'm', 'm', 'y'};
+ for (int i = 0; i < JAVA_CHARSETS.length; i++) {
+ try {
+ String s = new String(dummy, JAVA_CHARSETS[i].canonical);
+ decodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase(Locale.US));
+ } catch (UnsupportedOperationException e) {
+ } catch (UnsupportedEncodingException e) {
+ }
+ try {
+ "dummy".getBytes(JAVA_CHARSETS[i].canonical);
+ encodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase(Locale.US));
+ } catch (UnsupportedOperationException e) {
+ } catch (UnsupportedEncodingException e) {
+ }
+ }
+
+ charsetMap = new HashMap<String, Charset>();
+ for (int i = 0; i < JAVA_CHARSETS.length; i++) {
+ Charset c = JAVA_CHARSETS[i];
+ charsetMap.put(c.canonical.toLowerCase(Locale.US), c);
+ if (c.mime != null) {
+ charsetMap.put(c.mime.toLowerCase(Locale.US), c);
+ }
+ if (c.aliases != null) {
+ for (int j = 0; j < c.aliases.length; j++) {
+ charsetMap.put(c.aliases[j].toLowerCase(Locale.US), c);
+ }
+ }
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Character sets which support decoding: "
+ + decodingSupported);
+ log.debug("Character sets which support encoding: "
+ + encodingSupported);
+ }
+ }
+
+ /**
+ * ANDROID: THE FOLLOWING SET OF STATIC STRINGS ARE COPIED FROM A NEWER VERSION OF MIME4J
+ */
+
+ /** carriage return - line feed sequence */
+ public static final String CRLF = "\r\n";
+
+ /** US-ASCII CR, carriage return (13) */
+ public static final int CR = '\r';
+
+ /** US-ASCII LF, line feed (10) */
+ public static final int LF = '\n';
+
+ /** US-ASCII SP, space (32) */
+ public static final int SP = ' ';
+
+ /** US-ASCII HT, horizontal-tab (9)*/
+ public static final int HT = '\t';
+
+ public static final java.nio.charset.Charset US_ASCII = java.nio.charset.Charset
+ .forName("US-ASCII");
+
+ public static final java.nio.charset.Charset ISO_8859_1 = java.nio.charset.Charset
+ .forName("ISO-8859-1");
+
+ public static final java.nio.charset.Charset UTF_8 = java.nio.charset.Charset
+ .forName("UTF-8");
+
+ /**
+ * Returns <code>true</code> if the specified character is a whitespace
+ * character (CR, LF, SP or HT).
+ *
+ * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J
+ *
+ * @param ch
+ * character to test.
+ * @return <code>true</code> if the specified character is a whitespace
+ * character, <code>false</code> otherwise.
+ */
+ public static boolean isWhitespace(char ch) {
+ return ch == SP || ch == HT || ch == CR || ch == LF;
+ }
+
+ /**
+ * Returns <code>true</code> if the specified string consists entirely of
+ * whitespace characters.
+ *
+ * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J
+ *
+ * @param s
+ * string to test.
+ * @return <code>true</code> if the specified string consists entirely of
+ * whitespace characters, <code>false</code> otherwise.
+ */
+ public static boolean isWhitespace(final String s) {
+ if (s == null) {
+ throw new IllegalArgumentException("String may not be null");
+ }
+ final int len = s.length();
+ for (int i = 0; i < len; i++) {
+ if (!isWhitespace(s.charAt(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Determines if the VM supports encoding (chars to bytes) the
+ * specified character set. NOTE: the given character set name may
+ * not be known to the VM even if this method returns <code>true</code>.
+ * Use {@link #toJavaCharset(String)} to get the canonical Java character
+ * set name.
+ *
+ * @param charsetName the characters set name.
+ * @return <code>true</code> if encoding is supported, <code>false</code>
+ * otherwise.
+ */
+ public static boolean isEncodingSupported(String charsetName) {
+ return encodingSupported.contains(charsetName.toLowerCase(Locale.US));
+ }
+
+ /**
+ * Determines if the VM supports decoding (bytes to chars) the
+ * specified character set. NOTE: the given character set name may
+ * not be known to the VM even if this method returns <code>true</code>.
+ * Use {@link #toJavaCharset(String)} to get the canonical Java character
+ * set name.
+ *
+ * @param charsetName the characters set name.
+ * @return <code>true</code> if decoding is supported, <code>false</code>
+ * otherwise.
+ */
+ public static boolean isDecodingSupported(String charsetName) {
+ return decodingSupported.contains(charsetName.toLowerCase(Locale.US));
+ }
+
+ /**
+ * Gets the preferred MIME character set name for the specified
+ * character set or <code>null</code> if not known.
+ *
+ * @param charsetName the character set name to look for.
+ * @return the MIME preferred name or <code>null</code> if not known.
+ */
+ public static String toMimeCharset(String charsetName) {
+ Charset c = charsetMap.get(charsetName.toLowerCase(Locale.US));
+ if (c != null) {
+ return c.mime;
+ }
+ return null;
+ }
+
+ /**
+ * Gets the canonical Java character set name for the specified
+ * character set or <code>null</code> if not known. This should be
+ * called before doing any conversions using the Java API. NOTE:
+ * you must use {@link #isEncodingSupported(String)} or
+ * {@link #isDecodingSupported(String)} to make sure the returned
+ * Java character set is supported by the current VM.
+ *
+ * @param charsetName the character set name to look for.
+ * @return the canonical Java name or <code>null</code> if not known.
+ */
+ public static String toJavaCharset(String charsetName) {
+ Charset c = charsetMap.get(charsetName.toLowerCase(Locale.US));
+ if (c != null) {
+ return c.canonical;
+ }
+ return null;
+ }
+
+ public static java.nio.charset.Charset getCharset(String charsetName) {
+ String defaultCharset = "ISO-8859-1";
+
+ // Use the default chareset if given charset is null
+ if(charsetName == null) charsetName = defaultCharset;
+
+ try {
+ return java.nio.charset.Charset.forName(charsetName);
+ } catch (IllegalCharsetNameException e) {
+ log.info("Illegal charset " + charsetName + ", fallback to " +
+ defaultCharset + ": " + e);
+ // Use default charset on exception
+ return java.nio.charset.Charset.forName(defaultCharset);
+ } catch (UnsupportedCharsetException ex) {
+ log.info("Unsupported charset " + charsetName + ", fallback to " +
+ defaultCharset + ": " + ex);
+ // Use default charset on exception
+ return java.nio.charset.Charset.forName(defaultCharset);
+ }
+
+ }
+ /*
+ * Uncomment the code below and run the main method to regenerate the
+ * Javadoc table above when the known charsets change.
+ */
+
+ /*
+ private static String dumpHtmlTable() {
+ LinkedList l = new LinkedList(Arrays.asList(JAVA_CHARSETS));
+ Collections.sort(l);
+ StringBuffer sb = new StringBuffer();
+ sb.append(" * <table>\n");
+ sb.append(" * <tr>\n");
+ sb.append(" * <td>Canonical (Java) name</td>\n");
+ sb.append(" * <td>MIME preferred</td>\n");
+ sb.append(" * <td>Aliases</td>\n");
+ sb.append(" * </tr>\n");
+
+ for (Iterator it = l.iterator(); it.hasNext();) {
+ Charset c = (Charset) it.next();
+ sb.append(" * <tr>\n");
+ sb.append(" * <td>" + c.canonical + "</td>\n");
+ sb.append(" * <td>" + (c.mime == null ? "?" : c.mime)+ "</td>\n");
+ sb.append(" * <td>");
+ for (int i = 0; c.aliases != null && i < c.aliases.length; i++) {
+ sb.append(c.aliases[i] + " ");
+ }
+ sb.append("</td>\n");
+ sb.append(" * </tr>\n");
+ }
+ sb.append(" * </table>\n");
+ return sb.toString();
+ }
+
+ public static void main(String[] args) {
+ System.out.println(dumpHtmlTable());
+ }*/
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java b/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java
new file mode 100644
index 000000000..ad3c025cf
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java
@@ -0,0 +1,120 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmPhoneStateListener;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A singleton class designed to remember the active OMTP visual voicemail sources. Because a
+ * voicemail source is tied 1:1 to a phone account, the phone account handle is used as the key
+ * for each voicemail source and the associated data.
+ */
+public class OmtpVvmSourceManager {
+ public static final String TAG = "OmtpVvmSourceManager";
+
+ private static OmtpVvmSourceManager sInstance = new OmtpVvmSourceManager();
+
+ private Context mContext;
+ private TelephonyManager mTelephonyManager;
+ // Each phone account is associated with a phone state listener for updates to whether the
+ // device is able to sync.
+ private Set<PhoneAccountHandle> mActiveVvmSources;
+ private Map<PhoneAccountHandle, PhoneStateListener> mPhoneStateListenerMap;
+
+ /**
+ * Private constructor. Instance should only be acquired through getInstance().
+ */
+ private OmtpVvmSourceManager() {}
+
+ public static OmtpVvmSourceManager getInstance(Context context) {
+ sInstance.setup(context);
+ return sInstance;
+ }
+
+ /**
+ * Set the context and system services so they do not need to be retrieved every time.
+ * @param context The context to get the subscription and telephony manager for.
+ */
+ private void setup(Context context) {
+ if (mContext == null) {
+ mContext = context;
+ mTelephonyManager = (TelephonyManager)
+ mContext.getSystemService(Context.TELEPHONY_SERVICE);
+ mActiveVvmSources = Collections.newSetFromMap(
+ new ConcurrentHashMap<PhoneAccountHandle, Boolean>(8, 0.9f, 1));
+ mPhoneStateListenerMap =
+ new ConcurrentHashMap<PhoneAccountHandle, PhoneStateListener>(8, 0.9f, 1);
+ }
+ }
+
+ public void addSource(PhoneAccountHandle phoneAccount) {
+ mActiveVvmSources.add(phoneAccount);
+ }
+
+ public void removeSource(PhoneAccountHandle phoneAccount) {
+ // TODO: should use OmtpVvmCarrierConfigHelper to handle the event. But currently it
+ // couldn't handle events on removed SIMs
+ VoicemailStatus.disable(mContext, phoneAccount);
+ removePhoneStateListener(phoneAccount);
+ mActiveVvmSources.remove(phoneAccount);
+ }
+
+ public void addPhoneStateListener(PhoneAccountHandle phoneAccount) {
+ if (!mPhoneStateListenerMap.containsKey(phoneAccount)) {
+ VvmPhoneStateListener phoneStateListener = new VvmPhoneStateListener(mContext,
+ phoneAccount);
+ mPhoneStateListenerMap.put(phoneAccount, phoneStateListener);
+ mTelephonyManager.createForPhoneAccountHandle(phoneAccount)
+ .listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE);
+ }
+ }
+
+ public void removePhoneStateListener(PhoneAccountHandle phoneAccount) {
+ PhoneStateListener phoneStateListener =
+ mPhoneStateListenerMap.remove(phoneAccount);
+ mTelephonyManager.createForPhoneAccountHandle(phoneAccount).listen(phoneStateListener, 0);
+ }
+
+ public Set<PhoneAccountHandle> getOmtpVvmSources() {
+ return mActiveVvmSources;
+ }
+
+ /**
+ * Check if a certain account is registered.
+ *
+ * @param phoneAccount The account to look for.
+ * @return {@code true} if the account is in the list of registered OMTP voicemail sources.
+ * {@code false} otherwise.
+ */
+ public boolean isVvmSourceRegistered(PhoneAccountHandle phoneAccount) {
+ if (phoneAccount == null) {
+ return false;
+ }
+
+ return mActiveVvmSources.contains(phoneAccount);
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java
new file mode 100644
index 000000000..971a1c5a8
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java
@@ -0,0 +1,61 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import com.android.voicemailomtp.ActivationTask;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
+
+import java.util.List;
+
+public class OmtpVvmSyncReceiver extends BroadcastReceiver {
+
+ private static final String TAG = "OmtpVvmSyncReceiver";
+
+ @Override
+ public void onReceive(final Context context, Intent intent) {
+ if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) {
+ VvmLog.v(TAG, "Sync intent received");
+ for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context)
+ .getOmtpVvmSources()) {
+ SyncTask.start(context, source, OmtpVvmSyncService.SYNC_FULL_SYNC);
+ }
+ activateUnactivatedAccounts(context);
+ }
+ }
+
+ private static void activateUnactivatedAccounts(Context context) {
+ List<PhoneAccountHandle> accounts =
+ context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts();
+ for (PhoneAccountHandle phoneAccount : accounts) {
+ if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) {
+ continue;
+ }
+ if (!OmtpVvmSourceManager.getInstance(context).isVvmSourceRegistered(phoneAccount)) {
+ VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating");
+ ActivationTask.start(context, phoneAccount, null);
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java
new file mode 100644
index 000000000..a3418cc28
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java
@@ -0,0 +1,278 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.net.Network;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.voicemailomtp.ActivationTask;
+import com.android.voicemailomtp.Assert;
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.Voicemail;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.fetch.VoicemailFetchedCallback;
+import com.android.voicemailomtp.imap.ImapHelper;
+import com.android.voicemailomtp.imap.ImapHelper.InitializingException;
+import com.android.voicemailomtp.scheduling.BaseTask;
+import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil;
+import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper;
+import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException;
+import com.android.voicemailomtp.utils.VoicemailDatabaseUtil;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Sync OMTP visual voicemail. */
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class OmtpVvmSyncService {
+
+ private static final String TAG = OmtpVvmSyncService.class.getSimpleName();
+
+ /**
+ * Signifies a sync with both uploading to the server and downloading from the server.
+ */
+ public static final String SYNC_FULL_SYNC = "full_sync";
+ /**
+ * Only upload to the server.
+ */
+ public static final String SYNC_UPLOAD_ONLY = "upload_only";
+ /**
+ * Only download from the server.
+ */
+ public static final String SYNC_DOWNLOAD_ONLY = "download_only";
+ /**
+ * Only download single voicemail transcription.
+ */
+ public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION =
+ "download_one_transcription";
+
+ private final Context mContext;
+
+ // Record the timestamp of the last full sync so that duplicate syncs can be reduced.
+ private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp";
+ // Constant indicating that there has never been a full sync.
+ public static final long NO_PRIOR_FULL_SYNC = -1;
+
+ private VoicemailsQueryHelper mQueryHelper;
+
+ public OmtpVvmSyncService(Context context) {
+ mContext = context;
+ mQueryHelper = new VoicemailsQueryHelper(mContext);
+ }
+
+ public void sync(BaseTask task, String action, PhoneAccountHandle phoneAccount,
+ Voicemail voicemail, VoicemailStatus.Editor status) {
+ Assert.isTrue(phoneAccount != null);
+ VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount);
+ setupAndSendRequest(task, phoneAccount, voicemail, action, status);
+ }
+
+ private void setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount,
+ Voicemail voicemail, String action, VoicemailStatus.Editor status) {
+ if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) {
+ VvmLog.v(TAG, "Sync requested for disabled account");
+ return;
+ }
+ if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(phoneAccount)) {
+ ActivationTask.start(mContext, phoneAccount, null);
+ return;
+ }
+
+ OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount);
+ // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data
+ // channel errors, which should happen when the task starts, not when it ends. It is the
+ // "Sync in progress..." status.
+ config.handleEvent(VoicemailStatus.edit(mContext, phoneAccount),
+ OmtpEvents.DATA_IMAP_OPERATION_STARTED);
+ try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) {
+ if (network == null) {
+ VvmLog.e(TAG, "unable to acquire network");
+ task.fail();
+ return;
+ }
+ doSync(task, network.get(), phoneAccount, voicemail, action, status);
+ } catch (RequestFailedException e) {
+ config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+ task.fail();
+ }
+ }
+
+ private void doSync(BaseTask task, Network network, PhoneAccountHandle phoneAccount,
+ Voicemail voicemail, String action, VoicemailStatus.Editor status) {
+ try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) {
+ boolean success;
+ if (voicemail == null) {
+ success = syncAll(action, imapHelper, phoneAccount);
+ } else {
+ success = syncOne(imapHelper, voicemail, phoneAccount);
+ }
+ if (success) {
+ // TODO: b/30569269 failure should interrupt all subsequent task via exceptions
+ imapHelper.updateQuota();
+ imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED);
+ } else {
+ task.fail();
+ }
+ } catch (InitializingException e) {
+ VvmLog.w(TAG, "Can't retrieve Imap credentials.", e);
+ return;
+ }
+ }
+
+ private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) {
+ boolean uploadSuccess = true;
+ boolean downloadSuccess = true;
+
+ if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) {
+ uploadSuccess = upload(imapHelper);
+ }
+ if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) {
+ downloadSuccess = download(imapHelper, account);
+ }
+
+ VvmLog.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess)
+ + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]");
+
+ return uploadSuccess && downloadSuccess;
+ }
+
+ private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail,
+ PhoneAccountHandle account) {
+ if (shouldPerformPrefetch(account, imapHelper)) {
+ VoicemailFetchedCallback callback = new VoicemailFetchedCallback(mContext,
+ voicemail.getUri(), account);
+ imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData());
+ }
+
+ return imapHelper.fetchTranscription(
+ new TranscriptionFetchedCallback(mContext, voicemail),
+ voicemail.getSourceData());
+ }
+
+ private boolean upload(ImapHelper imapHelper) {
+ List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails();
+ List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails();
+
+ boolean success = true;
+
+ if (deletedVoicemails.size() > 0) {
+ if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) {
+ // We want to delete selectively instead of all the voicemails for this provider
+ // in case the state changed since the IMAP query was completed.
+ mQueryHelper.deleteFromDatabase(deletedVoicemails);
+ } else {
+ success = false;
+ }
+ }
+
+ if (readVoicemails.size() > 0) {
+ if (imapHelper.markMessagesAsRead(readVoicemails)) {
+ mQueryHelper.markCleanInDatabase(readVoicemails);
+ } else {
+ success = false;
+ }
+ }
+
+ return success;
+ }
+
+ private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) {
+ List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails();
+ List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails();
+
+ if (localVoicemails == null || serverVoicemails == null) {
+ // Null value means the query failed.
+ return false;
+ }
+
+ Map<String, Voicemail> remoteMap = buildMap(serverVoicemails);
+
+ // Go through all the local voicemails and check if they are on the server.
+ // They may be read or deleted on the server but not locally. Perform the
+ // appropriate local operation if the status differs from the server. Remove
+ // the messages that exist both locally and on the server to know which server
+ // messages to insert locally.
+ for (int i = 0; i < localVoicemails.size(); i++) {
+ Voicemail localVoicemail = localVoicemails.get(i);
+ Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData());
+ if (remoteVoicemail == null) {
+ mQueryHelper.deleteFromDatabase(localVoicemail);
+ } else {
+ if (remoteVoicemail.isRead() != localVoicemail.isRead()) {
+ mQueryHelper.markReadInDatabase(localVoicemail);
+ }
+
+ if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) &&
+ TextUtils.isEmpty(localVoicemail.getTranscription())) {
+ mQueryHelper.updateWithTranscription(localVoicemail,
+ remoteVoicemail.getTranscription());
+ }
+ }
+ }
+
+ // The leftover messages are messages that exist on the server but not locally.
+ boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper);
+ for (Voicemail remoteVoicemail : remoteMap.values()) {
+ Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail);
+ if (prefetchEnabled) {
+ VoicemailFetchedCallback fetchedCallback =
+ new VoicemailFetchedCallback(mContext, uri, account);
+ imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData());
+ }
+ }
+
+ return true;
+ }
+
+ private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) {
+ OmtpVvmCarrierConfigHelper carrierConfigHelper =
+ new OmtpVvmCarrierConfigHelper(mContext, account);
+ return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming();
+ }
+
+ /**
+ * Builds a map from provider data to message for the given collection of voicemails.
+ */
+ private Map<String, Voicemail> buildMap(List<Voicemail> messages) {
+ Map<String, Voicemail> map = new HashMap<String, Voicemail>();
+ for (Voicemail message : messages) {
+ map.put(message.getSourceData(), message);
+ }
+ return map;
+ }
+
+ public class TranscriptionFetchedCallback {
+
+ private Context mContext;
+ private Voicemail mVoicemail;
+
+ public TranscriptionFetchedCallback(Context context, Voicemail voicemail) {
+ mContext = context;
+ mVoicemail = voicemail;
+ }
+
+ public void setVoicemailTranscription(String transcription) {
+ VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext);
+ queryHelper.updateWithTranscription(mVoicemail, transcription);
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/SyncOneTask.java b/java/com/android/voicemailomtp/sync/SyncOneTask.java
new file mode 100644
index 000000000..9264e6c08
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/SyncOneTask.java
@@ -0,0 +1,82 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.Voicemail;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.scheduling.BaseTask;
+import com.android.voicemailomtp.scheduling.RetryPolicy;
+
+/**
+ * Task to download a single voicemail from the server. This task is initiated by a SMS notifying
+ * the new voicemail arrival, and ignores the duplicated tasks constraint.
+ */
+public class SyncOneTask extends BaseTask {
+
+ private static final int RETRY_TIMES = 2;
+ private static final int RETRY_INTERVAL_MILLIS = 5_000;
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+ private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+ private static final String EXTRA_VOICEMAIL = "extra_voicemail";
+
+ private PhoneAccountHandle mPhone;
+ private String mSyncType;
+ private Voicemail mVoicemail;
+
+ public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) {
+ Intent intent = BaseTask
+ .createIntent(context, SyncOneTask.class, phone);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+ intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION);
+ intent.putExtra(EXTRA_VOICEMAIL, voicemail);
+ context.startService(intent);
+ }
+
+ public SyncOneTask() {
+ super(TASK_ALLOW_DUPLICATES);
+ addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS));
+ }
+
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+ mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL);
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+ service.sync(this, mSyncType, mPhone, mVoicemail,
+ VoicemailStatus.edit(getContext(), mPhone));
+ }
+
+ @Override
+ public Intent createRestartIntent() {
+ Intent intent = super.createRestartIntent();
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+ intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+ intent.putExtra(EXTRA_VOICEMAIL, mVoicemail);
+ return intent;
+ }
+
+}
diff --git a/java/com/android/voicemailomtp/sync/SyncTask.java b/java/com/android/voicemailomtp/sync/SyncTask.java
new file mode 100644
index 000000000..41b22f22c
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/SyncTask.java
@@ -0,0 +1,79 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.scheduling.BaseTask;
+import com.android.voicemailomtp.scheduling.MinimalIntervalPolicy;
+import com.android.voicemailomtp.scheduling.RetryPolicy;
+
+/**
+ * System initiated sync request.
+ */
+public class SyncTask extends BaseTask {
+
+ // Try sync for a total of 5 times, should take around 5 minutes before finally giving up.
+ private static final int RETRY_TIMES = 4;
+ private static final int RETRY_INTERVAL_MILLIS = 5_000;
+ private static final int MINIMAL_INTERVAL_MILLIS = 60_000;
+
+ private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle";
+ private static final String EXTRA_SYNC_TYPE = "extra_sync_type";
+
+ private final RetryPolicy mRetryPolicy;
+
+ private PhoneAccountHandle mPhone;
+ private String mSyncType;
+
+ public static void start(Context context, PhoneAccountHandle phone, String syncType) {
+ Intent intent = BaseTask
+ .createIntent(context, SyncTask.class, phone);
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone);
+ intent.putExtra(EXTRA_SYNC_TYPE, syncType);
+ context.startService(intent);
+ }
+
+ public SyncTask() {
+ super(TASK_SYNC);
+ mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS);
+ addPolicy(mRetryPolicy);
+ addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS));
+ }
+
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE);
+ mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE);
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+ service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor());
+ }
+
+ @Override
+ public Intent createRestartIntent() {
+ Intent intent = super.createRestartIntent();
+ intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone);
+ intent.putExtra(EXTRA_SYNC_TYPE, mSyncType);
+ return intent;
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/UploadTask.java b/java/com/android/voicemailomtp/sync/UploadTask.java
new file mode 100644
index 000000000..30a16812b
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/UploadTask.java
@@ -0,0 +1,68 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import com.android.voicemailomtp.scheduling.BaseTask;
+import com.android.voicemailomtp.scheduling.PostponePolicy;
+
+/**
+ * Upload task triggered by database changes. Will wait until the database has been stable for
+ * {@link #POSTPONE_MILLIS} to execute.
+ */
+public class UploadTask extends BaseTask {
+
+ private static final String TAG = "VvmUploadTask";
+
+ private static final int POSTPONE_MILLIS = 5_000;
+
+ public UploadTask() {
+ super(TASK_UPLOAD);
+ addPolicy(new PostponePolicy(POSTPONE_MILLIS));
+ }
+
+ public static void start(Context context, PhoneAccountHandle phoneAccountHandle) {
+ Intent intent = BaseTask
+ .createIntent(context, UploadTask.class, phoneAccountHandle);
+ context.startService(intent);
+ }
+
+ @Override
+ public void onCreate(Context context, Intent intent, int flags, int startId) {
+ super.onCreate(context, intent, flags, startId);
+ }
+
+ @Override
+ public void onExecuteInBackgroundThread() {
+ OmtpVvmSyncService service = new OmtpVvmSyncService(getContext());
+
+ PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle();
+ if (phoneAccountHandle == null) {
+ // This should never happen
+ VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle());
+ return;
+ }
+ service.sync(this, OmtpVvmSyncService.SYNC_UPLOAD_ONLY,
+ phoneAccountHandle, null,
+ VoicemailStatus.edit(getContext(), phoneAccountHandle));
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java
new file mode 100644
index 000000000..ade9ef12d
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java
@@ -0,0 +1,41 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.VoicemailContract;
+import android.telecom.PhoneAccountHandle;
+
+/**
+ * Receives changes to the voicemail provider so they can be sent to the voicemail server.
+ */
+public class VoicemailProviderChangeReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false);
+ OmtpVvmSourceManager vvmSourceManager =
+ OmtpVvmSourceManager.getInstance(context);
+ if (vvmSourceManager.getOmtpVvmSources().size() > 0 && !isSelfChanged) {
+ for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context)
+ .getOmtpVvmSources()) {
+ UploadTask.start(context, source);
+ }
+ }
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java
new file mode 100644
index 000000000..89ba0b494
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java
@@ -0,0 +1,113 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Status;
+import android.telecom.PhoneAccountHandle;
+
+/**
+ * Construct queries to interact with the voicemail status table.
+ */
+public class VoicemailStatusQueryHelper {
+
+ final static String[] PROJECTION = new String[] {
+ Status._ID, // 0
+ Status.CONFIGURATION_STATE, // 1
+ Status.NOTIFICATION_CHANNEL_STATE, // 2
+ Status.SOURCE_PACKAGE // 3
+ };
+
+ public static final int _ID = 0;
+ public static final int CONFIGURATION_STATE = 1;
+ public static final int NOTIFICATION_CHANNEL_STATE = 2;
+ public static final int SOURCE_PACKAGE = 3;
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ private Uri mSourceUri;
+
+ public VoicemailStatusQueryHelper(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName());
+ }
+
+ /**
+ * Check if the configuration state for the voicemail source is "ok", meaning that the
+ * source is set up.
+ *
+ * @param phoneAccount The phone account for the voicemail source to check.
+ * @return {@code true} if the voicemail source is configured, {@code} false otherwise,
+ * including if the voicemail source is not registered in the table.
+ */
+ public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) {
+ return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK);
+ }
+
+ /**
+ * Check if the notifications channel of a voicemail source is active. That is, when a new
+ * voicemail is available, if the server able to notify the device.
+ *
+ * @return {@code true} if notifications channel is active, {@code false} otherwise.
+ */
+ public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) {
+ return isFieldEqualTo(phoneAccount, NOTIFICATION_CHANNEL_STATE,
+ Status.NOTIFICATION_CHANNEL_STATE_OK);
+ }
+
+ /**
+ * Check if a field for an entry in the status table is equal to a specific value.
+ *
+ * @param phoneAccount The phone account of the voicemail source to query for.
+ * @param columnIndex The column index of the field in the returned query.
+ * @param value The value to compare against.
+ * @return {@code true} if the stored value is equal to the provided value. {@code false}
+ * otherwise.
+ */
+ private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) {
+ Cursor cursor = null;
+ if (phoneAccount != null) {
+ String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+ String phoneAccountId = phoneAccount.getId();
+ if (phoneAccountComponentName == null || phoneAccountId == null) {
+ return false;
+ }
+ try {
+ String whereClause =
+ Status.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
+ Status.PHONE_ACCOUNT_ID + "=? AND " + Status.SOURCE_PACKAGE + "=?";
+ String[] whereArgs = { phoneAccountComponentName, phoneAccountId,
+ mContext.getPackageName()};
+ cursor = mContentResolver.query(
+ mSourceUri, PROJECTION, whereClause, whereArgs, null);
+ if (cursor != null && cursor.moveToFirst()) {
+ return cursor.getInt(columnIndex) == value;
+ }
+ }
+ finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java
new file mode 100644
index 000000000..1450e3d1b
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java
@@ -0,0 +1,244 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.VoicemailContract;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemailomtp.Voicemail;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Construct queries to interact with the voicemails table.
+ */
+public class VoicemailsQueryHelper {
+ final static String[] PROJECTION = new String[] {
+ Voicemails._ID, // 0
+ Voicemails.SOURCE_DATA, // 1
+ Voicemails.IS_READ, // 2
+ Voicemails.DELETED, // 3
+ Voicemails.TRANSCRIPTION // 4
+ };
+
+ public static final int _ID = 0;
+ public static final int SOURCE_DATA = 1;
+ public static final int IS_READ = 2;
+ public static final int DELETED = 3;
+ public static final int TRANSCRIPTION = 4;
+
+ final static String READ_SELECTION = Voicemails.DIRTY + "=1 AND "
+ + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1";
+ final static String DELETED_SELECTION = Voicemails.DELETED + "=1";
+
+ private Context mContext;
+ private ContentResolver mContentResolver;
+ private Uri mSourceUri;
+
+ public VoicemailsQueryHelper(Context context) {
+ mContext = context;
+ mContentResolver = context.getContentResolver();
+ mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName());
+ }
+
+ /**
+ * Get all the local read voicemails that have not been synced to the server.
+ *
+ * @return A list of read voicemails.
+ */
+ public List<Voicemail> getReadVoicemails() {
+ return getLocalVoicemails(READ_SELECTION);
+ }
+
+ /**
+ * Get all the locally deleted voicemails that have not been synced to the server.
+ *
+ * @return A list of deleted voicemails.
+ */
+ public List<Voicemail> getDeletedVoicemails() {
+ return getLocalVoicemails(DELETED_SELECTION);
+ }
+
+ /**
+ * Get all voicemails locally stored.
+ *
+ * @return A list of all locally stored voicemails.
+ */
+ public List<Voicemail> getAllVoicemails() {
+ return getLocalVoicemails(null);
+ }
+
+ /**
+ * Utility method to make queries to the voicemail database.
+ *
+ * @param selection A filter declaring which rows to return. {@code null} returns all rows.
+ * @return A list of voicemails according to the selection statement.
+ */
+ private List<Voicemail> getLocalVoicemails(String selection) {
+ Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null);
+ if (cursor == null) {
+ return null;
+ }
+ try {
+ List<Voicemail> voicemails = new ArrayList<Voicemail>();
+ while (cursor.moveToNext()) {
+ final long id = cursor.getLong(_ID);
+ final String sourceData = cursor.getString(SOURCE_DATA);
+ final boolean isRead = cursor.getInt(IS_READ) == 1;
+ final String transcription = cursor.getString(TRANSCRIPTION);
+ Voicemail voicemail = Voicemail
+ .createForUpdate(id, sourceData)
+ .setIsRead(isRead)
+ .setTranscription(transcription).build();
+ voicemails.add(voicemail);
+ }
+ return voicemails;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * Deletes a list of voicemails from the voicemail content provider.
+ *
+ * @param voicemails The list of voicemails to delete
+ * @return The number of voicemails deleted
+ */
+ public int deleteFromDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ if (count == 0) {
+ return 0;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ if (i > 0) {
+ sb.append(",");
+ }
+ sb.append(voicemails.get(i).getId());
+ }
+
+ String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
+ return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
+ }
+
+ /**
+ * Utility method to delete a single voicemail.
+ */
+ public void deleteFromDatabase(Voicemail voicemail) {
+ mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?",
+ new String[] { Long.toString(voicemail.getId()) });
+ }
+
+ public int markReadInDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ for (int i = 0; i < count; i++) {
+ markReadInDatabase(voicemails.get(i));
+ }
+ return count;
+ }
+
+ /**
+ * Utility method to mark single message as read.
+ */
+ public void markReadInDatabase(Voicemail voicemail) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.IS_READ, "1");
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /**
+ * Sends an update command to the voicemail content provider for a list of voicemails. From the
+ * view of the provider, since the updater is the owner of the entry, a blank "update" means
+ * that the voicemail source is indicating that the server has up-to-date information on the
+ * voicemail. This flips the "dirty" bit to "0".
+ *
+ * @param voicemails The list of voicemails to update
+ * @return The number of voicemails updated
+ */
+ public int markCleanInDatabase(List<Voicemail> voicemails) {
+ int count = voicemails.size();
+ for (int i = 0; i < count; i++) {
+ markCleanInDatabase(voicemails.get(i));
+ }
+ return count;
+ }
+
+ /**
+ * Utility method to mark single message as clean.
+ */
+ public void markCleanInDatabase(Voicemail voicemail) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /**
+ * Utility method to add a transcription to the voicemail.
+ */
+ public void updateWithTranscription(Voicemail voicemail, String transcription) {
+ Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId());
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.TRANSCRIPTION, transcription);
+ mContentResolver.update(uri, contentValues, null, null);
+ }
+
+ /**
+ * Voicemail is unique if the tuple of (phone account component name, phone account id, source
+ * data) is unique. If the phone account is missing, we also consider this unique since it's
+ * simply an "unknown" account.
+ * @param voicemail The voicemail to check if it is unique.
+ * @return {@code true} if the voicemail is unique, {@code false} otherwise.
+ */
+ public boolean isVoicemailUnique(Voicemail voicemail) {
+ Cursor cursor = null;
+ PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+ if (phoneAccount != null) {
+ String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
+ String phoneAccountId = phoneAccount.getId();
+ String sourceData = voicemail.getSourceData();
+ if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
+ return true;
+ }
+ try {
+ String whereClause =
+ Voicemails.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " +
+ Voicemails.PHONE_ACCOUNT_ID + "=? AND " + Voicemails.SOURCE_DATA + "=?";
+ String[] whereArgs = { phoneAccountComponentName, phoneAccountId, sourceData };
+ cursor = mContentResolver.query(
+ mSourceUri, PROJECTION, whereClause, whereArgs, null);
+ if (cursor.getCount() == 0) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java
new file mode 100644
index 000000000..966b940c2
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java
@@ -0,0 +1,118 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.annotation.TargetApi;
+import android.net.Network;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.NonNull;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+import java.io.Closeable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+/**
+ * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper,
+ * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed.
+ */
+@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/
+@TargetApi(VERSION_CODES.CUR_DEVELOPMENT)
+public class VvmNetworkRequest {
+
+ private static final String TAG = "VvmNetworkRequest";
+
+ /**
+ * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be
+ * closed once not needed anymore.
+ */
+ public static class NetworkWrapper implements Closeable {
+
+ private final Network mNetwork;
+ private final VvmNetworkRequestCallback mCallback;
+
+ private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) {
+ mNetwork = network;
+ mCallback = callback;
+ }
+
+ public Network get() {
+ return mNetwork;
+ }
+
+ @Override
+ public void close() {
+ mCallback.releaseNetwork();
+ }
+ }
+
+ public static class RequestFailedException extends Exception {
+
+ private RequestFailedException(Throwable cause) {
+ super(cause);
+ }
+ }
+
+ @NonNull
+ public static NetworkWrapper getNetwork(OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle handle, VoicemailStatus.Editor status) throws RequestFailedException {
+ FutureNetworkRequestCallback callback = new FutureNetworkRequestCallback(config, handle,
+ status);
+ callback.requestNetwork();
+ try {
+ return callback.getFuture().get();
+ } catch (InterruptedException | ExecutionException e) {
+ callback.releaseNetwork();
+ VvmLog.e(TAG, "can't get future network", e);
+ throw new RequestFailedException(e);
+ }
+ }
+
+ private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback {
+
+ /**
+ * {@link CompletableFuture#get()} will block until {@link CompletableFuture#
+ * complete(Object) } has been called on the other thread.
+ */
+ private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>();
+
+ public FutureNetworkRequestCallback(OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
+ super(config, phoneAccount, status);
+ }
+
+ public Future<NetworkWrapper> getFuture() {
+ return mFuture;
+ }
+
+ @Override
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ mFuture.complete(new NetworkWrapper(network, this));
+ }
+
+ @Override
+ public void onFailed(String reason) {
+ super.onFailed(reason);
+ mFuture.complete(null);
+ }
+
+ }
+}
diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java
new file mode 100644
index 000000000..8481a9d16
--- /dev/null
+++ b/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java
@@ -0,0 +1,171 @@
+/*
+ * 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.voicemailomtp.sync;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.CallSuper;
+import android.telecom.PhoneAccountHandle;
+
+import com.android.voicemailomtp.OmtpEvents;
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.TelephonyManagerStub;
+import com.android.voicemailomtp.VoicemailStatus;
+import com.android.voicemailomtp.VvmLog;
+
+/**
+ * Base class for network request call backs for visual voicemail syncing with the Imap server. This
+ * handles retries and network requests.
+ */
+public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback {
+
+ private static final String TAG = "VvmNetworkRequest";
+
+ // Timeout used to call ConnectivityManager.requestNetwork
+ private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
+
+ public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout";
+ public static final String NETWORK_REQUEST_FAILED_LOST = "lost";
+
+ protected Context mContext;
+ protected PhoneAccountHandle mPhoneAccount;
+ protected NetworkRequest mNetworkRequest;
+ private ConnectivityManager mConnectivityManager;
+ private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper;
+ private final VoicemailStatus.Editor mStatus;
+ private boolean mRequestSent = false;
+ private boolean mResultReceived = false;
+
+ public VvmNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount,
+ VoicemailStatus.Editor status) {
+ mContext = context;
+ mPhoneAccount = phoneAccount;
+ mStatus = status;
+ mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount);
+ mNetworkRequest = createNetworkRequest();
+ }
+
+ public VvmNetworkRequestCallback(OmtpVvmCarrierConfigHelper config,
+ PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) {
+ mContext = config.getContext();
+ mPhoneAccount = phoneAccount;
+ mStatus = status;
+ mCarrierConfigHelper = config;
+ mNetworkRequest = createNetworkRequest();
+ }
+
+ public VoicemailStatus.Editor getVoicemailStatusEditor() {
+ return mStatus;
+ }
+
+ /**
+ * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier
+ * requires it. Otherwise use whatever available.
+ */
+ private NetworkRequest createNetworkRequest() {
+
+ NetworkRequest.Builder builder = new NetworkRequest.Builder()
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+
+ if (mCarrierConfigHelper.isCellularDataRequired()) {
+ VvmLog.d(TAG, "Transport type: CELLULAR");
+ builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+ .setNetworkSpecifier(TelephonyManagerStub
+ .getNetworkSpecifierForPhoneAccountHandle(mContext, mPhoneAccount));
+ } else {
+ VvmLog.d(TAG, "Transport type: ANY");
+ }
+ return builder.build();
+ }
+
+ public NetworkRequest getNetworkRequest() {
+ return mNetworkRequest;
+ }
+
+ @Override
+ @CallSuper
+ public void onLost(Network network) {
+ VvmLog.d(TAG, "onLost");
+ mResultReceived = true;
+ onFailed(NETWORK_REQUEST_FAILED_LOST);
+ }
+
+ @Override
+ @CallSuper
+ public void onAvailable(Network network) {
+ super.onAvailable(network);
+ mResultReceived = true;
+ }
+
+ @CallSuper
+ public void onUnavailable() {
+ // TODO: b/32637799 this is hidden, do we really need this?
+ mResultReceived = true;
+ onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+ }
+
+ public void requestNetwork() {
+ if (mRequestSent == true) {
+ VvmLog.e(TAG, "requestNetwork() called twice");
+ return;
+ }
+ mRequestSent = true;
+ getConnectivityManager().requestNetwork(getNetworkRequest(), this);
+ /**
+ * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method.
+ * Implement our own timeout mechanism instead.
+ */
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (mResultReceived == false) {
+ onFailed(NETWORK_REQUEST_FAILED_TIMEOUT);
+ }
+ }
+ }, NETWORK_REQUEST_TIMEOUT_MILLIS);
+ }
+
+ public void releaseNetwork() {
+ VvmLog.d(TAG, "releaseNetwork");
+ getConnectivityManager().unregisterNetworkCallback(this);
+ }
+
+ public ConnectivityManager getConnectivityManager() {
+ if (mConnectivityManager == null) {
+ mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ }
+ return mConnectivityManager;
+ }
+
+ @CallSuper
+ public void onFailed(String reason) {
+ VvmLog.d(TAG, "onFailed: " + reason);
+ if (mCarrierConfigHelper.isCellularDataRequired()) {
+ mCarrierConfigHelper
+ .handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED);
+ } else {
+ mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION);
+ }
+ releaseNetwork();
+ }
+}
diff --git a/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java b/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java
new file mode 100644
index 000000000..eda7c4ee3
--- /dev/null
+++ b/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java
@@ -0,0 +1,160 @@
+/*
+ * 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.voicemailomtp.utils;
+
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Arrays;
+
+/**
+ * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on
+ * internal state. It also automatically wraps long lines based on given line length. <p> Delays
+ * writing indent until first actual write on a newline, enabling indent modification after
+ * newline.
+ */
+public class IndentingPrintWriter extends PrintWriter {
+
+ private final String mSingleIndent;
+ private final int mWrapLength;
+
+ /**
+ * Mutable version of current indent
+ */
+ private StringBuilder mIndentBuilder = new StringBuilder();
+ /**
+ * Cache of current {@link #mIndentBuilder} value
+ */
+ private char[] mCurrentIndent;
+ /**
+ * Length of current line being built, excluding any indent
+ */
+ private int mCurrentLength;
+
+ /**
+ * Flag indicating if we're currently sitting on an empty line, and that next write should be
+ * prefixed with the current indent.
+ */
+ private boolean mEmptyLine = true;
+
+ private char[] mSingleChar = new char[1];
+
+ public IndentingPrintWriter(Writer writer, String singleIndent) {
+ this(writer, singleIndent, -1);
+ }
+
+ public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) {
+ super(writer);
+ mSingleIndent = singleIndent;
+ mWrapLength = wrapLength;
+ }
+
+ public void increaseIndent() {
+ mIndentBuilder.append(mSingleIndent);
+ mCurrentIndent = null;
+ }
+
+ public void decreaseIndent() {
+ mIndentBuilder.delete(0, mSingleIndent.length());
+ mCurrentIndent = null;
+ }
+
+ public void printPair(String key, Object value) {
+ print(key + "=" + String.valueOf(value) + " ");
+ }
+
+ public void printPair(String key, Object[] value) {
+ print(key + "=" + Arrays.toString(value) + " ");
+ }
+
+ public void printHexPair(String key, int value) {
+ print(key + "=0x" + Integer.toHexString(value) + " ");
+ }
+
+ @Override
+ public void println() {
+ write('\n');
+ }
+
+ @Override
+ public void write(int c) {
+ mSingleChar[0] = (char) c;
+ write(mSingleChar, 0, 1);
+ }
+
+ @Override
+ public void write(String s, int off, int len) {
+ final char[] buf = new char[len];
+ s.getChars(off, len - off, buf, 0);
+ write(buf, 0, len);
+ }
+
+ @Override
+ public void write(char[] buf, int offset, int count) {
+ final int indentLength = mIndentBuilder.length();
+ final int bufferEnd = offset + count;
+ int lineStart = offset;
+ int lineEnd = offset;
+
+ // March through incoming buffer looking for newlines
+ while (lineEnd < bufferEnd) {
+ char ch = buf[lineEnd++];
+ mCurrentLength++;
+ if (ch == '\n') {
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ lineStart = lineEnd;
+ mEmptyLine = true;
+ mCurrentLength = 0;
+ }
+
+ // Wrap if we've pushed beyond line length
+ if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) {
+ if (!mEmptyLine) {
+ // Give ourselves a fresh line to work with
+ super.write('\n');
+ mEmptyLine = true;
+ mCurrentLength = lineEnd - lineStart;
+ } else {
+ // We need more than a dedicated line, slice it hard
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ super.write('\n');
+ mEmptyLine = true;
+ lineStart = lineEnd;
+ mCurrentLength = 0;
+ }
+ }
+ }
+
+ if (lineStart != lineEnd) {
+ maybeWriteIndent();
+ super.write(buf, lineStart, lineEnd - lineStart);
+ }
+ }
+
+ private void maybeWriteIndent() {
+ if (mEmptyLine) {
+ mEmptyLine = false;
+ if (mIndentBuilder.length() != 0) {
+ if (mCurrentIndent == null) {
+ mCurrentIndent = mIndentBuilder.toString().toCharArray();
+ }
+ super.write(mCurrentIndent, 0, mCurrentIndent.length);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java
new file mode 100644
index 000000000..f94070ecd
--- /dev/null
+++ b/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.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.voicemailomtp.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.net.Uri;
+import android.provider.VoicemailContract.Voicemails;
+import android.telecom.PhoneAccountHandle;
+import com.android.voicemailomtp.Voicemail;
+import java.util.List;
+
+public class VoicemailDatabaseUtil {
+
+ /**
+ * Inserts a new voicemail into the voicemail content provider.
+ *
+ * @param context The context of the app doing the inserting
+ * @param voicemail Data to be inserted
+ * @return {@link Uri} of the newly inserted {@link Voicemail}
+ * @hide
+ */
+ public static Uri insert(Context context, Voicemail voicemail) {
+ ContentResolver contentResolver = context.getContentResolver();
+ ContentValues contentValues = getContentValues(voicemail);
+ return contentResolver
+ .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues);
+ }
+
+ /**
+ * Inserts a list of voicemails into the voicemail content provider.
+ *
+ * @param context The context of the app doing the inserting
+ * @param voicemails Data to be inserted
+ * @return the number of voicemails inserted
+ * @hide
+ */
+ public static int insert(Context context, List<Voicemail> voicemails) {
+ ContentResolver contentResolver = context.getContentResolver();
+ int count = voicemails.size();
+ for (int i = 0; i < count; i++) {
+ ContentValues contentValues = getContentValues(voicemails.get(i));
+ contentResolver
+ .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues);
+ }
+ return count;
+ }
+
+
+ /**
+ * Maps structured {@link Voicemail} to {@link ContentValues} in content provider.
+ */
+ private static ContentValues getContentValues(Voicemail voicemail) {
+ ContentValues contentValues = new ContentValues();
+ contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis()));
+ contentValues.put(Voicemails.NUMBER, voicemail.getNumber());
+ contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration()));
+ contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage());
+ contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData());
+ contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0);
+
+ PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
+ if (phoneAccount != null) {
+ contentValues.put(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME,
+ phoneAccount.getComponentName().flattenToString());
+ contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId());
+ }
+
+ if (voicemail.getTranscription() != null) {
+ contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription());
+ }
+
+ return contentValues;
+ }
+}
diff --git a/java/com/android/voicemailomtp/utils/VvmDumpHandler.java b/java/com/android/voicemailomtp/utils/VvmDumpHandler.java
new file mode 100644
index 000000000..5768a9c19
--- /dev/null
+++ b/java/com/android/voicemailomtp/utils/VvmDumpHandler.java
@@ -0,0 +1,46 @@
+/*
+ * 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.voicemailomtp.utils;
+
+import android.content.Context;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+
+import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper;
+import com.android.voicemailomtp.VvmLog;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+public class VvmDumpHandler {
+
+ public static void dump(Context context, FileDescriptor fd, PrintWriter writer,
+ String[] args) {
+ IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, " ");
+ indentedWriter.println("******* OmtpVvm *******");
+ indentedWriter.println("======= Configs =======");
+ indentedWriter.increaseIndent();
+ for (PhoneAccountHandle handle : context.getSystemService(TelecomManager.class)
+ .getCallCapablePhoneAccounts()) {
+ OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle);
+ indentedWriter.println(config.toString());
+ }
+ indentedWriter.decreaseIndent();
+ indentedWriter.println("======== Logs =========");
+ VvmLog.dump(fd, indentedWriter, args);
+ }
+}
diff --git a/java/com/android/voicemailomtp/utils/XmlUtils.java b/java/com/android/voicemailomtp/utils/XmlUtils.java
new file mode 100644
index 000000000..768247e27
--- /dev/null
+++ b/java/com/android/voicemailomtp/utils/XmlUtils.java
@@ -0,0 +1,245 @@
+/*
+ * 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.voicemailomtp.utils;
+
+import android.util.ArrayMap;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class XmlUtils {
+
+ public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag,
+ String[] name, ReadMapCallback callback)
+ throws XmlPullParserException, java.io.IOException {
+ ArrayMap<String, Object> map = new ArrayMap<>();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ Object val = readThisValueXml(parser, name, callback, true);
+ map.put(name[0], val);
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return map;
+ }
+ throw new XmlPullParserException(
+ "Expected " + endTag + " end tag at: " + parser.getName());
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ throw new XmlPullParserException(
+ "Document ended before " + endTag + " end tag");
+ }
+
+ /**
+ * Read an ArrayList object from an XmlPullParser. The XML data could previously have been
+ * generated by writeListXml(). The XmlPullParser must be positioned <em>after</em> the tag
+ * that begins the list.
+ *
+ * @param parser The XmlPullParser from which to read the list data.
+ * @param endTag Name of the tag that will end the list, usually "list".
+ * @param name An array of one string, used to return the name attribute of the list's tag.
+ * @return HashMap The newly generated list.
+ */
+ public static final ArrayList readThisListXml(XmlPullParser parser, String endTag,
+ String[] name, ReadMapCallback callback, boolean arrayMap)
+ throws XmlPullParserException, java.io.IOException {
+ ArrayList list = new ArrayList();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ Object val = readThisValueXml(parser, name, callback, arrayMap);
+ list.add(val);
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return list;
+ }
+ throw new XmlPullParserException(
+ "Expected " + endTag + " end tag at: " + parser.getName());
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ throw new XmlPullParserException(
+ "Document ended before " + endTag + " end tag");
+ }
+
+ /**
+ * Read a String[] object from an XmlPullParser. The XML data could previously have been
+ * generated by writeStringArrayXml(). The XmlPullParser must be positioned <em>after</em> the
+ * tag that begins the list.
+ *
+ * @param parser The XmlPullParser from which to read the list data.
+ * @param endTag Name of the tag that will end the list, usually "string-array".
+ * @param name An array of one string, used to return the name attribute of the list's tag.
+ * @return Returns a newly generated String[].
+ */
+ public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag,
+ String[] name) throws XmlPullParserException, java.io.IOException {
+
+ parser.next();
+
+ List<String> array = new ArrayList<>();
+
+ int eventType = parser.getEventType();
+ do {
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ try {
+ array.add(parser.getAttributeValue(null, "value"));
+ } catch (NullPointerException e) {
+ throw new XmlPullParserException("Need value attribute in item");
+ } catch (NumberFormatException e) {
+ throw new XmlPullParserException("Not a number in value attribute in item");
+ }
+ } else {
+ throw new XmlPullParserException("Expected item tag at: " + parser.getName());
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(endTag)) {
+ return array.toArray(new String[0]);
+ } else if (parser.getName().equals("item")) {
+
+ } else {
+ throw new XmlPullParserException("Expected " + endTag + " end tag at: " +
+ parser.getName());
+ }
+ }
+ eventType = parser.next();
+ } while (eventType != XmlPullParser.END_DOCUMENT);
+
+ throw new XmlPullParserException("Document ended before " + endTag + " end tag");
+ }
+
+ private static Object readThisValueXml(XmlPullParser parser, String[] name,
+ ReadMapCallback callback, boolean arrayMap)
+ throws XmlPullParserException, java.io.IOException {
+ final String valueName = parser.getAttributeValue(null, "name");
+ final String tagName = parser.getName();
+
+ Object res;
+
+ if (tagName.equals("null")) {
+ res = null;
+ } else if (tagName.equals("string")) {
+ String value = "";
+ int eventType;
+ while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("string")) {
+ name[0] = valueName;
+ return value;
+ }
+ throw new XmlPullParserException(
+ "Unexpected end tag in <string>: " + parser.getName());
+ } else if (eventType == XmlPullParser.TEXT) {
+ value += parser.getText();
+ } else if (eventType == XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ "Unexpected start tag in <string>: " + parser.getName());
+ }
+ }
+ throw new XmlPullParserException(
+ "Unexpected end of document in <string>");
+ } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) {
+ // all work already done by readThisPrimitiveValueXml
+ } else if (tagName.equals("string-array")) {
+ res = readThisStringArrayXml(parser, "string-array", name);
+ name[0] = valueName;
+ return res;
+ } else if (tagName.equals("list")) {
+ parser.next();
+ res = readThisListXml(parser, "list", name, callback, arrayMap);
+ name[0] = valueName;
+ return res;
+ } else if (callback != null) {
+ res = callback.readThisUnknownObjectXml(parser, tagName);
+ name[0] = valueName;
+ return res;
+ } else {
+ throw new XmlPullParserException("Unknown tag: " + tagName);
+ }
+
+ // Skip through to end tag.
+ int eventType;
+ while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals(tagName)) {
+ name[0] = valueName;
+ return res;
+ }
+ throw new XmlPullParserException(
+ "Unexpected end tag in <" + tagName + ">: " + parser.getName());
+ } else if (eventType == XmlPullParser.TEXT) {
+ throw new XmlPullParserException(
+ "Unexpected text in <" + tagName + ">: " + parser.getName());
+ } else if (eventType == XmlPullParser.START_TAG) {
+ throw new XmlPullParserException(
+ "Unexpected start tag in <" + tagName + ">: " + parser.getName());
+ }
+ }
+ throw new XmlPullParserException(
+ "Unexpected end of document in <" + tagName + ">");
+ }
+
+ private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName)
+ throws XmlPullParserException, java.io.IOException {
+ try {
+ if (tagName.equals("int")) {
+ return Integer.parseInt(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("long")) {
+ return Long.valueOf(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("float")) {
+ return Float.valueOf(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("double")) {
+ return Double.valueOf(parser.getAttributeValue(null, "value"));
+ } else if (tagName.equals("boolean")) {
+ return Boolean.valueOf(parser.getAttributeValue(null, "value"));
+ } else {
+ return null;
+ }
+ } catch (NullPointerException e) {
+ throw new XmlPullParserException("Need value attribute in <" + tagName + ">");
+ } catch (NumberFormatException e) {
+ throw new XmlPullParserException(
+ "Not a number in value attribute in <" + tagName + ">");
+ }
+ }
+
+ public interface ReadMapCallback {
+
+ /**
+ * Called from readThisMapXml when a START_TAG is not recognized. The input stream is
+ * positioned within the start tag so that attributes can be read using in.getAttribute.
+ *
+ * @param in the XML input stream
+ * @param tag the START_TAG that was not recognized.
+ * @return the Object parsed from the stream which will be put into the map.
+ * @throws XmlPullParserException if the START_TAG is not recognized.
+ * @throws IOException on XmlPullParser serialization errors.
+ */
+ Object readThisUnknownObjectXml(XmlPullParser in, String tag)
+ throws XmlPullParserException, IOException;
+ }
+} \ No newline at end of file