From ccca31529c07970e89419fb85a9e8153a5396838 Mon Sep 17 00:00:00 2001 From: Eric Erfanian Date: Wed, 22 Feb 2017 16:32:36 -0800 Subject: Update dialer sources. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958 --- java/com/android/dialer/animation/AnimUtils.java | 247 +++ .../dialer/animation/AnimationListenerAdapter.java | 39 + java/com/android/dialer/app/AndroidManifest.xml | 116 ++ java/com/android/dialer/app/Bindings.java | 77 + .../com/android/dialer/app/CallDetailActivity.java | 480 ++++++ java/com/android/dialer/app/DialerApplication.java | 77 + java/com/android/dialer/app/DialtactsActivity.java | 1484 +++++++++++++++++ .../dialer/app/FloatingActionButtonBehavior.java | 50 + java/com/android/dialer/app/PhoneCallDetails.java | 207 +++ .../android/dialer/app/SpecialCharSequenceMgr.java | 493 ++++++ .../com/android/dialer/app/alert/AlertManager.java | 30 + .../dialer/app/bindings/DialerBindings.java | 25 + .../dialer/app/bindings/DialerBindingsFactory.java | 26 + .../dialer/app/bindings/DialerBindingsStub.java | 48 + .../app/calllog/BlockReportSpamListener.java | 212 +++ .../app/calllog/CallDetailHistoryAdapter.java | 214 +++ .../android/dialer/app/calllog/CallLogAdapter.java | 915 +++++++++++ .../dialer/app/calllog/CallLogAlertManager.java | 90 ++ .../android/dialer/app/calllog/CallLogAsync.java | 96 ++ .../dialer/app/calllog/CallLogAsyncTaskUtil.java | 376 +++++ .../dialer/app/calllog/CallLogFragment.java | 528 ++++++ .../dialer/app/calllog/CallLogGroupBuilder.java | 274 ++++ .../dialer/app/calllog/CallLogListItemHelper.java | 277 ++++ .../app/calllog/CallLogListItemViewHolder.java | 966 +++++++++++ .../app/calllog/CallLogModalAlertManager.java | 74 + .../app/calllog/CallLogNotificationsHelper.java | 299 ++++ .../app/calllog/CallLogNotificationsService.java | 203 +++ .../dialer/app/calllog/CallLogReceiver.java | 77 + .../android/dialer/app/calllog/CallTypeHelper.java | 136 ++ .../dialer/app/calllog/CallTypeIconsView.java | 221 +++ .../dialer/app/calllog/ClearCallLogDialog.java | 98 ++ .../app/calllog/DefaultVoicemailNotifier.java | 273 ++++ .../dialer/app/calllog/GroupingListAdapter.java | 153 ++ .../android/dialer/app/calllog/IntentProvider.java | 198 +++ .../calllog/MissedCallNotificationReceiver.java | 50 + .../dialer/app/calllog/MissedCallNotifier.java | 330 ++++ .../dialer/app/calllog/PhoneAccountUtils.java | 104 ++ .../dialer/app/calllog/PhoneCallDetailsHelper.java | 352 ++++ .../dialer/app/calllog/PhoneCallDetailsViews.java | 75 + .../dialer/app/calllog/PhoneNumberDisplayUtil.java | 85 + .../calllog/VisualVoicemailCallLogFragment.java | 132 ++ .../dialer/app/calllog/VoicemailQueryHandler.java | 74 + .../app/calllog/calllogcache/CallLogCache.java | 105 ++ .../calllog/calllogcache/CallLogCacheLollipop.java | 74 + .../calllogcache/CallLogCacheLollipopMr1.java | 116 ++ .../dialer/app/contactinfo/ContactInfoCache.java | 357 +++++ .../dialer/app/contactinfo/ContactInfoRequest.java | 122 ++ .../dialer/app/contactinfo/ContactPhotoLoader.java | 129 ++ .../ExpirableCacheHeadlessFragment.java | 67 + .../app/contactinfo/NumberWithCountryIso.java | 57 + .../dialer/app/dialpad/DialpadFragment.java | 1689 ++++++++++++++++++++ .../app/dialpad/PseudoEmergencyAnimator.java | 161 ++ .../dialer/app/dialpad/SmartDialCursorLoader.java | 202 +++ .../app/dialpad/UnicodeDialerKeyListener.java | 56 + .../app/filterednumber/BlockedNumbersAdapter.java | 97 ++ .../app/filterednumber/BlockedNumbersFragment.java | 271 ++++ .../BlockedNumbersSettingsActivity.java | 146 ++ .../dialer/app/filterednumber/NumbersAdapter.java | 138 ++ .../filterednumber/ViewNumbersToImportAdapter.java | 56 + .../ViewNumbersToImportFragment.java | 130 ++ .../app/legacybindings/DialerLegacyBindings.java | 47 + .../DialerLegacyBindingsFactory.java | 26 + .../legacybindings/DialerLegacyBindingsStub.java | 53 + .../dialer/app/list/AllContactsFragment.java | 209 +++ .../dialer/app/list/BlockedListSearchAdapter.java | 84 + .../dialer/app/list/BlockedListSearchFragment.java | 245 +++ .../dialer/app/list/ContentChangedFilter.java | 56 + .../app/list/DialerPhoneNumberListAdapter.java | 228 +++ .../dialer/app/list/DragDropController.java | 106 ++ .../com/android/dialer/app/list/ListsFragment.java | 587 +++++++ .../dialer/app/list/OnDragDropListener.java | 58 + .../app/list/OnListFragmentScrolledListener.java | 27 + .../dialer/app/list/PhoneFavoriteListView.java | 315 ++++ .../app/list/PhoneFavoriteSquareTileView.java | 119 ++ .../dialer/app/list/PhoneFavoriteTileView.java | 155 ++ .../dialer/app/list/PhoneFavoritesTileAdapter.java | 627 ++++++++ .../dialer/app/list/RegularSearchFragment.java | 146 ++ .../dialer/app/list/RegularSearchListAdapter.java | 126 ++ java/com/android/dialer/app/list/RemoveView.java | 105 ++ .../android/dialer/app/list/SearchFragment.java | 425 +++++ .../app/list/SmartDialNumberListAdapter.java | 117 ++ .../dialer/app/list/SmartDialSearchFragment.java | 120 ++ .../android/dialer/app/list/SpeedDialFragment.java | 512 ++++++ .../app/manifests/activities/AndroidManifest.xml | 129 ++ .../app/res/color/settings_text_color_primary.xml | 23 + .../res/color/settings_text_color_secondary.xml | 23 + .../app/res/drawable-hdpi/empty_call_log.png | Bin 0 -> 3538 bytes .../app/res/drawable-hdpi/empty_contacts.png | Bin 0 -> 2461 bytes .../app/res/drawable-hdpi/empty_speed_dial.png | Bin 0 -> 6041 bytes .../dialer/app/res/drawable-hdpi/fab_ic_dial.png | Bin 0 -> 1028 bytes .../res/drawable-hdpi/ic_archive_white_24dp.png | Bin 0 -> 247 bytes .../dialer/app/res/drawable-hdpi/ic_call_arrow.png | Bin 0 -> 538 bytes .../app/res/drawable-hdpi/ic_content_copy_24dp.png | Bin 0 -> 203 bytes .../app/res/drawable-hdpi/ic_delete_24dp.png | Bin 0 -> 242 bytes .../res/drawable-hdpi/ic_dialer_fork_add_call.png | Bin 0 -> 1649 bytes .../drawable-hdpi/ic_dialer_fork_current_call.png | Bin 0 -> 2305 bytes .../res/drawable-hdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 2419 bytes .../dialer/app/res/drawable-hdpi/ic_grade_24dp.png | Bin 0 -> 370 bytes .../dialer/app/res/drawable-hdpi/ic_handle.png | Bin 0 -> 543 bytes .../app/res/drawable-hdpi/ic_menu_history_lt.png | Bin 0 -> 1565 bytes .../app/res/drawable-hdpi/ic_mic_grey600.png | Bin 0 -> 377 bytes .../app/res/drawable-hdpi/ic_more_vert_24dp.png | Bin 0 -> 134 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 565 bytes .../dialer/app/res/drawable-hdpi/ic_not_spam.png | Bin 0 -> 858 bytes .../dialer/app/res/drawable-hdpi/ic_pause_24dp.png | Bin 0 -> 105 bytes .../app/res/drawable-hdpi/ic_people_24dp.png | Bin 0 -> 299 bytes .../dialer/app/res/drawable-hdpi/ic_phone_24dp.png | Bin 0 -> 347 bytes .../app/res/drawable-hdpi/ic_play_arrow_24dp.png | Bin 0 -> 195 bytes .../dialer/app/res/drawable-hdpi/ic_remove.png | Bin 0 -> 884 bytes .../app/res/drawable-hdpi/ic_results_phone.png | Bin 0 -> 1084 bytes .../app/res/drawable-hdpi/ic_schedule_24dp.png | Bin 0 -> 575 bytes .../app/res/drawable-hdpi/ic_share_white_24dp.png | Bin 0 -> 397 bytes .../dialer/app/res/drawable-hdpi/ic_star.png | Bin 0 -> 732 bytes .../dialer/app/res/drawable-hdpi/ic_unblock.png | Bin 0 -> 1049 bytes .../app/res/drawable-hdpi/ic_vm_sound_off_dis.png | Bin 0 -> 1339 bytes .../app/res/drawable-hdpi/ic_vm_sound_off_dk.png | Bin 0 -> 1337 bytes .../app/res/drawable-hdpi/ic_vm_sound_on_dis.png | Bin 0 -> 1755 bytes .../app/res/drawable-hdpi/ic_vm_sound_on_dk.png | Bin 0 -> 1750 bytes .../app/res/drawable-hdpi/ic_voicemail_24dp.png | Bin 0 -> 478 bytes .../app/res/drawable-hdpi/ic_volume_down_24dp.png | Bin 0 -> 186 bytes .../app/res/drawable-hdpi/ic_volume_up_24dp.png | Bin 0 -> 365 bytes .../app/res/drawable-hdpi/search_shadow.9.png | Bin 0 -> 183 bytes .../app/res/drawable-hdpi/shadow_contact_photo.png | Bin 0 -> 960 bytes .../app/res/drawable-mdpi/empty_call_log.png | Bin 0 -> 2463 bytes .../app/res/drawable-mdpi/empty_contacts.png | Bin 0 -> 1778 bytes .../app/res/drawable-mdpi/empty_speed_dial.png | Bin 0 -> 4119 bytes .../dialer/app/res/drawable-mdpi/fab_ic_dial.png | Bin 0 -> 905 bytes .../res/drawable-mdpi/ic_archive_white_24dp.png | Bin 0 -> 181 bytes .../dialer/app/res/drawable-mdpi/ic_call_arrow.png | Bin 0 -> 455 bytes .../app/res/drawable-mdpi/ic_content_copy_24dp.png | Bin 0 -> 134 bytes .../app/res/drawable-mdpi/ic_delete_24dp.png | Bin 0 -> 195 bytes .../res/drawable-mdpi/ic_dialer_fork_add_call.png | Bin 0 -> 1309 bytes .../drawable-mdpi/ic_dialer_fork_current_call.png | Bin 0 -> 1581 bytes .../res/drawable-mdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 1586 bytes .../dialer/app/res/drawable-mdpi/ic_grade_24dp.png | Bin 0 -> 271 bytes .../dialer/app/res/drawable-mdpi/ic_handle.png | Bin 0 -> 454 bytes .../app/res/drawable-mdpi/ic_menu_history_lt.png | Bin 0 -> 1086 bytes .../app/res/drawable-mdpi/ic_mic_grey600.png | Bin 0 -> 252 bytes .../app/res/drawable-mdpi/ic_more_vert_24dp.png | Bin 0 -> 112 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 377 bytes .../dialer/app/res/drawable-mdpi/ic_not_spam.png | Bin 0 -> 627 bytes .../dialer/app/res/drawable-mdpi/ic_pause_24dp.png | Bin 0 -> 83 bytes .../app/res/drawable-mdpi/ic_people_24dp.png | Bin 0 -> 210 bytes .../dialer/app/res/drawable-mdpi/ic_phone_24dp.png | Bin 0 -> 262 bytes .../app/res/drawable-mdpi/ic_play_arrow_24dp.png | Bin 0 -> 157 bytes .../dialer/app/res/drawable-mdpi/ic_remove.png | Bin 0 -> 728 bytes .../app/res/drawable-mdpi/ic_results_phone.png | Bin 0 -> 801 bytes .../app/res/drawable-mdpi/ic_schedule_24dp.png | Bin 0 -> 377 bytes .../app/res/drawable-mdpi/ic_share_white_24dp.png | Bin 0 -> 268 bytes .../dialer/app/res/drawable-mdpi/ic_star.png | Bin 0 -> 531 bytes .../dialer/app/res/drawable-mdpi/ic_unblock.png | Bin 0 -> 746 bytes .../app/res/drawable-mdpi/ic_vm_sound_off_dis.png | Bin 0 -> 948 bytes .../app/res/drawable-mdpi/ic_vm_sound_off_dk.png | Bin 0 -> 945 bytes .../app/res/drawable-mdpi/ic_vm_sound_on_dis.png | Bin 0 -> 1166 bytes .../app/res/drawable-mdpi/ic_vm_sound_on_dk.png | Bin 0 -> 1192 bytes .../app/res/drawable-mdpi/ic_voicemail_24dp.png | Bin 0 -> 221 bytes .../app/res/drawable-mdpi/ic_volume_down_24dp.png | Bin 0 -> 139 bytes .../app/res/drawable-mdpi/ic_volume_up_24dp.png | Bin 0 -> 251 bytes .../app/res/drawable-mdpi/search_shadow.9.png | Bin 0 -> 159 bytes .../app/res/drawable-mdpi/shadow_contact_photo.png | Bin 0 -> 948 bytes .../app/res/drawable-xhdpi/empty_call_log.png | Bin 0 -> 4860 bytes .../app/res/drawable-xhdpi/empty_contacts.png | Bin 0 -> 3352 bytes .../app/res/drawable-xhdpi/empty_speed_dial.png | Bin 0 -> 8689 bytes .../dialer/app/res/drawable-xhdpi/fab_ic_dial.png | Bin 0 -> 1699 bytes .../res/drawable-xhdpi/ic_archive_white_24dp.png | Bin 0 -> 267 bytes .../app/res/drawable-xhdpi/ic_call_arrow.png | Bin 0 -> 627 bytes .../res/drawable-xhdpi/ic_content_copy_24dp.png | Bin 0 -> 188 bytes .../app/res/drawable-xhdpi/ic_delete_24dp.png | Bin 0 -> 271 bytes .../res/drawable-xhdpi/ic_dialer_fork_add_call.png | Bin 0 -> 2150 bytes .../drawable-xhdpi/ic_dialer_fork_current_call.png | Bin 0 -> 3154 bytes .../drawable-xhdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 3298 bytes .../app/res/drawable-xhdpi/ic_grade_24dp.png | Bin 0 -> 479 bytes .../dialer/app/res/drawable-xhdpi/ic_handle.png | Bin 0 -> 681 bytes .../app/res/drawable-xhdpi/ic_menu_history_lt.png | Bin 0 -> 2237 bytes .../app/res/drawable-xhdpi/ic_mic_grey600.png | Bin 0 -> 454 bytes .../app/res/drawable-xhdpi/ic_more_vert_24dp.png | Bin 0 -> 158 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 755 bytes .../dialer/app/res/drawable-xhdpi/ic_not_spam.png | Bin 0 -> 996 bytes .../app/res/drawable-xhdpi/ic_pause_24dp.png | Bin 0 -> 90 bytes .../app/res/drawable-xhdpi/ic_people_24dp.png | Bin 0 -> 368 bytes .../app/res/drawable-xhdpi/ic_phone_24dp.png | Bin 0 -> 439 bytes .../app/res/drawable-xhdpi/ic_play_arrow_24dp.png | Bin 0 -> 220 bytes .../dialer/app/res/drawable-xhdpi/ic_remove.png | Bin 0 -> 1237 bytes .../app/res/drawable-xhdpi/ic_results_phone.png | Bin 0 -> 1376 bytes .../app/res/drawable-xhdpi/ic_schedule_24dp.png | Bin 0 -> 737 bytes .../app/res/drawable-xhdpi/ic_share_white_24dp.png | Bin 0 -> 496 bytes .../dialer/app/res/drawable-xhdpi/ic_star.png | Bin 0 -> 889 bytes .../dialer/app/res/drawable-xhdpi/ic_unblock.png | Bin 0 -> 1356 bytes .../app/res/drawable-xhdpi/ic_vm_sound_off_dis.png | Bin 0 -> 1794 bytes .../app/res/drawable-xhdpi/ic_vm_sound_off_dk.png | Bin 0 -> 1794 bytes .../app/res/drawable-xhdpi/ic_vm_sound_on_dis.png | Bin 0 -> 2354 bytes .../app/res/drawable-xhdpi/ic_vm_sound_on_dk.png | Bin 0 -> 2339 bytes .../app/res/drawable-xhdpi/ic_voicemail_24dp.png | Bin 0 -> 487 bytes .../app/res/drawable-xhdpi/ic_volume_down_24dp.png | Bin 0 -> 212 bytes .../app/res/drawable-xhdpi/ic_volume_up_24dp.png | Bin 0 -> 455 bytes .../app/res/drawable-xhdpi/search_shadow.9.png | Bin 0 -> 198 bytes .../res/drawable-xhdpi/shadow_contact_photo.png | Bin 0 -> 965 bytes .../app/res/drawable-xxhdpi/empty_call_log.png | Bin 0 -> 6226 bytes .../app/res/drawable-xxhdpi/empty_contacts.png | Bin 0 -> 3686 bytes .../app/res/drawable-xxhdpi/empty_speed_dial.png | Bin 0 -> 11039 bytes .../dialer/app/res/drawable-xxhdpi/fab_ic_dial.png | Bin 0 -> 3042 bytes .../res/drawable-xxhdpi/ic_archive_white_24dp.png | Bin 0 -> 390 bytes .../app/res/drawable-xxhdpi/ic_call_arrow.png | Bin 0 -> 1203 bytes .../res/drawable-xxhdpi/ic_content_copy_24dp.png | Bin 0 -> 266 bytes .../app/res/drawable-xxhdpi/ic_delete_24dp.png | Bin 0 -> 323 bytes .../drawable-xxhdpi/ic_dialer_fork_add_call.png | Bin 0 -> 2583 bytes .../ic_dialer_fork_current_call.png | Bin 0 -> 3622 bytes .../drawable-xxhdpi/ic_dialer_fork_tt_keypad.png | Bin 0 -> 3229 bytes .../app/res/drawable-xxhdpi/ic_grade_24dp.png | Bin 0 -> 676 bytes .../dialer/app/res/drawable-xxhdpi/ic_handle.png | Bin 0 -> 1431 bytes .../app/res/drawable-xxhdpi/ic_menu_history_lt.png | Bin 0 -> 2945 bytes .../app/res/drawable-xxhdpi/ic_mic_grey600.png | Bin 0 -> 631 bytes .../app/res/drawable-xxhdpi/ic_more_vert_24dp.png | Bin 0 -> 216 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 1112 bytes .../dialer/app/res/drawable-xxhdpi/ic_not_spam.png | Bin 0 -> 1340 bytes .../app/res/drawable-xxhdpi/ic_pause_24dp.png | Bin 0 -> 92 bytes .../app/res/drawable-xxhdpi/ic_people_24dp.png | Bin 0 -> 488 bytes .../app/res/drawable-xxhdpi/ic_phone_24dp.png | Bin 0 -> 619 bytes .../app/res/drawable-xxhdpi/ic_play_arrow_24dp.png | Bin 0 -> 283 bytes .../dialer/app/res/drawable-xxhdpi/ic_remove.png | Bin 0 -> 1942 bytes .../app/res/drawable-xxhdpi/ic_results_phone.png | Bin 0 -> 2090 bytes .../app/res/drawable-xxhdpi/ic_schedule_24dp.png | Bin 0 -> 1107 bytes .../res/drawable-xxhdpi/ic_share_white_24dp.png | Bin 0 -> 698 bytes .../dialer/app/res/drawable-xxhdpi/ic_star.png | Bin 0 -> 1539 bytes .../dialer/app/res/drawable-xxhdpi/ic_unblock.png | Bin 0 -> 1990 bytes .../res/drawable-xxhdpi/ic_vm_sound_off_dis.png | Bin 0 -> 2316 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png | Bin 0 -> 2319 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png | Bin 0 -> 2878 bytes .../app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png | Bin 0 -> 2879 bytes .../app/res/drawable-xxhdpi/ic_voicemail_24dp.png | Bin 0 -> 625 bytes .../res/drawable-xxhdpi/ic_volume_down_24dp.png | Bin 0 -> 291 bytes .../app/res/drawable-xxhdpi/ic_volume_up_24dp.png | Bin 0 -> 654 bytes .../app/res/drawable-xxhdpi/search_shadow.9.png | Bin 0 -> 1148 bytes .../res/drawable-xxhdpi/shadow_contact_photo.png | Bin 0 -> 970 bytes .../app/res/drawable-xxxhdpi/empty_call_log.png | Bin 0 -> 8761 bytes .../app/res/drawable-xxxhdpi/empty_contacts.png | Bin 0 -> 5204 bytes .../app/res/drawable-xxxhdpi/fab_ic_dial.png | Bin 0 -> 3800 bytes .../res/drawable-xxxhdpi/ic_archive_white_24dp.png | Bin 0 -> 489 bytes .../app/res/drawable-xxxhdpi/ic_call_arrow.png | Bin 0 -> 1344 bytes .../res/drawable-xxxhdpi/ic_content_copy_24dp.png | Bin 0 -> 329 bytes .../app/res/drawable-xxxhdpi/ic_delete_24dp.png | Bin 0 -> 1394 bytes .../app/res/drawable-xxxhdpi/ic_grade_24dp.png | Bin 0 -> 887 bytes .../dialer/app/res/drawable-xxxhdpi/ic_handle.png | Bin 0 -> 1687 bytes .../app/res/drawable-xxxhdpi/ic_mic_grey600.png | Bin 0 -> 853 bytes .../app/res/drawable-xxxhdpi/ic_more_vert_24dp.png | Bin 0 -> 305 bytes .../ic_not_interested_googblue_24dp.png | Bin 0 -> 1458 bytes .../app/res/drawable-xxxhdpi/ic_not_spam.png | Bin 0 -> 1752 bytes .../app/res/drawable-xxxhdpi/ic_pause_24dp.png | Bin 0 -> 94 bytes .../app/res/drawable-xxxhdpi/ic_people_24dp.png | Bin 0 -> 636 bytes .../app/res/drawable-xxxhdpi/ic_phone_24dp.png | Bin 0 -> 837 bytes .../res/drawable-xxxhdpi/ic_play_arrow_24dp.png | Bin 0 -> 343 bytes .../app/res/drawable-xxxhdpi/ic_results_phone.png | Bin 0 -> 2281 bytes .../app/res/drawable-xxxhdpi/ic_schedule_24dp.png | Bin 0 -> 1478 bytes .../res/drawable-xxxhdpi/ic_share_white_24dp.png | Bin 0 -> 938 bytes .../dialer/app/res/drawable-xxxhdpi/ic_unblock.png | Bin 0 -> 1389 bytes .../app/res/drawable-xxxhdpi/ic_voicemail_24dp.png | Bin 0 -> 971 bytes .../res/drawable-xxxhdpi/ic_volume_down_24dp.png | Bin 0 -> 356 bytes .../app/res/drawable-xxxhdpi/ic_volume_up_24dp.png | Bin 0 -> 878 bytes .../app/res/drawable/background_dial_holo_dark.xml | 22 + .../app/res/drawable/floating_action_button.xml | 25 + .../res/drawable/ic_call_detail_content_copy.xml | 20 + .../app/res/drawable/ic_call_detail_edit.xml | 20 + .../app/res/drawable/ic_call_detail_report.xml | 20 + .../app/res/drawable/ic_call_detail_unblock.xml | 20 + .../android/dialer/app/res/drawable/ic_pause.xml | 31 + .../dialer/app/res/drawable/ic_play_arrow.xml | 32 + .../dialer/app/res/drawable/ic_search_phone.xml | 20 + .../app/res/drawable/ic_speakerphone_off.xml | 20 + .../dialer/app/res/drawable/ic_speakerphone_on.xml | 20 + .../app/res/drawable/ic_voicemail_seek_handle.xml | 20 + .../drawable/ic_voicemail_seek_handle_disabled.xml | 20 + .../dialer/app/res/drawable/oval_ripple.xml | 26 + .../dialer/app/res/drawable/overflow_menu.xml | 20 + .../dialer/app/res/drawable/rounded_corner.xml | 22 + .../dialer/app/res/drawable/seekbar_drawable.xml | 63 + .../drawable/selectable_primary_flat_button.xml | 31 + .../dialer/app/res/drawable/shadow_fade_left.xml | 24 + .../dialer/app/res/drawable/shadow_fade_up.xml | 24 + .../app/res/layout-land/dialpad_fragment.xml | 90 ++ .../empty_content_view_dialpad_search.xml | 71 + .../account_filter_header_for_phone_favorite.xml | 47 + .../app/res/layout/all_contacts_activity.xml | 26 + .../app/res/layout/all_contacts_fragment.xml | 54 + .../app/res/layout/blocked_number_footer.xml | 38 + .../app/res/layout/blocked_number_fragment.xml | 30 + .../app/res/layout/blocked_number_header.xml | 220 +++ .../dialer/app/res/layout/blocked_number_item.xml | 72 + .../app/res/layout/blocked_numbers_activity.xml | 22 + .../android/dialer/app/res/layout/call_detail.xml | 32 + .../dialer/app/res/layout/call_detail_footer.xml | 52 + .../dialer/app/res/layout/call_detail_header.xml | 89 ++ .../app/res/layout/call_detail_history_item.xml | 56 + .../dialer/app/res/layout/call_log_alert_item.xml | 22 + .../dialer/app/res/layout/call_log_fragment.xml | 48 + .../dialer/app/res/layout/call_log_list_item.xml | 176 ++ .../app/res/layout/call_log_list_item_actions.xml | 230 +++ .../app/res/layout/dialpad_chooser_list_item.xml | 38 + .../dialer/app/res/layout/dialpad_fragment.xml | 78 + .../dialer/app/res/layout/dialtacts_activity.xml | 73 + .../dialer/app/res/layout/empty_content_view.xml | 54 + .../layout/empty_content_view_dialpad_search.xml | 56 + .../dialer/app/res/layout/keyguard_preview.xml | 30 + .../dialer/app/res/layout/lists_fragment.xml | 98 ++ .../app/res/layout/phone_favorite_tile_view.xml | 128 ++ .../dialer/app/res/layout/search_edittext.xml | 71 + .../dialer/app/res/layout/speed_dial_fragment.xml | 51 + .../res/layout/view_numbers_to_import_fragment.xml | 58 + .../app/res/layout/voicemail_playback_layout.xml | 115 ++ .../dialer/app/res/menu/dialpad_options.xml | 30 + .../dialer/app/res/menu/dialtacts_options.xml | 28 + .../app/res/mipmap-hdpi/ic_launcher_phone.png | Bin 0 -> 2780 bytes .../app/res/mipmap-mdpi/ic_launcher_phone.png | Bin 0 -> 1778 bytes .../app/res/mipmap-xhdpi/ic_launcher_phone.png | Bin 0 -> 3939 bytes .../app/res/mipmap-xxhdpi/ic_launcher_phone.png | Bin 0 -> 6251 bytes .../app/res/mipmap-xxxhdpi/ic_launcher_phone.png | Bin 0 -> 8793 bytes .../dialer/app/res/values/animation_constants.xml | 30 + java/com/android/dialer/app/res/values/attrs.xml | 21 + java/com/android/dialer/app/res/values/colors.xml | 115 ++ java/com/android/dialer/app/res/values/dimens.xml | 148 ++ .../app/res/values/donottranslate_config.xml | 37 + java/com/android/dialer/app/res/values/ids.xml | 28 + java/com/android/dialer/app/res/values/strings.xml | 960 +++++++++++ java/com/android/dialer/app/res/values/styles.xml | 279 ++++ .../app/res/xml/display_options_settings.xml | 31 + java/com/android/dialer/app/res/xml/file_paths.xml | 24 + java/com/android/dialer/app/res/xml/searchable.xml | 22 + .../android/dialer/app/res/xml/sound_settings.xml | 46 + .../app/settings/AppCompatPreferenceActivity.java | 155 ++ .../app/settings/DefaultRingtonePreference.java | 64 + .../app/settings/DialerSettingsActivity.java | 187 +++ .../settings/DisplayOptionsSettingsFragment.java | 30 + .../dialer/app/settings/SoundSettingsFragment.java | 242 +++ .../app/voicemail/VoicemailAudioManager.java | 252 +++ .../app/voicemail/VoicemailErrorManager.java | 129 ++ .../app/voicemail/VoicemailPlaybackLayout.java | 449 ++++++ .../app/voicemail/VoicemailPlaybackPresenter.java | 1050 ++++++++++++ .../dialer/app/voicemail/WiredHeadsetManager.java | 88 + .../dialer/app/voicemail/error/AndroidManifest.xml | 5 + .../error/OmtpVoicemailMessageCreator.java | 177 ++ .../app/voicemail/error/VoicemailErrorAlert.java | 165 ++ .../app/voicemail/error/VoicemailErrorMessage.java | 178 +++ .../error/VoicemailErrorMessageCreator.java | 45 + .../app/voicemail/error/VoicemailStatus.java | 260 +++ .../error/VoicemailStatusCorruptionHandler.java | 114 ++ .../app/voicemail/error/VoicemailStatusReader.java | 25 + .../app/voicemail/error/VoicemailTosMessage.java | 25 + .../error/Vvm3VoicemailMessageCreator.java | 428 +++++ .../res/layout/voicemai_error_message_fragment.xml | 114 ++ .../error/res/layout/voicemail_tos_fragment.xml | 72 + .../app/voicemail/error/res/values/dimens.xml | 12 + .../app/voicemail/error/res/values/strings.xml | 176 ++ .../app/voicemail/error/res/values/styles.xml | 26 + .../dialer/app/widget/ActionBarController.java | 247 +++ .../app/widget/DialpadSearchEmptyContentView.java | 43 + .../dialer/app/widget/EmptyContentView.java | 121 ++ .../dialer/app/widget/SearchEditTextLayout.java | 324 ++++ java/com/android/dialer/backup/AndroidManifest.xml | 27 + .../android/dialer/backup/DialerBackupAgent.java | 276 ++++ .../android/dialer/backup/DialerBackupUtils.java | 320 ++++ .../android/dialer/backup/proto/VoicemailInfo.java | 377 +++++ .../android/dialer/blocking/AndroidManifest.xml | 13 + .../dialer/blocking/BlockNumberDialogFragment.java | 328 ++++ .../dialer/blocking/BlockReportSpamDialogs.java | 305 ++++ .../blocking/BlockedNumbersAutoMigrator.java | 110 ++ .../dialer/blocking/BlockedNumbersMigrator.java | 159 ++ .../blocking/FilteredNumberAsyncQueryHandler.java | 428 +++++ .../dialer/blocking/FilteredNumberCompat.java | 320 ++++ .../dialer/blocking/FilteredNumberProvider.java | 176 ++ .../dialer/blocking/FilteredNumbersUtil.java | 380 +++++ .../MigrateBlockedNumbersDialogFragment.java | 113 ++ .../blocking/res/drawable-hdpi/ic_block_24dp.png | Bin 0 -> 478 bytes .../blocking/res/drawable-hdpi/ic_report_24dp.png | Bin 0 -> 240 bytes .../res/drawable-hdpi/ic_report_white_36dp.png | Bin 0 -> 312 bytes .../blocking/res/drawable-mdpi/ic_block_24dp.png | Bin 0 -> 335 bytes .../blocking/res/drawable-mdpi/ic_report_24dp.png | Bin 0 -> 174 bytes .../res/drawable-mdpi/ic_report_white_36dp.png | Bin 0 -> 240 bytes .../blocking/res/drawable-xhdpi/ic_block_24dp.png | Bin 0 -> 665 bytes .../blocking/res/drawable-xhdpi/ic_report_24dp.png | Bin 0 -> 272 bytes .../res/drawable-xhdpi/ic_report_white_36dp.png | Bin 0 -> 340 bytes .../blocking/res/drawable-xxhdpi/ic_block_24dp.png | Bin 0 -> 973 bytes .../res/drawable-xxhdpi/ic_report_24dp.png | Bin 0 -> 340 bytes .../res/drawable-xxhdpi/ic_report_white_36dp.png | Bin 0 -> 522 bytes .../res/drawable-xxxhdpi/ic_block_24dp.png | Bin 0 -> 1295 bytes .../res/drawable-xxxhdpi/ic_report_24dp.png | Bin 0 -> 450 bytes .../res/drawable-xxxhdpi/ic_report_white_36dp.png | Bin 0 -> 649 bytes .../blocking/res/drawable/blocked_contact.xml | 36 + .../res/layout/block_report_spam_dialog.xml | 36 + .../android/dialer/blocking/res/values/colors.xml | 24 + .../android/dialer/blocking/res/values/dimens.xml | 18 + .../android/dialer/blocking/res/values/strings.xml | 122 ++ java/com/android/dialer/buildtype/BuildType.java | 62 + .../dialer/buildtype/BuildTypeAccessor.java | 31 + .../buildtype/dogfood/BuildTypeAccessorImpl.java | 30 + .../dialer/callcomposer/AndroidManifest.xml | 28 + .../dialer/callcomposer/CallComposerActivity.java | 728 +++++++++ .../dialer/callcomposer/CallComposerFragment.java | 125 ++ .../callcomposer/CallComposerPagerAdapter.java | 57 + .../callcomposer/CameraComposerFragment.java | 378 +++++ .../callcomposer/GalleryComposerFragment.java | 256 +++ .../dialer/callcomposer/GalleryCursorLoader.java | 54 + .../dialer/callcomposer/GalleryGridAdapter.java | 118 ++ .../dialer/callcomposer/GalleryGridItemData.java | 91 ++ .../dialer/callcomposer/GalleryGridItemView.java | 126 ++ .../callcomposer/MessageComposerFragment.java | 143 ++ .../dialer/callcomposer/camera/AndroidManifest.xml | 16 + .../dialer/callcomposer/camera/CameraManager.java | 822 ++++++++++ .../dialer/callcomposer/camera/CameraPreview.java | 177 ++ .../callcomposer/camera/HardwareCameraPreview.java | 125 ++ .../callcomposer/camera/ImagePersistTask.java | 143 ++ .../callcomposer/camera/SoftwareCameraPreview.java | 120 ++ .../camera/camerafocus/AndroidManifest.xml | 16 + .../camera/camerafocus/FocusIndicator.java | 28 + .../camera/camerafocus/FocusOverlayManager.java | 482 ++++++ .../camera/camerafocus/OverlayRenderer.java | 97 ++ .../callcomposer/camera/camerafocus/PieItem.java | 179 +++ .../camera/camerafocus/PieRenderer.java | 816 ++++++++++ .../camera/camerafocus/RenderOverlay.java | 153 ++ .../camera/camerafocus/res/values/dimens.xml | 26 + .../camera/exif/CountedDataInputStream.java | 129 ++ .../dialer/callcomposer/camera/exif/ExifData.java | 89 ++ .../callcomposer/camera/exif/ExifInterface.java | 374 +++++ .../camera/exif/ExifInvalidFormatException.java | 24 + .../callcomposer/camera/exif/ExifParser.java | 846 ++++++++++ .../callcomposer/camera/exif/ExifReader.java | 81 + .../dialer/callcomposer/camera/exif/ExifTag.java | 619 +++++++ .../dialer/callcomposer/camera/exif/IfdData.java | 126 ++ .../dialer/callcomposer/camera/exif/IfdId.java | 28 + .../callcomposer/camera/exif/JpegHeader.java | 38 + .../dialer/callcomposer/camera/exif/Rational.java | 70 + .../callcomposer/cameraui/AndroidManifest.xml | 16 + .../cameraui/CameraMediaChooserView.java | 107 ++ .../cameraui/res/drawable-hdpi/ic_capture.png | Bin 0 -> 2690 bytes .../cameraui/res/drawable-mdpi/ic_capture.png | Bin 0 -> 1851 bytes .../cameraui/res/drawable-xhdpi/ic_capture.png | Bin 0 -> 3636 bytes .../cameraui/res/drawable-xxhdpi/ic_capture.png | Bin 0 -> 5449 bytes .../cameraui/res/drawable-xxxhdpi/ic_capture.png | Bin 0 -> 7354 bytes .../res/drawable/transparent_button_background.xml | 26 + .../cameraui/res/layout/camera_view.xml | 121 ++ .../callcomposer/cameraui/res/values/colors.xml | 4 + .../callcomposer/cameraui/res/values/dimens.xml | 22 + .../callcomposer/cameraui/res/values/strings.xml | 17 + .../callcomposer/nano/CallComposerContact.java | 220 +++ .../res/drawable/call_composer_contact_border.xml | 30 + .../res/drawable/gallery_background.xml | 22 + .../drawable/gallery_grid_checkbox_background.xml | 22 + .../drawable/gallery_grid_item_view_background.xml | 22 + .../drawable/gallery_item_selected_drawable.xml | 37 + .../res/layout/call_composer_activity.xml | 147 ++ .../res/layout/fragment_camera_composer.xml | 33 + .../res/layout/fragment_gallery_composer.xml | 38 + .../res/layout/fragment_message_composer.xml | 79 + .../res/layout/gallery_grid_item_view.xml | 57 + .../callcomposer/res/layout/permission_view.xml | 52 + .../dialer/callcomposer/res/values/colors.xml | 24 + .../dialer/callcomposer/res/values/dimens.xml | 63 + .../dialer/callcomposer/res/values/strings.xml | 42 + .../dialer/callcomposer/res/values/styles.xml | 50 + .../callcomposer/util/CopyAndResizeImageTask.java | 124 ++ .../dialer/callintent/CallIntentBuilder.java | 108 ++ .../dialer/callintent/CallIntentParser.java | 54 + java/com/android/dialer/callintent/Constants.java | 31 + .../dialer/callintent/nano/CallInitiationType.java | 101 ++ .../callintent/nano/CallSpecificAppData.java | 143 ++ java/com/android/dialer/common/AndroidManifest.xml | 3 + java/com/android/dialer/common/Assert.java | 185 +++ .../android/dialer/common/AsyncTaskExecutor.java | 51 + .../android/dialer/common/AsyncTaskExecutors.java | 91 ++ ...Value_FallibleAsyncTask_FallibleTaskResult.java | 79 + java/com/android/dialer/common/ConfigProvider.java | 27 + .../dialer/common/ConfigProviderBindings.java | 68 + .../dialer/common/ConfigProviderFactory.java | 26 + java/com/android/dialer/common/DpUtil.java | 31 + .../android/dialer/common/FallibleAsyncTask.java | 94 ++ java/com/android/dialer/common/FragmentUtils.java | 98 ++ java/com/android/dialer/common/LogUtil.java | 214 +++ java/com/android/dialer/common/MathUtil.java | 57 + java/com/android/dialer/common/NetworkUtil.java | 192 +++ java/com/android/dialer/common/UiUtil.java | 41 + .../android/dialer/common/res/values/strings.xml | 5 + java/com/android/dialer/compat/ActivityCompat.java | 29 + .../android/dialer/compat/AppCompatConstants.java | 33 + java/com/android/dialer/compat/CompatUtils.java | 222 +++ .../dialer/compat/PathInterpolatorCompat.java | 120 ++ .../android/dialer/compat/SdkVersionOverride.java | 43 + java/com/android/dialer/constants/Constants.java | 47 + .../android/dialer/constants/ScheduledJobIds.java | 31 + .../dialer/constants/aospdialer/ConstantsImpl.java | 37 + .../dialer/database/CallLogQueryHandler.java | 369 +++++ java/com/android/dialer/database/Database.java | 49 + .../android/dialer/database/DatabaseBindings.java | 25 + .../dialer/database/DatabaseBindingsFactory.java | 26 + .../dialer/database/DatabaseBindingsStub.java | 35 + .../dialer/database/DialerDatabaseHelper.java | 1242 ++++++++++++++ .../dialer/database/FilteredNumberContract.java | 137 ++ .../dialer/database/VoicemailStatusQuery.java | 91 ++ java/com/android/dialer/debug/AndroidManifest.xml | 3 + .../dialer/debug/bindings/impl/DebugBindings.java | 32 + .../android/dialer/debug/impl/AndroidManifest.xml | 18 + .../android/dialer/debug/impl/DebugConnection.java | 55 + .../dialer/debug/impl/DebugConnectionService.java | 103 ++ .../android/dialer/dialpadview/AndroidManifest.xml | 3 + .../dialer/dialpadview/DialpadKeyButton.java | 231 +++ .../dialer/dialpadview/DialpadTextView.java | 71 + .../android/dialer/dialpadview/DialpadView.java | 464 ++++++ .../android/dialer/dialpadview/DigitsEditText.java | 57 + .../res/anim/dialpad_slide_in_bottom.xml | 19 + .../dialpadview/res/anim/dialpad_slide_in_left.xml | 22 + .../res/anim/dialpad_slide_in_right.xml | 20 + .../res/anim/dialpad_slide_out_bottom.xml | 19 + .../res/anim/dialpad_slide_out_left.xml | 22 + .../res/anim/dialpad_slide_out_right.xml | 20 + .../dialpadview/res/drawable-hdpi/dialer_fab.png | Bin 0 -> 3273 bytes .../dialpadview/res/drawable-hdpi/fab_green.png | Bin 0 -> 2798 bytes .../dialpadview/res/drawable-hdpi/fab_ic_call.png | Bin 0 -> 875 bytes .../res/drawable-hdpi/ic_close_black_24dp.png | Bin 0 -> 207 bytes .../res/drawable-hdpi/ic_dialpad_delete.png | Bin 0 -> 805 bytes .../res/drawable-hdpi/ic_dialpad_voicemail.png | Bin 0 -> 623 bytes .../res/drawable-hdpi/ic_overflow_menu.png | Bin 0 -> 503 bytes .../dialpadview/res/drawable-mdpi/dialer_fab.png | Bin 0 -> 1945 bytes .../dialpadview/res/drawable-mdpi/fab_green.png | Bin 0 -> 1845 bytes .../dialpadview/res/drawable-mdpi/fab_ic_call.png | Bin 0 -> 698 bytes .../res/drawable-mdpi/ic_close_black_24dp.png | Bin 0 -> 164 bytes .../res/drawable-mdpi/ic_dialpad_delete.png | Bin 0 -> 669 bytes .../res/drawable-mdpi/ic_dialpad_voicemail.png | Bin 0 -> 504 bytes .../res/drawable-mdpi/ic_overflow_menu.png | Bin 0 -> 424 bytes .../dialpadview/res/drawable-xhdpi/dialer_fab.png | Bin 0 -> 4872 bytes .../dialpadview/res/drawable-xhdpi/fab_green.png | Bin 0 -> 4092 bytes .../dialpadview/res/drawable-xhdpi/fab_ic_call.png | Bin 0 -> 1266 bytes .../res/drawable-xhdpi/ic_close_black_24dp.png | Bin 0 -> 235 bytes .../res/drawable-xhdpi/ic_dialpad_delete.png | Bin 0 -> 1110 bytes .../res/drawable-xhdpi/ic_dialpad_voicemail.png | Bin 0 -> 787 bytes .../res/drawable-xhdpi/ic_overflow_menu.png | Bin 0 -> 550 bytes .../dialpadview/res/drawable-xxhdpi/dialer_fab.png | Bin 0 -> 8621 bytes .../dialpadview/res/drawable-xxhdpi/fab_green.png | Bin 0 -> 7004 bytes .../res/drawable-xxhdpi/fab_ic_call.png | Bin 0 -> 2321 bytes .../res/drawable-xxhdpi/ic_close_black_24dp.png | Bin 0 -> 309 bytes .../res/drawable-xxhdpi/ic_dialpad_delete.png | Bin 0 -> 1745 bytes .../res/drawable-xxhdpi/ic_dialpad_voicemail.png | Bin 0 -> 1578 bytes .../res/drawable-xxhdpi/ic_overflow_menu.png | Bin 0 -> 1384 bytes .../res/drawable-xxxhdpi/dialer_fab.png | Bin 0 -> 12782 bytes .../dialpadview/res/drawable-xxxhdpi/fab_green.png | Bin 0 -> 9900 bytes .../res/drawable-xxxhdpi/fab_ic_call.png | Bin 0 -> 2921 bytes .../res/drawable-xxxhdpi/ic_close_black_24dp.png | Bin 0 -> 377 bytes .../res/drawable-xxxhdpi/ic_dialpad_delete.png | Bin 0 -> 2128 bytes .../res/drawable-xxxhdpi/ic_dialpad_voicemail.png | Bin 0 -> 1829 bytes .../res/drawable-xxxhdpi/ic_overflow_menu.png | Bin 0 -> 1785 bytes .../dialpadview/res/drawable/btn_dialpad_key.xml | 18 + .../dialpadview/res/drawable/dialpad_scrim.xml | 7 + .../dialpadview/res/layout-land/dialpad_key.xml | 44 + .../res/layout-land/dialpad_key_one.xml | 44 + .../res/layout-land/dialpad_key_pound.xml | 33 + .../res/layout-land/dialpad_key_star.xml | 33 + .../res/layout-land/dialpad_key_zero.xml | 44 + .../dialer/dialpadview/res/layout/dialpad.xml | 99 ++ .../dialer/dialpadview/res/layout/dialpad_key.xml | 35 + .../dialpadview/res/layout/dialpad_key_one.xml | 41 + .../dialpadview/res/layout/dialpad_key_pound.xml | 26 + .../dialpadview/res/layout/dialpad_key_star.xml | 26 + .../dialpadview/res/layout/dialpad_key_zero.xml | 37 + .../dialer/dialpadview/res/layout/dialpad_view.xml | 23 + .../res/layout/dialpad_view_unthemed.xml | 153 ++ .../dialer/dialpadview/res/values-land/dimens.xml | 27 + .../dialer/dialpadview/res/values-land/styles.xml | 37 + .../dialpadview/res/values/animation_constants.xml | 20 + .../dialer/dialpadview/res/values/attrs.xml | 39 + .../dialer/dialpadview/res/values/colors.xml | 27 + .../dialer/dialpadview/res/values/dimens.xml | 48 + .../dialer/dialpadview/res/values/strings.xml | 53 + .../dialer/dialpadview/res/values/styles.xml | 118 ++ java/com/android/dialer/disabled_lint_checks.txt | 1 + .../AutoValue_EnrichedCallCapabilities.java | 76 + .../AutoValue_OutgoingCallComposerData.java | 127 ++ .../enrichedcall/EnrichedCallCapabilities.java | 36 + .../dialer/enrichedcall/EnrichedCallManager.java | 225 +++ .../enrichedcall/EnrichedCallManagerStub.java | 84 + .../enrichedcall/OutgoingCallComposerData.java | 94 ++ java/com/android/dialer/enrichedcall/Session.java | 63 + .../enrichedcall/StubEnrichedCallModule.java | 32 + .../enrichedcall/extensions/StateExtension.java | 54 + .../android/dialer/inject/ApplicationModule.java | 39 + .../android/dialer/inject/DialerAppComponent.java | 29 + .../dialer/interactions/AndroidManifest.xml | 20 + .../dialer/interactions/ContactUpdateService.java | 48 + .../interactions/PhoneNumberInteraction.java | 557 +++++++ .../interactions/UndemoteOutgoingCallReceiver.java | 107 ++ .../res/layout/phone_disambig_item.xml | 43 + .../res/layout/set_primary_checkbox.xml | 32 + .../dialer/interactions/res/values/strings.xml | 29 + java/com/android/dialer/logging/Logger.java | 49 + .../android/dialer/logging/LoggingBindings.java | 59 + .../dialer/logging/LoggingBindingsFactory.java | 24 + .../dialer/logging/LoggingBindingsStub.java | 36 + .../dialer/logging/nano/ContactLookupResult.java | 91 ++ .../android/dialer/logging/nano/ContactSource.java | 90 ++ .../dialer/logging/nano/DialerImpression.java | 178 +++ .../dialer/logging/nano/InteractionEvent.java | 95 ++ .../dialer/logging/nano/ReportingLocation.java | 87 + .../android/dialer/logging/nano/ScreenEvent.java | 104 ++ .../multimedia/AutoValue_MultimediaData.java | 165 ++ .../android/dialer/multimedia/MultimediaData.java | 100 ++ .../android/dialer/p13n/inference/P13nRanking.java | 75 + .../dialer/p13n/inference/protocol/P13nRanker.java | 75 + .../p13n/inference/protocol/P13nRankerFactory.java | 26 + .../android/dialer/p13n/logging/P13nLogger.java | 35 + .../dialer/p13n/logging/P13nLoggerFactory.java | 29 + .../android/dialer/p13n/logging/P13nLogging.java | 60 + .../CachedNumberLookupService.java | 77 + .../dialer/phonenumbercache/CallLogQuery.java | 107 ++ .../dialer/phonenumbercache/ContactInfo.java | 165 ++ .../dialer/phonenumbercache/ContactInfoHelper.java | 586 +++++++ .../dialer/phonenumbercache/PhoneLookupUtil.java | 40 + .../dialer/phonenumbercache/PhoneNumberCache.java | 50 + .../phonenumbercache/PhoneNumberCacheBindings.java | 26 + .../PhoneNumberCacheBindingsFactory.java | 26 + .../PhoneNumberCacheBindingsStub.java | 29 + .../dialer/phonenumbercache/PhoneQuery.java | 96 ++ .../dialer/phonenumberutil/AndroidManifest.xml | 3 + .../dialer/phonenumberutil/PhoneNumberHelper.java | 276 ++++ .../dialer/phonenumberutil/res/values/strings.xml | 27 + .../android/dialer/proguard/UsedByReflection.java | 34 + java/com/android/dialer/protos/ProtoParsers.java | 167 ++ .../android/dialer/shortcuts/AndroidManifest.xml | 50 + .../dialer/shortcuts/AutoValue_DialerShortcut.java | 161 ++ .../dialer/shortcuts/CallContactActivity.java | 133 ++ .../android/dialer/shortcuts/DialerShortcut.java | 190 +++ .../android/dialer/shortcuts/DynamicShortcuts.java | 243 +++ java/com/android/dialer/shortcuts/IconFactory.java | 112 ++ .../dialer/shortcuts/PeriodicJobService.java | 118 ++ .../android/dialer/shortcuts/PinnedShortcuts.java | 159 ++ .../dialer/shortcuts/RefreshShortcutsTask.java | 71 + .../dialer/shortcuts/ShortcutInfoFactory.java | 100 ++ .../dialer/shortcuts/ShortcutRefresher.java | 86 + .../dialer/shortcuts/ShortcutUsageReporter.java | 132 ++ java/com/android/dialer/shortcuts/Shortcuts.java | 34 + .../dialer/shortcuts/ShortcutsJobScheduler.java | 48 + .../res/drawable/ic_shortcut_add_contact.xml | 39 + .../android/dialer/shortcuts/res/values/colors.xml | 20 + .../android/dialer/shortcuts/res/values/dimens.xml | 19 + .../dialer/shortcuts/res/values/strings.xml | 37 + .../android/dialer/shortcuts/res/values/themes.xml | 39 + .../android/dialer/shortcuts/res/xml/shortcuts.xml | 31 + java/com/android/dialer/simulator/Simulator.java | 27 + .../dialer/simulator/impl/AndroidManifest.xml | 18 + .../impl/AutoValue_SimulatorCallLog_CallEntry.java | 160 ++ .../impl/AutoValue_SimulatorContacts_Contact.java | 231 +++ .../AutoValue_SimulatorVoicemail_Voicemail.java | 184 +++ .../simulator/impl/SimulatorActionProvider.java | 88 + .../dialer/simulator/impl/SimulatorCallLog.java | 139 ++ .../dialer/simulator/impl/SimulatorConnection.java | 56 + .../simulator/impl/SimulatorConnectionService.java | 87 + .../dialer/simulator/impl/SimulatorContacts.java | 319 ++++ .../dialer/simulator/impl/SimulatorModule.java | 34 + .../dialer/simulator/impl/SimulatorVoiceCall.java | 47 + .../dialer/simulator/impl/SimulatorVoicemail.java | 154 ++ .../dialer/smartdial/LatinSmartDialMap.java | 784 +++++++++ .../com/android/dialer/smartdial/SmartDialMap.java | 60 + .../dialer/smartdial/SmartDialMatchPosition.java | 70 + .../dialer/smartdial/SmartDialNameMatcher.java | 434 +++++ .../android/dialer/smartdial/SmartDialPrefix.java | 605 +++++++ java/com/android/dialer/spam/Spam.java | 49 + java/com/android/dialer/spam/SpamBindings.java | 146 ++ .../android/dialer/spam/SpamBindingsFactory.java | 26 + java/com/android/dialer/spam/SpamBindingsStub.java | 92 ++ java/com/android/dialer/telecom/TelecomUtil.java | 212 +++ java/com/android/dialer/theme/AndroidManifest.xml | 3 + .../anim/front_back_switch_button_animation.xml | 14 + .../res/animator/activated_button_elevation.xml | 21 + .../dialer/theme/res/animator/button_elevation.xml | 21 + .../res/drawable/front_back_switch_button.xml | 75 + .../front_back_switch_button_animation.xml | 8 + .../com/android/dialer/theme/res/values/colors.xml | 64 + .../com/android/dialer/theme/res/values/dimens.xml | 28 + .../android/dialer/theme/res/values/strings.xml | 27 + .../com/android/dialer/theme/res/values/styles.xml | 56 + .../com/android/dialer/theme/res/values/themes.xml | 21 + java/com/android/dialer/util/AndroidManifest.xml | 3 + java/com/android/dialer/util/CallUtil.java | 135 ++ java/com/android/dialer/util/DialerUtils.java | 246 +++ .../com/android/dialer/util/DrawableConverter.java | 97 ++ java/com/android/dialer/util/ExpirableCache.java | 269 ++++ java/com/android/dialer/util/IntentUtil.java | 78 + java/com/android/dialer/util/MoreStrings.java | 64 + java/com/android/dialer/util/OrientationUtil.java | 30 + java/com/android/dialer/util/PermissionsUtil.java | 121 ++ java/com/android/dialer/util/SettingsUtil.java | 95 ++ .../com/android/dialer/util/TouchPointManager.java | 60 + .../dialer/util/TransactionSafeActivity.java | 64 + java/com/android/dialer/util/ViewUtil.java | 129 ++ .../com/android/dialer/util/res/values/strings.xml | 42 + .../dialer/voicemailstatus/AndroidManifest.xml | 3 + .../VisualVoicemailEnabledChecker.java | 111 ++ .../voicemailstatus/VoicemailStatusHelper.java | 96 ++ .../voicemailstatus/VoicemailStatusHelperImpl.java | 278 ++++ .../dialer/voicemailstatus/res/values/strings.xml | 41 + java/com/android/dialer/widget/AndroidManifest.xml | 3 + .../dialer/widget/ResizingTextEditText.java | 51 + .../dialer/widget/ResizingTextTextView.java | 51 + .../com/android/dialer/widget/res/values/attrs.xml | 23 + 698 files changed, 60924 insertions(+) create mode 100644 java/com/android/dialer/animation/AnimUtils.java create mode 100644 java/com/android/dialer/animation/AnimationListenerAdapter.java create mode 100644 java/com/android/dialer/app/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/Bindings.java create mode 100644 java/com/android/dialer/app/CallDetailActivity.java create mode 100644 java/com/android/dialer/app/DialerApplication.java create mode 100644 java/com/android/dialer/app/DialtactsActivity.java create mode 100644 java/com/android/dialer/app/FloatingActionButtonBehavior.java create mode 100644 java/com/android/dialer/app/PhoneCallDetails.java create mode 100644 java/com/android/dialer/app/SpecialCharSequenceMgr.java create mode 100644 java/com/android/dialer/app/alert/AlertManager.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindings.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindingsFactory.java create mode 100644 java/com/android/dialer/app/bindings/DialerBindingsStub.java create mode 100644 java/com/android/dialer/app/calllog/BlockReportSpamListener.java create mode 100644 java/com/android/dialer/app/calllog/CallDetailHistoryAdapter.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAdapter.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAlertManager.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAsync.java create mode 100644 java/com/android/dialer/app/calllog/CallLogAsyncTaskUtil.java create mode 100644 java/com/android/dialer/app/calllog/CallLogFragment.java create mode 100644 java/com/android/dialer/app/calllog/CallLogGroupBuilder.java create mode 100644 java/com/android/dialer/app/calllog/CallLogListItemHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java create mode 100644 java/com/android/dialer/app/calllog/CallLogModalAlertManager.java create mode 100644 java/com/android/dialer/app/calllog/CallLogNotificationsHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallLogNotificationsService.java create mode 100644 java/com/android/dialer/app/calllog/CallLogReceiver.java create mode 100644 java/com/android/dialer/app/calllog/CallTypeHelper.java create mode 100644 java/com/android/dialer/app/calllog/CallTypeIconsView.java create mode 100644 java/com/android/dialer/app/calllog/ClearCallLogDialog.java create mode 100644 java/com/android/dialer/app/calllog/DefaultVoicemailNotifier.java create mode 100644 java/com/android/dialer/app/calllog/GroupingListAdapter.java create mode 100644 java/com/android/dialer/app/calllog/IntentProvider.java create mode 100644 java/com/android/dialer/app/calllog/MissedCallNotificationReceiver.java create mode 100644 java/com/android/dialer/app/calllog/MissedCallNotifier.java create mode 100644 java/com/android/dialer/app/calllog/PhoneAccountUtils.java create mode 100644 java/com/android/dialer/app/calllog/PhoneCallDetailsHelper.java create mode 100644 java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java create mode 100644 java/com/android/dialer/app/calllog/PhoneNumberDisplayUtil.java create mode 100644 java/com/android/dialer/app/calllog/VisualVoicemailCallLogFragment.java create mode 100644 java/com/android/dialer/app/calllog/VoicemailQueryHandler.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCache.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java create mode 100644 java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactInfoCache.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactInfoRequest.java create mode 100644 java/com/android/dialer/app/contactinfo/ContactPhotoLoader.java create mode 100644 java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java create mode 100644 java/com/android/dialer/app/contactinfo/NumberWithCountryIso.java create mode 100644 java/com/android/dialer/app/dialpad/DialpadFragment.java create mode 100644 java/com/android/dialer/app/dialpad/PseudoEmergencyAnimator.java create mode 100644 java/com/android/dialer/app/dialpad/SmartDialCursorLoader.java create mode 100644 java/com/android/dialer/app/dialpad/UnicodeDialerKeyListener.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersFragment.java create mode 100644 java/com/android/dialer/app/filterednumber/BlockedNumbersSettingsActivity.java create mode 100644 java/com/android/dialer/app/filterednumber/NumbersAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/ViewNumbersToImportAdapter.java create mode 100644 java/com/android/dialer/app/filterednumber/ViewNumbersToImportFragment.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindings.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindingsFactory.java create mode 100644 java/com/android/dialer/app/legacybindings/DialerLegacyBindingsStub.java create mode 100644 java/com/android/dialer/app/list/AllContactsFragment.java create mode 100644 java/com/android/dialer/app/list/BlockedListSearchAdapter.java create mode 100644 java/com/android/dialer/app/list/BlockedListSearchFragment.java create mode 100644 java/com/android/dialer/app/list/ContentChangedFilter.java create mode 100644 java/com/android/dialer/app/list/DialerPhoneNumberListAdapter.java create mode 100644 java/com/android/dialer/app/list/DragDropController.java create mode 100644 java/com/android/dialer/app/list/ListsFragment.java create mode 100644 java/com/android/dialer/app/list/OnDragDropListener.java create mode 100644 java/com/android/dialer/app/list/OnListFragmentScrolledListener.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteListView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteSquareTileView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoriteTileView.java create mode 100644 java/com/android/dialer/app/list/PhoneFavoritesTileAdapter.java create mode 100644 java/com/android/dialer/app/list/RegularSearchFragment.java create mode 100644 java/com/android/dialer/app/list/RegularSearchListAdapter.java create mode 100644 java/com/android/dialer/app/list/RemoveView.java create mode 100644 java/com/android/dialer/app/list/SearchFragment.java create mode 100644 java/com/android/dialer/app/list/SmartDialNumberListAdapter.java create mode 100644 java/com/android/dialer/app/list/SmartDialSearchFragment.java create mode 100644 java/com/android/dialer/app/list/SpeedDialFragment.java create mode 100644 java/com/android/dialer/app/manifests/activities/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/res/color/settings_text_color_primary.xml create mode 100644 java/com/android/dialer/app/res/color/settings_text_color_secondary.xml create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-hdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-mdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-xhdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/empty_speed_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_add_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_current_call.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_dialer_fork_tt_keypad.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_menu_history_lt.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_remove.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_star.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_off_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dis.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_vm_sound_on_dk.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/search_shadow.9.png create mode 100644 java/com/android/dialer/app/res/drawable-xxhdpi/shadow_contact_photo.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/empty_call_log.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/empty_contacts.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/fab_ic_dial.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_archive_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_call_arrow.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_content_copy_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_delete_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_grade_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_handle.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_mic_grey600.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_more_vert_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_interested_googblue_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_not_spam.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_pause_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_people_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_phone_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_play_arrow_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_results_phone.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_schedule_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_share_white_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_unblock.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_voicemail_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_down_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable-xxxhdpi/ic_volume_up_24dp.png create mode 100644 java/com/android/dialer/app/res/drawable/background_dial_holo_dark.xml create mode 100644 java/com/android/dialer/app/res/drawable/floating_action_button.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_content_copy.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_edit.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_report.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_call_detail_unblock.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_pause.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_play_arrow.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_search_phone.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_speakerphone_off.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_speakerphone_on.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle.xml create mode 100644 java/com/android/dialer/app/res/drawable/ic_voicemail_seek_handle_disabled.xml create mode 100644 java/com/android/dialer/app/res/drawable/oval_ripple.xml create mode 100644 java/com/android/dialer/app/res/drawable/overflow_menu.xml create mode 100644 java/com/android/dialer/app/res/drawable/rounded_corner.xml create mode 100644 java/com/android/dialer/app/res/drawable/seekbar_drawable.xml create mode 100644 java/com/android/dialer/app/res/drawable/selectable_primary_flat_button.xml create mode 100644 java/com/android/dialer/app/res/drawable/shadow_fade_left.xml create mode 100644 java/com/android/dialer/app/res/drawable/shadow_fade_up.xml create mode 100644 java/com/android/dialer/app/res/layout-land/dialpad_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout-land/empty_content_view_dialpad_search.xml create mode 100644 java/com/android/dialer/app/res/layout/account_filter_header_for_phone_favorite.xml create mode 100644 java/com/android/dialer/app/res/layout/all_contacts_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/all_contacts_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_footer.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_header.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_number_item.xml create mode 100644 java/com/android/dialer/app/res/layout/blocked_numbers_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_footer.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_header.xml create mode 100644 java/com/android/dialer/app/res/layout/call_detail_history_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_alert_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_list_item.xml create mode 100644 java/com/android/dialer/app/res/layout/call_log_list_item_actions.xml create mode 100644 java/com/android/dialer/app/res/layout/dialpad_chooser_list_item.xml create mode 100644 java/com/android/dialer/app/res/layout/dialpad_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/dialtacts_activity.xml create mode 100644 java/com/android/dialer/app/res/layout/empty_content_view.xml create mode 100644 java/com/android/dialer/app/res/layout/empty_content_view_dialpad_search.xml create mode 100644 java/com/android/dialer/app/res/layout/keyguard_preview.xml create mode 100644 java/com/android/dialer/app/res/layout/lists_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/phone_favorite_tile_view.xml create mode 100644 java/com/android/dialer/app/res/layout/search_edittext.xml create mode 100644 java/com/android/dialer/app/res/layout/speed_dial_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/view_numbers_to_import_fragment.xml create mode 100644 java/com/android/dialer/app/res/layout/voicemail_playback_layout.xml create mode 100644 java/com/android/dialer/app/res/menu/dialpad_options.xml create mode 100644 java/com/android/dialer/app/res/menu/dialtacts_options.xml create mode 100644 java/com/android/dialer/app/res/mipmap-hdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-mdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xxhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/mipmap-xxxhdpi/ic_launcher_phone.png create mode 100644 java/com/android/dialer/app/res/values/animation_constants.xml create mode 100644 java/com/android/dialer/app/res/values/attrs.xml create mode 100644 java/com/android/dialer/app/res/values/colors.xml create mode 100644 java/com/android/dialer/app/res/values/dimens.xml create mode 100644 java/com/android/dialer/app/res/values/donottranslate_config.xml create mode 100644 java/com/android/dialer/app/res/values/ids.xml create mode 100644 java/com/android/dialer/app/res/values/strings.xml create mode 100644 java/com/android/dialer/app/res/values/styles.xml create mode 100644 java/com/android/dialer/app/res/xml/display_options_settings.xml create mode 100644 java/com/android/dialer/app/res/xml/file_paths.xml create mode 100644 java/com/android/dialer/app/res/xml/searchable.xml create mode 100644 java/com/android/dialer/app/res/xml/sound_settings.xml create mode 100644 java/com/android/dialer/app/settings/AppCompatPreferenceActivity.java create mode 100644 java/com/android/dialer/app/settings/DefaultRingtonePreference.java create mode 100644 java/com/android/dialer/app/settings/DialerSettingsActivity.java create mode 100644 java/com/android/dialer/app/settings/DisplayOptionsSettingsFragment.java create mode 100644 java/com/android/dialer/app/settings/SoundSettingsFragment.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailAudioManager.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailErrorManager.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailPlaybackLayout.java create mode 100644 java/com/android/dialer/app/voicemail/VoicemailPlaybackPresenter.java create mode 100644 java/com/android/dialer/app/voicemail/WiredHeadsetManager.java create mode 100644 java/com/android/dialer/app/voicemail/error/AndroidManifest.xml create mode 100644 java/com/android/dialer/app/voicemail/error/OmtpVoicemailMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorAlert.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorMessage.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailErrorMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatus.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatusCorruptionHandler.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailStatusReader.java create mode 100644 java/com/android/dialer/app/voicemail/error/VoicemailTosMessage.java create mode 100644 java/com/android/dialer/app/voicemail/error/Vvm3VoicemailMessageCreator.java create mode 100644 java/com/android/dialer/app/voicemail/error/res/layout/voicemai_error_message_fragment.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/layout/voicemail_tos_fragment.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/dimens.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/strings.xml create mode 100644 java/com/android/dialer/app/voicemail/error/res/values/styles.xml create mode 100644 java/com/android/dialer/app/widget/ActionBarController.java create mode 100644 java/com/android/dialer/app/widget/DialpadSearchEmptyContentView.java create mode 100644 java/com/android/dialer/app/widget/EmptyContentView.java create mode 100644 java/com/android/dialer/app/widget/SearchEditTextLayout.java create mode 100644 java/com/android/dialer/backup/AndroidManifest.xml create mode 100644 java/com/android/dialer/backup/DialerBackupAgent.java create mode 100644 java/com/android/dialer/backup/DialerBackupUtils.java create mode 100644 java/com/android/dialer/backup/proto/VoicemailInfo.java create mode 100644 java/com/android/dialer/blocking/AndroidManifest.xml create mode 100644 java/com/android/dialer/blocking/BlockNumberDialogFragment.java create mode 100644 java/com/android/dialer/blocking/BlockReportSpamDialogs.java create mode 100644 java/com/android/dialer/blocking/BlockedNumbersAutoMigrator.java create mode 100644 java/com/android/dialer/blocking/BlockedNumbersMigrator.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberAsyncQueryHandler.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberCompat.java create mode 100644 java/com/android/dialer/blocking/FilteredNumberProvider.java create mode 100644 java/com/android/dialer/blocking/FilteredNumbersUtil.java create mode 100644 java/com/android/dialer/blocking/MigrateBlockedNumbersDialogFragment.java create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-hdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-mdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_block_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_24dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable-xxxhdpi/ic_report_white_36dp.png create mode 100644 java/com/android/dialer/blocking/res/drawable/blocked_contact.xml create mode 100644 java/com/android/dialer/blocking/res/layout/block_report_spam_dialog.xml create mode 100644 java/com/android/dialer/blocking/res/values/colors.xml create mode 100644 java/com/android/dialer/blocking/res/values/dimens.xml create mode 100644 java/com/android/dialer/blocking/res/values/strings.xml create mode 100644 java/com/android/dialer/buildtype/BuildType.java create mode 100644 java/com/android/dialer/buildtype/BuildTypeAccessor.java create mode 100644 java/com/android/dialer/buildtype/dogfood/BuildTypeAccessorImpl.java create mode 100644 java/com/android/dialer/callcomposer/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/CallComposerActivity.java create mode 100644 java/com/android/dialer/callcomposer/CallComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/CallComposerPagerAdapter.java create mode 100644 java/com/android/dialer/callcomposer/CameraComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/GalleryComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/GalleryCursorLoader.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridAdapter.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridItemData.java create mode 100644 java/com/android/dialer/callcomposer/GalleryGridItemView.java create mode 100644 java/com/android/dialer/callcomposer/MessageComposerFragment.java create mode 100644 java/com/android/dialer/callcomposer/camera/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/camera/CameraManager.java create mode 100644 java/com/android/dialer/callcomposer/camera/CameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/HardwareCameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/ImagePersistTask.java create mode 100644 java/com/android/dialer/callcomposer/camera/SoftwareCameraPreview.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/FocusIndicator.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/FocusOverlayManager.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/OverlayRenderer.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/PieItem.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/PieRenderer.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/RenderOverlay.java create mode 100644 java/com/android/dialer/callcomposer/camera/camerafocus/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/camera/exif/CountedDataInputStream.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifData.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifInterface.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifInvalidFormatException.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifParser.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifReader.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/ExifTag.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/IfdData.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/IfdId.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/JpegHeader.java create mode 100644 java/com/android/dialer/callcomposer/camera/exif/Rational.java create mode 100644 java/com/android/dialer/callcomposer/cameraui/AndroidManifest.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/CameraMediaChooserView.java create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-hdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-mdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xxhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable-xxxhdpi/ic_capture.png create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/drawable/transparent_button_background.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/layout/camera_view.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/colors.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/cameraui/res/values/strings.xml create mode 100644 java/com/android/dialer/callcomposer/nano/CallComposerContact.java create mode 100644 java/com/android/dialer/callcomposer/res/drawable/call_composer_contact_border.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_grid_checkbox_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_grid_item_view_background.xml create mode 100644 java/com/android/dialer/callcomposer/res/drawable/gallery_item_selected_drawable.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/call_composer_activity.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_camera_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_gallery_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/fragment_message_composer.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/gallery_grid_item_view.xml create mode 100644 java/com/android/dialer/callcomposer/res/layout/permission_view.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/colors.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/dimens.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/strings.xml create mode 100644 java/com/android/dialer/callcomposer/res/values/styles.xml create mode 100644 java/com/android/dialer/callcomposer/util/CopyAndResizeImageTask.java create mode 100644 java/com/android/dialer/callintent/CallIntentBuilder.java create mode 100644 java/com/android/dialer/callintent/CallIntentParser.java create mode 100644 java/com/android/dialer/callintent/Constants.java create mode 100644 java/com/android/dialer/callintent/nano/CallInitiationType.java create mode 100644 java/com/android/dialer/callintent/nano/CallSpecificAppData.java create mode 100644 java/com/android/dialer/common/AndroidManifest.xml create mode 100644 java/com/android/dialer/common/Assert.java create mode 100644 java/com/android/dialer/common/AsyncTaskExecutor.java create mode 100644 java/com/android/dialer/common/AsyncTaskExecutors.java create mode 100644 java/com/android/dialer/common/AutoValue_FallibleAsyncTask_FallibleTaskResult.java create mode 100644 java/com/android/dialer/common/ConfigProvider.java create mode 100644 java/com/android/dialer/common/ConfigProviderBindings.java create mode 100644 java/com/android/dialer/common/ConfigProviderFactory.java create mode 100644 java/com/android/dialer/common/DpUtil.java create mode 100644 java/com/android/dialer/common/FallibleAsyncTask.java create mode 100644 java/com/android/dialer/common/FragmentUtils.java create mode 100644 java/com/android/dialer/common/LogUtil.java create mode 100644 java/com/android/dialer/common/MathUtil.java create mode 100644 java/com/android/dialer/common/NetworkUtil.java create mode 100644 java/com/android/dialer/common/UiUtil.java create mode 100644 java/com/android/dialer/common/res/values/strings.xml create mode 100644 java/com/android/dialer/compat/ActivityCompat.java create mode 100644 java/com/android/dialer/compat/AppCompatConstants.java create mode 100644 java/com/android/dialer/compat/CompatUtils.java create mode 100644 java/com/android/dialer/compat/PathInterpolatorCompat.java create mode 100644 java/com/android/dialer/compat/SdkVersionOverride.java create mode 100644 java/com/android/dialer/constants/Constants.java create mode 100644 java/com/android/dialer/constants/ScheduledJobIds.java create mode 100644 java/com/android/dialer/constants/aospdialer/ConstantsImpl.java create mode 100644 java/com/android/dialer/database/CallLogQueryHandler.java create mode 100644 java/com/android/dialer/database/Database.java create mode 100644 java/com/android/dialer/database/DatabaseBindings.java create mode 100644 java/com/android/dialer/database/DatabaseBindingsFactory.java create mode 100644 java/com/android/dialer/database/DatabaseBindingsStub.java create mode 100644 java/com/android/dialer/database/DialerDatabaseHelper.java create mode 100644 java/com/android/dialer/database/FilteredNumberContract.java create mode 100644 java/com/android/dialer/database/VoicemailStatusQuery.java create mode 100644 java/com/android/dialer/debug/AndroidManifest.xml create mode 100644 java/com/android/dialer/debug/bindings/impl/DebugBindings.java create mode 100644 java/com/android/dialer/debug/impl/AndroidManifest.xml create mode 100644 java/com/android/dialer/debug/impl/DebugConnection.java create mode 100644 java/com/android/dialer/debug/impl/DebugConnectionService.java create mode 100644 java/com/android/dialer/dialpadview/AndroidManifest.xml create mode 100644 java/com/android/dialer/dialpadview/DialpadKeyButton.java create mode 100644 java/com/android/dialer/dialpadview/DialpadTextView.java create mode 100644 java/com/android/dialer/dialpadview/DialpadView.java create mode 100644 java/com/android/dialer/dialpadview/DigitsEditText.java create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_bottom.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_left.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_in_right.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_bottom.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_left.xml create mode 100644 java/com/android/dialer/dialpadview/res/anim/dialpad_slide_out_right.xml create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-hdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-mdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/dialer_fab.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_green.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/fab_ic_call.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_close_black_24dp.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_delete.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_dialpad_voicemail.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable-xxxhdpi/ic_overflow_menu.png create mode 100644 java/com/android/dialer/dialpadview/res/drawable/btn_dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/drawable/dialpad_scrim.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_one.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_pound.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_star.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout-land/dialpad_key_zero.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_one.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_pound.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_star.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_key_zero.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_view.xml create mode 100644 java/com/android/dialer/dialpadview/res/layout/dialpad_view_unthemed.xml create mode 100644 java/com/android/dialer/dialpadview/res/values-land/dimens.xml create mode 100644 java/com/android/dialer/dialpadview/res/values-land/styles.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/animation_constants.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/attrs.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/colors.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/dimens.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/strings.xml create mode 100644 java/com/android/dialer/dialpadview/res/values/styles.xml create mode 100644 java/com/android/dialer/disabled_lint_checks.txt create mode 100644 java/com/android/dialer/enrichedcall/AutoValue_EnrichedCallCapabilities.java create mode 100644 java/com/android/dialer/enrichedcall/AutoValue_OutgoingCallComposerData.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallCapabilities.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallManager.java create mode 100644 java/com/android/dialer/enrichedcall/EnrichedCallManagerStub.java create mode 100644 java/com/android/dialer/enrichedcall/OutgoingCallComposerData.java create mode 100644 java/com/android/dialer/enrichedcall/Session.java create mode 100644 java/com/android/dialer/enrichedcall/StubEnrichedCallModule.java create mode 100644 java/com/android/dialer/enrichedcall/extensions/StateExtension.java create mode 100644 java/com/android/dialer/inject/ApplicationModule.java create mode 100644 java/com/android/dialer/inject/DialerAppComponent.java create mode 100644 java/com/android/dialer/interactions/AndroidManifest.xml create mode 100644 java/com/android/dialer/interactions/ContactUpdateService.java create mode 100644 java/com/android/dialer/interactions/PhoneNumberInteraction.java create mode 100644 java/com/android/dialer/interactions/UndemoteOutgoingCallReceiver.java create mode 100644 java/com/android/dialer/interactions/res/layout/phone_disambig_item.xml create mode 100644 java/com/android/dialer/interactions/res/layout/set_primary_checkbox.xml create mode 100644 java/com/android/dialer/interactions/res/values/strings.xml create mode 100644 java/com/android/dialer/logging/Logger.java create mode 100644 java/com/android/dialer/logging/LoggingBindings.java create mode 100644 java/com/android/dialer/logging/LoggingBindingsFactory.java create mode 100644 java/com/android/dialer/logging/LoggingBindingsStub.java create mode 100644 java/com/android/dialer/logging/nano/ContactLookupResult.java create mode 100644 java/com/android/dialer/logging/nano/ContactSource.java create mode 100644 java/com/android/dialer/logging/nano/DialerImpression.java create mode 100644 java/com/android/dialer/logging/nano/InteractionEvent.java create mode 100644 java/com/android/dialer/logging/nano/ReportingLocation.java create mode 100644 java/com/android/dialer/logging/nano/ScreenEvent.java create mode 100644 java/com/android/dialer/multimedia/AutoValue_MultimediaData.java create mode 100644 java/com/android/dialer/multimedia/MultimediaData.java create mode 100644 java/com/android/dialer/p13n/inference/P13nRanking.java create mode 100644 java/com/android/dialer/p13n/inference/protocol/P13nRanker.java create mode 100644 java/com/android/dialer/p13n/inference/protocol/P13nRankerFactory.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLogger.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLoggerFactory.java create mode 100644 java/com/android/dialer/p13n/logging/P13nLogging.java create mode 100644 java/com/android/dialer/phonenumbercache/CachedNumberLookupService.java create mode 100644 java/com/android/dialer/phonenumbercache/CallLogQuery.java create mode 100644 java/com/android/dialer/phonenumbercache/ContactInfo.java create mode 100644 java/com/android/dialer/phonenumbercache/ContactInfoHelper.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneLookupUtil.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCache.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindings.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsFactory.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneNumberCacheBindingsStub.java create mode 100644 java/com/android/dialer/phonenumbercache/PhoneQuery.java create mode 100644 java/com/android/dialer/phonenumberutil/AndroidManifest.xml create mode 100644 java/com/android/dialer/phonenumberutil/PhoneNumberHelper.java create mode 100644 java/com/android/dialer/phonenumberutil/res/values/strings.xml create mode 100644 java/com/android/dialer/proguard/UsedByReflection.java create mode 100644 java/com/android/dialer/protos/ProtoParsers.java create mode 100644 java/com/android/dialer/shortcuts/AndroidManifest.xml create mode 100644 java/com/android/dialer/shortcuts/AutoValue_DialerShortcut.java create mode 100644 java/com/android/dialer/shortcuts/CallContactActivity.java create mode 100644 java/com/android/dialer/shortcuts/DialerShortcut.java create mode 100644 java/com/android/dialer/shortcuts/DynamicShortcuts.java create mode 100644 java/com/android/dialer/shortcuts/IconFactory.java create mode 100644 java/com/android/dialer/shortcuts/PeriodicJobService.java create mode 100644 java/com/android/dialer/shortcuts/PinnedShortcuts.java create mode 100644 java/com/android/dialer/shortcuts/RefreshShortcutsTask.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutInfoFactory.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutRefresher.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutUsageReporter.java create mode 100644 java/com/android/dialer/shortcuts/Shortcuts.java create mode 100644 java/com/android/dialer/shortcuts/ShortcutsJobScheduler.java create mode 100644 java/com/android/dialer/shortcuts/res/drawable/ic_shortcut_add_contact.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/colors.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/dimens.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/strings.xml create mode 100644 java/com/android/dialer/shortcuts/res/values/themes.xml create mode 100644 java/com/android/dialer/shortcuts/res/xml/shortcuts.xml create mode 100644 java/com/android/dialer/simulator/Simulator.java create mode 100644 java/com/android/dialer/simulator/impl/AndroidManifest.xml create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorCallLog_CallEntry.java create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorContacts_Contact.java create mode 100644 java/com/android/dialer/simulator/impl/AutoValue_SimulatorVoicemail_Voicemail.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorActionProvider.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorCallLog.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnection.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorConnectionService.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorContacts.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorModule.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVoiceCall.java create mode 100644 java/com/android/dialer/simulator/impl/SimulatorVoicemail.java create mode 100644 java/com/android/dialer/smartdial/LatinSmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialMap.java create mode 100644 java/com/android/dialer/smartdial/SmartDialMatchPosition.java create mode 100644 java/com/android/dialer/smartdial/SmartDialNameMatcher.java create mode 100644 java/com/android/dialer/smartdial/SmartDialPrefix.java create mode 100644 java/com/android/dialer/spam/Spam.java create mode 100644 java/com/android/dialer/spam/SpamBindings.java create mode 100644 java/com/android/dialer/spam/SpamBindingsFactory.java create mode 100644 java/com/android/dialer/spam/SpamBindingsStub.java create mode 100644 java/com/android/dialer/telecom/TelecomUtil.java create mode 100644 java/com/android/dialer/theme/AndroidManifest.xml create mode 100644 java/com/android/dialer/theme/res/anim/front_back_switch_button_animation.xml create mode 100644 java/com/android/dialer/theme/res/animator/activated_button_elevation.xml create mode 100644 java/com/android/dialer/theme/res/animator/button_elevation.xml create mode 100644 java/com/android/dialer/theme/res/drawable/front_back_switch_button.xml create mode 100644 java/com/android/dialer/theme/res/drawable/front_back_switch_button_animation.xml create mode 100644 java/com/android/dialer/theme/res/values/colors.xml create mode 100644 java/com/android/dialer/theme/res/values/dimens.xml create mode 100644 java/com/android/dialer/theme/res/values/strings.xml create mode 100644 java/com/android/dialer/theme/res/values/styles.xml create mode 100644 java/com/android/dialer/theme/res/values/themes.xml create mode 100644 java/com/android/dialer/util/AndroidManifest.xml create mode 100644 java/com/android/dialer/util/CallUtil.java create mode 100644 java/com/android/dialer/util/DialerUtils.java create mode 100644 java/com/android/dialer/util/DrawableConverter.java create mode 100644 java/com/android/dialer/util/ExpirableCache.java create mode 100644 java/com/android/dialer/util/IntentUtil.java create mode 100644 java/com/android/dialer/util/MoreStrings.java create mode 100644 java/com/android/dialer/util/OrientationUtil.java create mode 100644 java/com/android/dialer/util/PermissionsUtil.java create mode 100644 java/com/android/dialer/util/SettingsUtil.java create mode 100644 java/com/android/dialer/util/TouchPointManager.java create mode 100644 java/com/android/dialer/util/TransactionSafeActivity.java create mode 100644 java/com/android/dialer/util/ViewUtil.java create mode 100644 java/com/android/dialer/util/res/values/strings.xml create mode 100644 java/com/android/dialer/voicemailstatus/AndroidManifest.xml create mode 100644 java/com/android/dialer/voicemailstatus/VisualVoicemailEnabledChecker.java create mode 100644 java/com/android/dialer/voicemailstatus/VoicemailStatusHelper.java create mode 100644 java/com/android/dialer/voicemailstatus/VoicemailStatusHelperImpl.java create mode 100644 java/com/android/dialer/voicemailstatus/res/values/strings.xml create mode 100644 java/com/android/dialer/widget/AndroidManifest.xml create mode 100644 java/com/android/dialer/widget/ResizingTextEditText.java create mode 100644 java/com/android/dialer/widget/ResizingTextTextView.java create mode 100644 java/com/android/dialer/widget/res/values/attrs.xml (limited to 'java/com/android/dialer') diff --git a/java/com/android/dialer/animation/AnimUtils.java b/java/com/android/dialer/animation/AnimUtils.java new file mode 100644 index 000000000..9c9396e56 --- /dev/null +++ b/java/com/android/dialer/animation/AnimUtils.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.animation; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.ViewPropertyAnimator; +import android.view.animation.Interpolator; +import com.android.dialer.compat.PathInterpolatorCompat; + +public class AnimUtils { + + public static final int DEFAULT_DURATION = -1; + public static final int NO_DELAY = 0; + + public static final Interpolator EASE_IN = PathInterpolatorCompat.create(0.0f, 0.0f, 0.2f, 1.0f); + public static final Interpolator EASE_OUT = PathInterpolatorCompat.create(0.4f, 0.0f, 1.0f, 1.0f); + public static final Interpolator EASE_OUT_EASE_IN = + PathInterpolatorCompat.create(0.4f, 0, 0.2f, 1); + + public static void crossFadeViews(View fadeIn, View fadeOut, int duration) { + fadeIn(fadeIn, duration); + fadeOut(fadeOut, duration); + } + + public static void fadeOut(View fadeOut, int duration) { + fadeOut(fadeOut, duration, null); + } + + public static void fadeOut(final View fadeOut, int durationMs, final AnimationCallback callback) { + fadeOut.setAlpha(1); + final ViewPropertyAnimator animator = fadeOut.animate(); + animator.cancel(); + animator + .alpha(0) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + fadeOut.setVisibility(View.GONE); + if (callback != null) { + callback.onAnimationEnd(); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeOut.setVisibility(View.GONE); + fadeOut.setAlpha(0); + if (callback != null) { + callback.onAnimationCancel(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + public static void fadeIn(View fadeIn, int durationMs) { + fadeIn(fadeIn, durationMs, NO_DELAY, null); + } + + public static void fadeIn( + final View fadeIn, int durationMs, int delay, final AnimationCallback callback) { + fadeIn.setAlpha(0); + final ViewPropertyAnimator animator = fadeIn.animate(); + animator.cancel(); + + animator.setStartDelay(delay); + animator + .alpha(1) + .withLayer() + .setListener( + new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + fadeIn.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + fadeIn.setAlpha(1); + if (callback != null) { + callback.onAnimationCancel(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + if (callback != null) { + callback.onAnimationEnd(); + } + } + }); + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.start(); + } + + /** + * Scales in the view from scale of 0 to actual dimensions. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + * @param startDelayMs The delay to applying the scaling in milliseconds. + */ + public static void scaleIn(final View view, int durationMs, int startDelayMs) { + AnimatorListenerAdapter listener = + (new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + view.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setScaleX(1); + view.setScaleY(1); + } + }); + scaleInternal( + view, + 0 /* startScaleValue */, + 1 /* endScaleValue */, + durationMs, + startDelayMs, + listener, + EASE_IN); + } + + /** + * Scales out the view from actual dimensions to 0. + * + * @param view The view to scale. + * @param durationMs The duration of the scaling in milliseconds. + */ + public static void scaleOut(final View view, int durationMs) { + AnimatorListenerAdapter listener = + new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setVisibility(View.GONE); + } + + @Override + public void onAnimationCancel(Animator animation) { + view.setVisibility(View.GONE); + view.setScaleX(0); + view.setScaleY(0); + } + }; + + scaleInternal( + view, + 1 /* startScaleValue */, + 0 /* endScaleValue */, + durationMs, + NO_DELAY, + listener, + EASE_OUT); + } + + private static void scaleInternal( + final View view, + int startScaleValue, + int endScaleValue, + int durationMs, + int startDelay, + AnimatorListenerAdapter listener, + Interpolator interpolator) { + view.setScaleX(startScaleValue); + view.setScaleY(startScaleValue); + + final ViewPropertyAnimator animator = view.animate(); + animator.cancel(); + + animator + .setInterpolator(interpolator) + .scaleX(endScaleValue) + .scaleY(endScaleValue) + .setListener(listener) + .withLayer(); + + if (durationMs != DEFAULT_DURATION) { + animator.setDuration(durationMs); + } + animator.setStartDelay(startDelay); + + animator.start(); + } + + /** + * Animates a view to the new specified dimensions. + * + * @param view The view to change the dimensions of. + * @param newWidth The new width of the view. + * @param newHeight The new height of the view. + */ + public static void changeDimensions(final View view, final int newWidth, final int newHeight) { + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + + final int oldWidth = view.getWidth(); + final int oldHeight = view.getHeight(); + final int deltaWidth = newWidth - oldWidth; + final int deltaHeight = newHeight - oldHeight; + + animator.addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animator) { + Float value = (Float) animator.getAnimatedValue(); + + view.getLayoutParams().width = (int) (value * deltaWidth + oldWidth); + view.getLayoutParams().height = (int) (value * deltaHeight + oldHeight); + view.requestLayout(); + } + }); + animator.start(); + } + + public static class AnimationCallback { + + public void onAnimationEnd() {} + + public void onAnimationCancel() {} + } +} diff --git a/java/com/android/dialer/animation/AnimationListenerAdapter.java b/java/com/android/dialer/animation/AnimationListenerAdapter.java new file mode 100644 index 000000000..3f847f2b6 --- /dev/null +++ b/java/com/android/dialer/animation/AnimationListenerAdapter.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.animation; + +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; + +/** + * Provides empty implementations of the methods in {@link AnimationListener} for convenience + * reasons. + */ +public class AnimationListenerAdapter implements AnimationListener { + + /** {@inheritDoc} */ + @Override + public void onAnimationStart(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationEnd(Animation animation) {} + + /** {@inheritDoc} */ + @Override + public void onAnimationRepeat(Animation animation) {} +} diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml new file mode 100644 index 000000000..80f294acc --- /dev/null +++ b/java/com/android/dialer/app/AndroidManifest.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com/android/dialer/app/Bindings.java b/java/com/android/dialer/app/Bindings.java new file mode 100644 index 000000000..2beb40184 --- /dev/null +++ b/java/com/android/dialer/app/Bindings.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.content.Context; +import com.android.dialer.app.bindings.DialerBindings; +import com.android.dialer.app.bindings.DialerBindingsFactory; +import com.android.dialer.app.bindings.DialerBindingsStub; +import com.android.dialer.app.legacybindings.DialerLegacyBindings; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsFactory; +import com.android.dialer.app.legacybindings.DialerLegacyBindingsStub; +import java.util.Objects; + +/** Accessor for the in call UI bindings. */ +public class Bindings { + + private static DialerBindings instance; + private static DialerLegacyBindings legacyInstance; + + private Bindings() {} + + public static DialerBindings get(Context context) { + Objects.requireNonNull(context); + if (instance != null) { + return instance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerBindingsFactory) { + instance = ((DialerBindingsFactory) application).newDialerBindings(); + } + + if (instance == null) { + instance = new DialerBindingsStub(); + } + return instance; + } + + public static DialerLegacyBindings getLegacy(Context context) { + Objects.requireNonNull(context); + if (legacyInstance != null) { + return legacyInstance; + } + + Context application = context.getApplicationContext(); + if (application instanceof DialerLegacyBindingsFactory) { + legacyInstance = ((DialerLegacyBindingsFactory) application).newDialerLegacyBindings(); + } + + if (legacyInstance == null) { + legacyInstance = new DialerLegacyBindingsStub(); + } + return legacyInstance; + } + + public static void setForTesting(DialerBindings testInstance) { + instance = testInstance; + } + + public static void setLegacyBindingForTesting(DialerLegacyBindings testLegacyInstance) { + legacyInstance = testLegacyInstance; + } +} diff --git a/java/com/android/dialer/app/CallDetailActivity.java b/java/com/android/dialer/app/CallDetailActivity.java new file mode 100644 index 000000000..cda2b2e2c --- /dev/null +++ b/java/com/android/dialer/app/CallDetailActivity.java @@ -0,0 +1,480 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app; + +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.ContactsContract.CommonDataKinds.Phone; +import android.support.v7.app.AppCompatActivity; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.View; +import android.widget.ListView; +import android.widget.QuickContactBadge; +import android.widget.TextView; +import android.widget.Toast; +import com.android.contacts.common.ClipboardUtils; +import com.android.contacts.common.ContactPhotoManager; +import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; +import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.UriUtils; +import com.android.dialer.app.calllog.CallDetailHistoryAdapter; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil; +import com.android.dialer.app.calllog.CallLogAsyncTaskUtil.CallLogAsyncTaskListener; +import com.android.dialer.app.calllog.CallTypeHelper; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.common.Assert; +import com.android.dialer.common.AsyncTaskExecutor; +import com.android.dialer.common.AsyncTaskExecutors; +import com.android.dialer.compat.CompatUtils; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.logging.nano.ScreenEvent; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import com.android.dialer.proguard.UsedByReflection; +import com.android.dialer.spam.Spam; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.DialerUtils; +import com.android.dialer.util.TouchPointManager; + +/** + * Displays the details of a specific call log entry. + * + *

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

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

If both are available, the data on the intent takes precedence. + */ + private Uri[] getCallLogEntryUris() { + final Uri uri = getIntent().getData(); + if (uri != null) { + // If there is a data on the intent, it takes precedence over the extra. + return new Uri[] {uri}; + } + final long[] ids = getIntent().getLongArrayExtra(EXTRA_CALL_LOG_IDS); + final int numIds = ids == null ? 0 : ids.length; + final Uri[] uris = new Uri[numIds]; + for (int index = 0; index < numIds; ++index) { + uris[index] = + ContentUris.withAppendedId( + TelecomUtil.getCallLogUri(CallDetailActivity.this), ids[index]); + } + return uris; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuItem deleteMenuItem = + menu.add( + Menu.NONE, R.id.call_detail_delete_menu_item, Menu.NONE, R.string.call_details_delete); + deleteMenuItem.setIcon(R.drawable.ic_delete_24dp); + deleteMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + deleteMenuItem.setOnMenuItemClickListener(this); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.call_detail_delete_menu_item) { + Logger.get(mContext).logImpression(DialerImpression.Type.USER_DELETED_CALL_LOG_ITEM); + if (hasVoicemail()) { + CallLogAsyncTaskUtil.deleteVoicemail(this, mVoicemailUri, mCallLogAsyncTaskListener); + } else { + final StringBuilder callIds = new StringBuilder(); + for (Uri callUri : getCallLogEntryUris()) { + if (callIds.length() != 0) { + callIds.append(","); + } + callIds.append(ContentUris.parseId(callUri)); + } + CallLogAsyncTaskUtil.deleteCalls(this, callIds.toString(), mCallLogAsyncTaskListener); + } + } + return true; + } + + @Override + public void onClick(View view) { + int resId = view.getId(); + if (resId == R.id.call_detail_action_copy) { + ClipboardUtils.copyText(mContext, null, mNumber, true); + } else if (resId == R.id.call_detail_action_edit_before_call) { + Intent dialIntent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(getDialableNumber())); + DialerUtils.startActivityWithErrorToast(mContext, dialIntent); + } else { + Assert.fail("Unexpected onClick event from " + view); + } + } + + // Loads and displays the contact photo. + private void updateContactPhoto(boolean isSpam) { + if (mDetails == null) { + return; + } + + mQuickContactBadge.assignContactUri(mDetails.contactUri); + final String displayName = + TextUtils.isEmpty(mDetails.namePrimary) + ? mDetails.displayNumber + : mDetails.namePrimary.toString(); + mQuickContactBadge.setContentDescription( + mResources.getString(R.string.description_contact_details, displayName)); + + final boolean isVoicemailNumber = + PhoneNumberHelper.isVoicemailNumber(mContext, mDetails.accountHandle, mNumber); + if (isSpam) { + mQuickContactBadge.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); + return; + } + + final boolean isBusiness = mContactInfoHelper.isBusiness(mDetails.sourceType); + int contactType = ContactPhotoManager.TYPE_DEFAULT; + if (isVoicemailNumber) { + contactType = ContactPhotoManager.TYPE_VOICEMAIL; + } else if (isBusiness) { + contactType = ContactPhotoManager.TYPE_BUSINESS; + } + + final String lookupKey = + mDetails.contactUri == null ? null : UriUtils.getLookupKeyFromUri(mDetails.contactUri); + + final DefaultImageRequest request = + new DefaultImageRequest(displayName, lookupKey, contactType, true /* isCircular */); + + mContactPhotoManager.loadDirectoryPhoto( + mQuickContactBadge, + mDetails.photoUri, + false /* darkTheme */, + true /* isCircular */, + request); + } + + private void closeSystemDialogs() { + sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)); + } + + private String getDialableNumber() { + return mNumber + mPostDialDigits; + } + + public boolean hasVoicemail() { + return mVoicemailUri != null; + } + + private static boolean hasIncomingCalls(PhoneCallDetails[] details) { + for (int i = 0; i < details.length; i++) { + if (details[i].hasIncomingCalls()) { + return true; + } + } + return false; + } +} diff --git a/java/com/android/dialer/app/DialerApplication.java b/java/com/android/dialer/app/DialerApplication.java new file mode 100644 index 000000000..3b979212b --- /dev/null +++ b/java/com/android/dialer/app/DialerApplication.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app; + +import android.app.Application; +import android.os.Trace; +import android.preference.PreferenceManager; +import com.android.dialer.blocking.BlockedNumbersAutoMigrator; +import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; +import com.android.dialer.enrichedcall.EnrichedCallManager; +import com.android.dialer.inject.ApplicationModule; +import com.android.dialer.inject.DaggerDialerAppComponent; +import com.android.dialer.inject.DialerAppComponent; + +public class DialerApplication extends Application implements EnrichedCallManager.Factory { + + private static final String TAG = "DialerApplication"; + + private volatile DialerAppComponent component; + + @Override + public void onCreate() { + Trace.beginSection(TAG + " onCreate"); + super.onCreate(); + new BlockedNumbersAutoMigrator( + this, + PreferenceManager.getDefaultSharedPreferences(this), + new FilteredNumberAsyncQueryHandler(this)) + .autoMigrate(); + Trace.endSection(); + } + + @Override + public EnrichedCallManager getEnrichedCallManager() { + return component().enrichedCallManager(); + } + + protected DialerAppComponent buildApplicationComponent() { + return DaggerDialerAppComponent.builder() + .applicationModule(new ApplicationModule(this)) + .build(); + } + + /** + * Returns the application component. + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

It handles the following actions: + * + *

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

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

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

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

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

It must be a {@link Integer} + */ + public static final String EXTRA_MISSED_CALL_COUNT = "MISSED_CALL_COUNT"; + + public static final int UNKNOWN_MISSED_CALL_COUNT = -1; + private VoicemailQueryHandler mVoicemailQueryHandler; + + public CallLogNotificationsService() { + super("CallLogNotificationsService"); + } + + /** + * Updates notifications for any new voicemails. + * + * @param context a valid context. + * @param voicemailUri The uri pointing to the voicemail to update the notification for. If {@code + * null}, then notifications for all new voicemails will be updated. + */ + public static void updateVoicemailNotifications(Context context, Uri voicemailUri) { + if (!TelecomUtil.isDefaultDialer(context)) { + LogUtil.i( + "CallLogNotificationsService.updateVoicemailNotifications", + "not default dialer, ignoring voicemail notifications"); + return; + } + if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS); + // If voicemailUri is null, then notifications for all voicemails will be updated. + if (voicemailUri != null) { + serviceIntent.putExtra(CallLogNotificationsService.EXTRA_NEW_VOICEMAIL_URI, voicemailUri); + } + context.startService(serviceIntent); + } + } + + /** + * Updates notifications for any new missed calls. + * + * @param context A valid context. + * @param count The number of new missed calls. + * @param number The phone number of the newest missed call. + */ + public static void updateMissedCallNotifications(Context context, int count, String number) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS); + serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count); + serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number); + context.startService(serviceIntent); + } + + public static void markNewVoicemailsAsOld(Context context) { + Intent serviceIntent = new Intent(context, CallLogNotificationsService.class); + serviceIntent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); + context.startService(serviceIntent); + } + + public static boolean updateBadgeCount(Context context, int count) { + boolean success = ShortcutBadger.applyCount(context, count); + LogUtil.i( + "CallLogNotificationsService.updateBadgeCount", + "update badge count: %d success: %b", + count, + success); + return success; + } + + @Override + protected void onHandleIntent(Intent intent) { + if (intent == null) { + LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle null intent"); + return; + } + + if (!PermissionsUtil.hasPermission(this, android.Manifest.permission.READ_CALL_LOG)) { + return; + } + + String action = intent.getAction(); + switch (action) { + case ACTION_MARK_NEW_VOICEMAILS_AS_OLD: + if (mVoicemailQueryHandler == null) { + mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver()); + } + mVoicemailQueryHandler.markNewVoicemailsAsOld(); + break; + case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS: + Uri voicemailUri = intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI); + DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri); + break; + case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS: + int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT, UNKNOWN_MISSED_CALL_COUNT); + String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER); + MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number); + updateBadgeCount(this, count); + break; + case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD: + CallLogNotificationsHelper.removeMissedCallNotifications(this); + break; + case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION: + MissedCallNotifier.getInstance(this) + .callBackFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + break; + case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION: + MissedCallNotifier.getInstance(this) + .sendSmsFromMissedCall(intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER)); + break; + default: + LogUtil.d("CallLogNotificationsService.onHandleIntent", "could not handle: " + intent); + break; + } + } +} diff --git a/java/com/android/dialer/app/calllog/CallLogReceiver.java b/java/com/android/dialer/app/calllog/CallLogReceiver.java new file mode 100644 index 000000000..a781b0887 --- /dev/null +++ b/java/com/android/dialer/app/calllog/CallLogReceiver.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.provider.VoicemailContract; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler; +import com.android.dialer.app.voicemail.error.VoicemailStatusCorruptionHandler.Source; +import com.android.dialer.common.LogUtil; +import com.android.dialer.database.CallLogQueryHandler; + +/** + * Receiver for call log events. + * + *

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

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

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

There are two integers stored as metadata for every list item in the adapter. + */ +abstract class GroupingListAdapter extends RecyclerView.Adapter { + + protected ContentObserver mChangeObserver = + new ContentObserver(new Handler()) { + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + }; + protected DataSetObserver mDataSetObserver = + new DataSetObserver() { + @Override + public void onChanged() { + notifyDataSetChanged(); + } + }; + private Cursor mCursor; + /** + * SparseIntArray, which maps the cursor position of the first element of a group to the size of + * the group. The index of a key in this map corresponds to the list position of that group. + */ + private SparseIntArray mGroupMetadata; + + private int mItemCount; + + public GroupingListAdapter() { + reset(); + } + + /** + * Finds all groups of adjacent items in the cursor and calls {@link #addGroup} for each of them. + */ + protected abstract void addGroups(Cursor cursor); + + protected abstract void onContentChanged(); + + public void changeCursor(Cursor cursor) { + if (cursor == mCursor) { + return; + } + + if (mCursor != null) { + mCursor.unregisterContentObserver(mChangeObserver); + mCursor.unregisterDataSetObserver(mDataSetObserver); + mCursor.close(); + } + + // Reset whenever the cursor is changed. + reset(); + mCursor = cursor; + + if (cursor != null) { + addGroups(mCursor); + + // Calculate the item count by subtracting group child counts from the cursor count. + mItemCount = mGroupMetadata.size(); + + cursor.registerContentObserver(mChangeObserver); + cursor.registerDataSetObserver(mDataSetObserver); + notifyDataSetChanged(); + } + } + + /** + * Records information about grouping in the list. Should be called by the overridden {@link + * #addGroups} method. + */ + public void addGroup(int cursorPosition, int groupSize) { + int lastIndex = mGroupMetadata.size() - 1; + if (lastIndex < 0 || cursorPosition <= mGroupMetadata.keyAt(lastIndex)) { + mGroupMetadata.put(cursorPosition, groupSize); + } else { + // Optimization to avoid binary search if adding groups in ascending cursor position. + mGroupMetadata.append(cursorPosition, groupSize); + } + } + + @Override + public int getItemCount() { + return mItemCount; + } + + /** + * Given the position of a list item, returns the size of the group of items corresponding to that + * position. + */ + public int getGroupSize(int listPosition) { + if (listPosition < 0 || listPosition >= mGroupMetadata.size()) { + return 0; + } + + return mGroupMetadata.valueAt(listPosition); + } + + /** + * Given the position of a list item, returns the the first item in the group of items + * corresponding to that position. + */ + public Object getItem(int listPosition) { + if (mCursor == null || listPosition < 0 || listPosition >= mGroupMetadata.size()) { + return null; + } + + int cursorPosition = mGroupMetadata.keyAt(listPosition); + if (mCursor.moveToPosition(cursorPosition)) { + return mCursor; + } else { + return null; + } + } + + private void reset() { + mItemCount = 0; + mGroupMetadata = new SparseIntArray(); + } +} diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java new file mode 100644 index 000000000..879ac353d --- /dev/null +++ b/java/com/android/dialer/app/calllog/IntentProvider.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.ContactsContract; +import android.telecom.PhoneAccountHandle; +import com.android.contacts.common.model.Contact; +import com.android.contacts.common.model.ContactLoader; +import com.android.dialer.app.CallDetailActivity; +import com.android.dialer.callintent.CallIntentBuilder; +import com.android.dialer.callintent.nano.CallInitiationType; +import com.android.dialer.telecom.TelecomUtil; +import com.android.dialer.util.CallUtil; +import com.android.dialer.util.IntentUtil; +import java.util.ArrayList; + +/** + * Used to create an intent to attach to an action in the call log. + * + *

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

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

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

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

It can be injected in tests using {@link #setCurrentTimeForTest(long)}. + */ + private long getCurrentTimeMillis() { + if (mCurrentTimeMillisForTest == null) { + return System.currentTimeMillis(); + } else { + return mCurrentTimeMillisForTest; + } + } + + /** Sets the call count, date, and if it is a voicemail, sets the duration. */ + private void setDetailText( + PhoneCallDetailsViews views, Integer callCount, PhoneCallDetails details) { + // Combine the count (if present) and the date. + CharSequence dateText = details.callLocationAndDate; + final CharSequence text; + if (callCount != null) { + text = mResources.getString(R.string.call_log_item_count_and_date, callCount, dateText); + } else { + text = dateText; + } + + if (details.callTypes[0] == Calls.VOICEMAIL_TYPE && details.duration > 0) { + views.callLocationAndDate.setText( + mResources.getString( + R.string.voicemailCallLogDateTimeFormatWithDuration, + text, + getVoicemailDuration(details))); + } else { + views.callLocationAndDate.setText(text); + } + } + + private String getVoicemailDuration(PhoneCallDetails details) { + long minutes = TimeUnit.SECONDS.toMinutes(details.duration); + long seconds = details.duration - TimeUnit.MINUTES.toSeconds(minutes); + if (minutes > 99) { + minutes = 99; + } + return mResources.getString(R.string.voicemailDurationFormat, minutes, seconds); + } +} diff --git a/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java new file mode 100644 index 000000000..476996826 --- /dev/null +++ b/java/com/android/dialer/app/calllog/PhoneCallDetailsViews.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.calllog; + +import android.content.Context; +import android.view.View; +import android.widget.TextView; +import com.android.dialer.app.R; + +/** Encapsulates the views that are used to display the details of a phone call in the call log. */ +public final class PhoneCallDetailsViews { + + public final TextView nameView; + public final View callTypeView; + public final CallTypeIconsView callTypeIcons; + public final TextView callLocationAndDate; + public final TextView voicemailTranscriptionView; + public final TextView callAccountLabel; + + private PhoneCallDetailsViews( + TextView nameView, + View callTypeView, + CallTypeIconsView callTypeIcons, + TextView callLocationAndDate, + TextView voicemailTranscriptionView, + TextView callAccountLabel) { + this.nameView = nameView; + this.callTypeView = callTypeView; + this.callTypeIcons = callTypeIcons; + this.callLocationAndDate = callLocationAndDate; + this.voicemailTranscriptionView = voicemailTranscriptionView; + this.callAccountLabel = callAccountLabel; + } + + /** + * Create a new instance by extracting the elements from the given view. + * + *

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

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

This is designed with the specific use case of the {@link CallLogAdapter} in mind. + */ +public abstract class CallLogCache { + // TODO: Dialer should be fixed so as not to check isVoicemail() so often but at the time of + // this writing, that was a much larger undertaking than creating this cache. + + protected final Context mContext; + + private boolean mHasCheckedForVideoAvailability; + private int mVideoAvailability; + + public CallLogCache(Context context) { + mContext = context; + } + + /** Return the most compatible version of the TelecomCallLogCache. */ + public static CallLogCache getCallLogCache(Context context) { + if (CompatUtils.isClassAvailable("android.telecom.PhoneAccountHandle")) { + return new CallLogCacheLollipopMr1(context); + } + return new CallLogCacheLollipop(context); + } + + public void reset() { + mHasCheckedForVideoAvailability = false; + mVideoAvailability = 0; + } + + /** + * Returns true if the given number is the number of the configured voicemail. To be able to + * mock-out this, it is not a static method. + */ + public abstract boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number); + + /** + * Returns {@code true} when the current sim supports video calls, regardless of the value in a + * contact's {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} + * column. + */ + public boolean isVideoEnabled() { + if (!mHasCheckedForVideoAvailability) { + mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); + mHasCheckedForVideoAvailability = true; + } + return (mVideoAvailability & CallUtil.VIDEO_CALLING_ENABLED) != 0; + } + + /** + * Returns {@code true} when the current sim supports checking video calling capabilities via the + * {@link android.provider.ContactsContract.CommonDataKinds.Phone#CARRIER_PRESENCE} column. + */ + public boolean canRelyOnVideoPresence() { + if (!mHasCheckedForVideoAvailability) { + mVideoAvailability = CallUtil.getVideoCallingAvailability(mContext); + mHasCheckedForVideoAvailability = true; + } + return (mVideoAvailability & CallUtil.VIDEO_CALLING_PRESENCE) != 0; + } + + /** Extract account label from PhoneAccount object. */ + public abstract String getAccountLabel(PhoneAccountHandle accountHandle); + + /** Extract account color from PhoneAccount object. */ + public abstract int getAccountColor(PhoneAccountHandle accountHandle); + + /** + * Determines if the PhoneAccount supports specifying a call subject (i.e. calling with a note) + * for outgoing calls. + * + * @param accountHandle The PhoneAccount handle. + * @return {@code true} if calling with a note is supported, {@code false} otherwise. + */ + public abstract boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle); +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java new file mode 100644 index 000000000..78aaa4193 --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipop.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneNumberUtils; +import android.text.TextUtils; + +/** + * This is a compatibility class for the CallLogCache for versions of dialer before Lollipop Mr1 + * (the introduction of phone accounts). + * + *

This class should not be initialized directly and instead be acquired from {@link + * CallLogCache#getCallLogCache}. + */ +class CallLogCacheLollipop extends CallLogCache { + + private String mVoicemailNumber; + + /* package */ CallLogCacheLollipop(Context context) { + super(context); + } + + @Override + public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + String numberString = number.toString(); + + if (!TextUtils.isEmpty(mVoicemailNumber)) { + return PhoneNumberUtils.compare(numberString, mVoicemailNumber); + } + + if (PhoneNumberUtils.isVoiceMailNumber(numberString)) { + mVoicemailNumber = numberString; + return true; + } + + return false; + } + + @Override + public String getAccountLabel(PhoneAccountHandle accountHandle) { + return null; + } + + @Override + public int getAccountColor(PhoneAccountHandle accountHandle) { + return PhoneAccount.NO_HIGHLIGHT_COLOR; + } + + @Override + public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + return false; + } +} diff --git a/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java new file mode 100644 index 000000000..c342b7e3b --- /dev/null +++ b/java/com/android/dialer/app/calllog/calllogcache/CallLogCacheLollipopMr1.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.dialer.app.calllog.calllogcache; + +import android.content.Context; +import android.support.annotation.VisibleForTesting; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.Pair; +import com.android.dialer.app.calllog.PhoneAccountUtils; +import com.android.dialer.phonenumberutil.PhoneNumberHelper; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This is the CallLogCache for versions of dialer Lollipop Mr1 and above with support for multi-SIM + * devices. + * + *

This class should not be initialized directly and instead be acquired from {@link + * CallLogCache#getCallLogCache}. + */ +class CallLogCacheLollipopMr1 extends CallLogCache { + + /* + * Maps from a phone-account/number pair to a boolean because multiple numbers could return true + * for the voicemail number if those numbers are not pre-normalized. Access must be synchronzied + * as it's used in the background thread in CallLogAdapter. {@see CallLogAdapter#loadData} + */ + @VisibleForTesting + final Map, Boolean> mVoicemailQueryCache = + new ConcurrentHashMap<>(); + + private final Map mPhoneAccountLabelCache = new HashMap<>(); + private final Map mPhoneAccountColorCache = new HashMap<>(); + private final Map mPhoneAccountCallWithNoteCache = new HashMap<>(); + + /* package */ CallLogCacheLollipopMr1(Context context) { + super(context); + } + + @Override + public void reset() { + mVoicemailQueryCache.clear(); + mPhoneAccountLabelCache.clear(); + mPhoneAccountColorCache.clear(); + mPhoneAccountCallWithNoteCache.clear(); + + super.reset(); + } + + @Override + public boolean isVoicemailNumber(PhoneAccountHandle accountHandle, CharSequence number) { + if (TextUtils.isEmpty(number)) { + return false; + } + + Pair key = new Pair<>(accountHandle, number); + Boolean value = mVoicemailQueryCache.get(key); + if (value != null) { + return value; + } + boolean isVoicemail = + PhoneNumberHelper.isVoicemailNumber(mContext, accountHandle, number.toString()); + mVoicemailQueryCache.put(key, isVoicemail); + return isVoicemail; + } + + @Override + public String getAccountLabel(PhoneAccountHandle accountHandle) { + if (mPhoneAccountLabelCache.containsKey(accountHandle)) { + return mPhoneAccountLabelCache.get(accountHandle); + } else { + String label = PhoneAccountUtils.getAccountLabel(mContext, accountHandle); + mPhoneAccountLabelCache.put(accountHandle, label); + return label; + } + } + + @Override + public int getAccountColor(PhoneAccountHandle accountHandle) { + if (mPhoneAccountColorCache.containsKey(accountHandle)) { + return mPhoneAccountColorCache.get(accountHandle); + } else { + Integer color = PhoneAccountUtils.getAccountColor(mContext, accountHandle); + mPhoneAccountColorCache.put(accountHandle, color); + return color; + } + } + + @Override + public boolean doesAccountSupportCallSubject(PhoneAccountHandle accountHandle) { + if (mPhoneAccountCallWithNoteCache.containsKey(accountHandle)) { + return mPhoneAccountCallWithNoteCache.get(accountHandle); + } else { + Boolean supportsCallWithNote = + PhoneAccountUtils.getAccountSupportsCallSubject(mContext, accountHandle); + mPhoneAccountCallWithNoteCache.put(accountHandle, supportsCallWithNote); + return supportsCallWithNote; + } + } +} diff --git a/java/com/android/dialer/app/contactinfo/ContactInfoCache.java b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java new file mode 100644 index 000000000..4135cb7b8 --- /dev/null +++ b/java/com/android/dialer/app/contactinfo/ContactInfoCache.java @@ -0,0 +1,357 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.app.contactinfo; + +import android.os.Handler; +import android.os.Message; +import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import com.android.dialer.phonenumbercache.ContactInfo; +import com.android.dialer.phonenumbercache.ContactInfoHelper; +import com.android.dialer.util.ExpirableCache; +import java.lang.ref.WeakReference; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.PriorityBlockingQueue; + +/** + * This is a cache of contact details for the phone numbers in the c all log. The key is the phone + * number with the country in which teh call was placed or received. The content of the cache is + * expired (but not purged) whenever the application comes to the foreground. + * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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