summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/app
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/com/android/dialer/app
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/com/android/dialer/app')
-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
354 files changed, 28363 insertions, 0 deletions
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();
+ }
+}